Näppäimistöltä tulevien keskeytysten ja muiden signaalien käsittely C-ohjelmissa

Useissa käyttöjärjestelmissä on mahdollista keskeyttää (katkaista) suorituksessa oleva ohjelma jollakin näppäimellä tai näppäinyhdistelmällä, esim. control-C:llä tai Break-näppäimellä.

Kyseiset näppäimet tai näppäinkombinaatiot ovat tietysti käyttöjärjestelmästä riippuvia. C-kielen määrittelyssä asiaa tarkastellaan hiukan abstraktimmalla tasolla ns. signaalien avulla.

Kirjasto signal.h

Asiaan liittyvät hommat ovat standardikirjastossa signal.h. Siinä on määritelty joukko signaaleja sekä funktio signal, jolla voidaan yrittää liittää johonkin signaaliin joko jokin valmiiksi määritellyistä käsittelytavoista taikka ohjelmassa itsessään määritelty funktio.

Signaalit C-standardin mukaan
SIGABRT ohjelman epänormaali päättyminen, kuten abort-funktion aiheuttama
SIGFPE virheellinen aritmeettinen operaatio kuten nollalla jako tai ylivuoto; nimi johtuu sanoista "floating-point exception", mutta signaalin merkitys on siis ainakin periaatteessa yleisempi (mutta useissa järjestelmissä esim. liukulukulaskennan ylivuoto voi aiheuttaa SIGFPE:n, kokonaislukulaskennan ylivuoto ei)
SIGILL luvaton konekäsky (illegal instruction), tai yleisemmin "detection of an invalid function image"
SIGINT vuorovaikutteinen "huomiota pyytävä" signaali (interactive attention request)
SIGSEGV luvaton muistiviittaus; nimi johtuu historiallisista syistä sanoista "segmentation violation"
SIGTERM ohjelman saama lopettamispyyntö (termination request).

Signaalin käsittelytavaksi voidaan ilmoittaa SIG_IGN, joka tarkoittaa signaalin jättämistä huomiotta (engl. ignore), tai SIG_DFL, joka tarkoittaa järjestelmän oletusarvoista käsittelytapaa, taikka funktio. Viimeksi mainitussa tapauksessa funktion tulee olla tyypitön (void) ja sillä tulee olla yksi int-tyyppinen argumentti; signaali aiheuttaa funktion kutsumisen siten, että sille välittyy argumenttina signaalin koodi. Koodit ovat järjestelmäkohtaisia kokonaislukuja, mutta edellä mainitut SIGABRT ovat järjestelmäriippumattomia tapoja viitata näihin koodeihin.

Keskeytysten estäminen

Raaimmassa tapauksessa (tuollaiset keskeytysyritykset halutaan estää kokonaan) voi tehdä näin: sopivaan kohtaan jonnekin tiedoston alkuun

#include <signal.h>
ja sitten vaikkapa pääohjelman alkuun
signal(SIGINT, SIG_IGN);

Periaatteessa ei ole mitään takeita siitä, että tuo todella estää keskeytykset, koska on toteutuksesta riippuva asia, aiheuttaako control-C:n (tai vastaavan) hakkaaminen todella signaalin (ja tarkkaan ottaen minkä signaalin) C-kielen mielessä vai käsitteleekö käyttöjärjestelmä sen väkisin itse. Luultavasti se kuitenkin toimii useimmissa C-toteutuksissa sen keskeytystavan suhteen, joka kyseisessä järjestelmässä on tavallinen tapa katkaista ohjelman suoritus. Mutta mainittakoon, että esimerkiksi DJGPP:tä käytettäessä ohjelma toimii ns. protected modessa, jossa kyseisten keskeytysten saaminen ohjelman hallintaan ei onnistu.

Jos kokeillaan tyypillisessä Unix-ympäristössä, niin edellä kuvattu tapa estää control-C-keskeytykset vaan ei ns. quit-keskeytystä (joka kai tavallisimmin on liitetty control-\:aan). Jos senkin haluaa hoitaa niin on ehkä lisäksi sanottava

signal(SIGQUIT, SIG_IGN);
mutta tämä onkin sitten jo konekohtaista, ei standardi-C:tä; standardi ei tunne SIGQUIT-nimistä signaalia. Tyypillisessä Unix-koneessa saa lisätietoja homman hoitamisesta komennoilla
man 2 signal
man 4 signal
ja Unix-standardin mukaiset tiedot asiasta löytyvät dokumentaatiosta The Single UNIX® Specification (jonka lukeminen vaatii rekisteröitymisen mutta on maksutonta).

Varoitus: Mitä enemmän tuollaisia keskeytystapoja onnistuu ohjelmallisesti estämään, sitä varmempaa on, että kun ohjelma joutuu omituiseen tilaan kuten ikiluuppiin, sitä ei saa enää kirveelläkään poikki. Tai ehkä juuri kirveellä. :-) Kun joskus noita kokeilin, sain aikaan tilanteita, joissa olin loppujen lopuksi onnellinen, etten ollut huomannut estää SIGKILL-keskeytyksiäkin. :-) Tosin niiden estämisen ei spesifikaatioiden mukaan pitäisi olla mahdollistakaan.

Aihepiiriä käsittelee myös C-fakin kohta 19.38: How can I trap or ignore keyboard interrupts like control-C?

Oma keskeytystenkäsittely

Seuraavassa on yksinkertainen keskeytyksenkäsittelyrutiini tilanteisiin, joissa halutaan, että kesken laskennan voi pyytää ohjelmalta tietoja laskennan tilasta.

Tämä kävisi seuraavaan tapaan:

long kierros; 
void hoida(int i) {
  int merkki;
  printf("Kierros %ld\n", kierros);
  signal(SIGINT,&hoida);
  return; }
int main(void) {
  signal(SIGINT, &hoida);
...

Tässä kierros on globaali muuttuja, jota sitten käytetään kierroslaskurina laskentasilmukassa. Realistisessa tilanteessa keskeytyksenkäsittelyrutiini tulostaisi muutakin tietoa tilanteesta.

Rutiinin sisällä on kutsu signal(SIGINT,&hoida);, koska C-standardin mukaan järjestelmä voi signaalinkäsittelyrutiinin alussa suorittaa automaattisesti operaation signal(signaali,SIG_DFL). Kutsu siis huolehtii siitä, että signaalin sattuessa myöhemmin sen käsittelee oma rutiinimme eikä järjestelmän oletuskäsittely.

Ota huomioon edellä esitetty varoitus: jos SIGINT-signaalin ottaminen "ohjelman haltuun" onnistuu, ohjelmaa ei ehkä pysty katkaisemaan normaalilla tavalla! Ellei tiedetä, että jokin muu katkaisumenetelmä toimii, kannattaisi rutiiniin lisätä koodi, joka tietojen tulostamisen jälkeen kysyisi käyttäjältä, haluaako hän katkaista ohjelman suorituksen. (Tästä taas seuraisi, että ohjelman suoritus on pysähdyksissä, kunnes käyttäjä vastaa kysymykseen.)

Varoitus

Signaalien käsittely on hankalampaa kuin voisi luulla. Erityisesti mainittakoon, edellä mainittujen järjestelmäriippuvuuksien lisäksi, että normaali paluu (return;) SIGFPE:n käsittelevästä funktiosta ei ole sallittu. Täten aritmeettisten virheiden käsittely vaatii lisätoimenpiteitä, jos ohjelman suoritusta halutaan jatkaa virhetilanteissa. Ks. Korpela - Larmela: C-ohjelmointikieli, kohta 24.9 Signaalit - signal.h.


Jukka Korpela.
Kirjoitetty 1998-09-09, täydennetty 2000-07-04 ja 2003-01-09.