Computer Science/시스템 프로그래밍

[시스템 프로그래밍] Chapter 8

seungwon9201 2024. 5. 9. 21:10

기본적인 signal개념

시그널이란 이벤트가 발생했을 때 타깃프로세스에게 알려주기 위한 수단으로 사용되는 software notification이다.

시그널의 liftime은 해당이벤트가 발생했을때 생성되고 발생된 시그널은 타깃프로세스에게 전달되고 프로세스가 시그널을 전달받으면 delivered 됐다고 말한다. 프로세스가 시그널을 받으면 수신한 시그널에 대한 처리를 하도록 되어있음. 그 처리를 마치고 난 뒤에 삭제된다. 그러나 항상 타깃프로세스에게 시그널이 전달되는 것이 아니고 막힐 수 있음(이러한 시그널을 pending signal) pending signal은 바로 삭제되는 것이 아니라 pending signal list에 들어가서 대기하게 된다. 프로세스는 도착한 시그널을 캐치해서 수신한 시그널에 대한 처리를 할 수 있다. 

 

sigaction함수 : 프로세스가 시그널을 받았을때 어떤 애플리케이션 개발자가 정의한 다른 특정 안 액션을 수행하게 하고 싶을 때 signal handler함수를 정의하고 signal handler함수를 등록해 주는 함수이다. 즉, signal을 받았을 때 어떤 액션을 취할 것인가 등록하는 함수이다. 또한 sigaction함수를 통해서 constant 한 값을 등록할 수 있다. ex) SIG_DFL(signal이 도착하면 default액션을 수행해라) or SIG_IGN(signal이 도착하면 signal을 무시해라)

 

sigpromask함수 : 프로세스들은 기본적으로 도착하는 시그널을 막는 객체를 가지고 있다. 이것을 signalmask라고 한다. 이 함수를 통해서 프로세스의 signalmask를 컨트롤할 수 있음. signalmask에 signal번호를 등록을 해서 등록된함수가 도착을 하면 막히게 된다(pending 된다). 이처럼 signalmask를 컨트롤해야 할 때 사용하는 함수이다.


 

Generating signals

모든 시그널은 symbolic name과 유니크한 ID값을 가지고 있다. 시그널의 이름은 모두SIG로 시작한다.

kill명령어 혹은 kill함수를 통해서 시그널을 전송할 수 있다.

kill 명령어는 두개의 파라미터가 있다. 몇 번 시그널을 보낼 건지 누구에게 보낼 건지 정해야 한다.

ex) kill -s USR1 3423 : USR1이란 signal을 3423번 프로세스로 보내겠다는 뜻(지정할 때는 앞에 SIG를 빼고 지정함)

kill -l로 kill명령어를 확인할 수 있음. 8장에서 주로 사용할 kill명령어는 SIGKILL(9), SIGINT(2), USR1

ex) kill -9 1234를 입력하면 1234 프로세스를 종료시킬 수 있음

kill함수로 signal을 보내보자. kill함수의 첫번째 파라미터는 타깃프로세스ID이고 두번째 파라미터는 전송하려고 하는 시그널 번호이다.첫번째 파라미터에 0이  오는 경우에 이 시그널을 호출한 프로세스와 같은 프로세스 그룹에 속한 모든 프로세스에게 전달하겠다라는 의미, -1이면 허가된 모든 프로세스에게 siganl을 전달, -10이라면 절대값으로 변환해서 10이라는게 프로세스 그룹에 ID이기때문에 프로세스ID가 10인 그룹의 모든 프로세스를 전달한다는 의미이다.

 

signal을 보내는 함수중에서 특정목적을 위해서 만들어진 함수가 있다. 첫번쨰 함수는 raise함수이다. raise함수는 나 자신에게 파라미터로 지정한 signal을 지정한 프로세스에게 signal을 보내는 함수. 나 자신에게 시그널을 보내야할 필요가 있을때 사용하자.두번째 함수는 alarm함수이다. 알람 함수는 타이머의 역할을 하는 함수이다. 파라미터에 초 단위의 숫자값을 입력을하면 시간이 다 된뒤에 alarm signal을 보낸다. 만약 파라미터값을 0으로 준다면 기존에 요청한 알람을 취소하는 역할을 한다. alarm signal을 받으면 기본적으로 종료한다. 알람함수의 리턴값의 의미는 이전에 설정되었던 남아있는 초 값을 의미한다(중단되었을때의 남아있는 시간). 시간이 다 되면 0을 리턴한다. 밑에 예제는 알람함수를 호출하면 sleep처럼 10초가 흘러갈때까지 멈추는것이 아니고 이 알람함수는 알람을 설정해달라고 하는 함수이기 때문에 알람이 설정되면 바로 리턴된다. 즉, 10초가 지나서 알람 시그널을 받으면 프로그램을 종료한다.
sigaddset함수는 signal set타입에 변수에다가 signal을 추가하겠다는 함수이다. 첫번째 파라미터는 signal set변수 포인터 값이고 두번쨰 파라미터는 내가 넣고자 하는 siganl번호이다. sigdelset함수는 signal set으로부터 signal번호를 빼고 싶을떄 사용한다. sigemptyset함수는 현재 signal set안에 내용을 모두 비우겠다라는 함수이다. 보통 signalset타입의 변수를 선언하고 이 변수를 초기화하기 위해서 sigemptyset함수를 호출한다. 그래서 이 변수를 초기화 시킨다. sigfillset함수는 반대로 초기화 하는것이다. signal set변수안에 모든 signal 번호를 다 등록을 하는 함수이다. sigismember함수는 membership을 확인하는 함수이다.현재 첫번째 signal set에 두번쨰 파라미터의 signal번호가 있는지 없는지 확인하는 함수로 있으면 1이 ,없으면 0이 리턴된다. 나머지 4개의 함수는 성공하면 0을리턴하고 실패시 -1을 리턴한다.
이렇게 signal set을 가지고 sigpromask를 호출해서 이 함수를 호출하는 프로세스의 signalmask를 제어할 수 있다. 첫번쨰 파라미터는 signal을 추가할거냐 뺄거냐 어떻게 수정할거냐를 보는거고 두번째 파라미터는 signal set타입을 넘겨주는거다.이 안에는 수정하려고 하는 번호들이 들어가있다. 마지막 파라미터는 변경되기전의 원래의 siganlmask의 signal정보를 반환을 해준다. 마지막 파라미터를 왜 쓰냐면 잠깐 signalmask를 변경했다가 다시 원래의 signalmask로 돌아가기위해서 사용한다. 저정할필요가없다면 마지막은 NULL.

 

signalmask : 프로세스가 블록 시키려고 하는 signal의 목록을 가지고 있는 것. signalmask에 유지되는 목록은 signal set타입으로 관리가 된다. 첫 번째 파라미터의 의미는 integer타입의 how인데 signalmask를 어떤 식으로 수정하려고 하는지를 지정하는 것(전에 sigpromask함수를 통해서 signalmask에 signal을 추가할 수도 있고 뺄 수도 있다고 했는데  뺄 건지 더할 건지 정하는 것)

ex) SIG_BLOCK : 첫 번째 파라미터로 이걸 지정하면 두 번째 파라미터인 set을 signalmask에 추가하겠다는 의미이다, 첫번째 파라미터는 SIG_BLOCK을 주고 signal set안에 1, 2번 signal을 추가했다고 하고 이 signal set을 sigpromask함수의 두번째 파라미터로 지정을했다. 이러면 이 프로세스의 signalmask에 1,2번 signal을 추가하겠다는 의미이다.

ex) SIG_UNBLOCK : 반대로 두번째 파라미터로 지정한 signal set안에 있는 signal번호를 signalmask에서 빼겠다는 의미이다.

ex) SIG_SETMASK : 두번째 파라미터로 지정한 signal들로 signalmask의 내용을 새로 설정을 하겠다는 의미이다. 기존에 있는 signal을 무시하고 새로 넣겠다는 의미이다.  

sigpromask함수는 single 스레드 프로세스에서 사용을 해야 한다. 만약 다중 스레드의 프로세스라면 이 프로세스의 signal을 변경을 하면 signal이 어느 스레드로 전달이 될지 애매할 수 있다. 그렇기 때문에 특정스레드에게 signal을 보내고자 한다면 스레드 레벨에서 signalmask를 제어하는 함수인 pthread_sigmask() 함수를 사용하자. 지금은 싱글스레드 기반의 프로세스라고 가정을 하고 그때 프로세스 레벨에 signalmask를 제어하기 위한 함수가 sigpromask함수이다. 그러나 SIGSTOP, SIGKILL signal은 signalmask로 막을 수가 없다. 

 

첫번째 코드는 인터럽트 시그널을 signalmask에 추가하고자 하는경우이다 (CTRL+C를 눌러도 인터럽트가 안되도록하기)

 

 

POSIX에서 기본적으로 void 리턴타입과 int 파라미터 하나를 가지고 있는 함수를 signal handler함수로 사용할 수 있다. 그러나 특정한 다른 파라미터를 signal handler함수에 넘겨주는 것이 불가능해서 POSIX:RTX와 POSIX:XI extension버전에서  sa_sigaction을 이용해서 별도의 파라미터를 넘겨줄 수 있게 되었다. 그래서 액션 구조체에 두 가지 타입의 핸들러를 등록할 수 있는데 3번 필드를 통해서 어떤 핸들러 시크널을 사용할지 정할 수 있다. 그리고 위처럼 시그널핸들러 타입, SIG_DFL이나 SIG_IGN(시그널을 무시해라, 버려라)으로 등록할 수 있다.

 

반복문을 돌면서 0과 1 사이에 인터벌 사이에 있는 sin(x)의 평균값을 계속 계산하는 프로그램이 있는데, 반복해서 계산을 하던 중에 사용자가 Ctrl+c를 눌러서 인터럽트 시그널을 발생을 시킨다. 원래프로그램은 돌아가다가 인터럽트를 받으면 기본적으로 종료가 되어야 하지만 아래의 예제는 반복문을 돌고 있던 중에 인터럽트 시그널이 도착을 하면 시그널 핸들러를 등록을 한다. 그곳에서는 flat 변수를 선언을 해서 그 변숫값을 변경을 하는 시그널 핸들러이다. 그래서 시그널핸들러를 불렀다가 리턴을 하면 시그널이 와서 멈췄던 부분을 계속 진행을 한다. 

즉, 정리를 하면 프로그램이 돌다가 시그널이 개입하게 되면 메인프로그램은 다음에 실행할 것을 중단하고 시그널을 먼저 처리하려고 한다. 그래서 시그널핸들러 루틴으로 점프를 한다. 시그널핸들러 루틴을 실행을 하고 이것이 끝나면 돌아와서 멈췄던 부분부터 다시 실행하도록 되어 있다.

main함수부터 보면 시그널이 오게될경우에 default 액션이 아니라 내가 따로 정한 액션을 수행하기 위해서 sigation구조체 타입의 변수act를 선언한다. 나머지 변수는 계산을 위한 변수이다. 그 후에 setdoneflag라는 함수를 등록한다.(위에 정의되어있음) 이함수는 doneflag함수값을 1로 설정하는 함수이다. done flat 함수는 맨위에 정의되어 있음. while문을 보고 분석하면 doneflag 값이 0이면 반복문은 계속되고 시그널을 받았을때 doneflag값이 1로 바뀌어서 반복문은 중단이 된다. 즉, doneflag값을 1로 바꾸는 이러한 함수를 sigaction구조체에 등록을 한것이다.

원래 기존의 프로그램이 돌아갈 때 Ctr+c를 누르면 계산 프로그램이 중단이 되었는데, signal handler를 등록을 하게 되면 정상적으로 종료된다. 

시그널 핸들러를 사용하면 중간에 소스코드를 점프해서 돌아오는 특성 때문에 고려해야 할 사항이 생겼다. donelflag변수는 critical section으로 만들어서 처리를 해야 한다. 즉, 이 변수를 여러스레드가 동시에 수정을하면 충돌문제가 발생하니 여러 프로세스가 동시에 액세스 하지못하도록 해야한다. 위의 코드를 보면 doneflag 값을 int타입으로 선언한것이 아니라 sig_atomic_t로 선언을 헤서 critical section으로 만든것이다. 이러한 타입으로 선언하면 이 엑세스가 종료될때까지 os가 다른 애들이 엑세스 하는것을 막아준다. 그 옆에 volatile의 용도는 doneflag를 참조할때 직접 메모리에서 참조하라고 선언한것이다. 즉, volatile로 선언을 하게 되면 매번 doneflag변수를 엑세스할때 레지스터에 로드된 값을 엑세스 하지 말고 매번 메모리에서 직접 읽으라는 것이다. doneflag는 외부에서 변경될 여지가 있기 때문에 레지스터에서 읽게 될 경우에 외부(여기선 시그널 핸들러를 의미)에 의해서 변숫값이 변경이 되더라고 변경되기 전의 값을 참조할 수 있기 때문에 매번 메모리에서 읽으라는 것이다.

 

아래 예제도 위와 유사하게 반복문을 돌면서 계산하는 작업을 수행하는 프로그램이다. 다른 점은 10000번마다 버퍼에 중간계산 결과를 버퍼에 저장을 하는 프로그램이다. 그래서 SIGUSR1 신호를 받으면 지금까지 버퍼에 저장한 중간계산결과를 화면에 출력하는 예제이다. 여기서도 메인프로그램과 시그널핸들러가 버퍼를 동시에 액세스 할 수 있다. 그렇기 때문에 버퍼를 ciritical section으로 만들어야 한다. buf는 int타입이 아니기 때문에 위에서 했던 방식으론 할 수 없다. 아래 코드를 보자.

전역변수로 캐릭터 버퍼를 선언하고 시그낼 핸들러 함수를 보면 write함수를 써서 표준출력장치에다가 버퍼의 내용을 buflen만큼 출력을한다. 아래에 results함수는 10000번째마다 버퍼에 쓰는작업을 구성한 함수이다. 이 함수는 snprintf함수를 통해서 버퍼의 사이즈 만큼 스트링값을 버퍼에 쓰고 맨 끝에 null캐릭터를 추가해서 snprintf가 버퍼의 내용이 스트링이 되도록 구성을 해준다. results함수에서 sigpromask함수를 호출을한다. 거기에 SIGUSR1을 넣어놔서 pending시키고 변경되기전 정보인 oset을 가지고 아래에 다시 sigpromask를 호출하여 엑세스할 수 있도록 한다.

 

메인함수에서는 for문으로 10000번째마다 result함수를 호출해서 중간계산결과를 버퍼에 쓴다. 그래서 계속 계산작업을 한다. 중간에 SIGUSR1이 오게되면 점프해서 시그널핸들러로 이동을 한다. 그 후에 시그널핸들러 함수로 뛰어서 버퍼의 내용을 출력한다.

그럼 어떻게 critical section으로 만들까? result함수에서 버퍼 안에 있는 내용을 다 쓰고 난 다음에 시그널핸들러가 불리면 될 것이다. 앞에서 시그널을 제어하기 위한 함수를 배웠었다. 바로 signalmask함수이다. 그래서 메인 프로그램이 버퍼를 액세스 하는 동안 sigpromask함수를 써서 SIGUSR1을 막아두면 된다. 그래서 SIGUSR1이 오더라도 pending이 되어서 방해받지 않고 액세스 할 수 있다. 버퍼 액세스가 끝난 다음에 sigpromask함수를 호출을 해서 막았던 SIGUSR1이 다시 프로세스에 도착을 하도록 해야 한다. 그래서 sigpromask사이의 공간을 critical section이 될 수 있다. 


시그널을 기다리는 코드가 필요한 경우가 있다. 이때는 pause(), siguspend(), sigwait() 3가지 함수가 있다.

프로세스가 pause함수를 호출을 하면 이 함수를 호출한 스레드를 중단시키고 시그널이 도착할 때까지 대기시킨다. user-defined 핸들러가 등록이 되어있고 그 핸들러가 호출될 수 있는 타깃시그널이 오거나 프로세스가 종료되는 시그널이 도착하면 pause함수가 리턴이 된다. 주목적은 아무 시그널이 오면 꺠어났다가 다시 잠들고 타깃 시그널이 오면 작업을 수행하고 싶을 때 사용한다. pause함수는 항상 -1을 리턴하고 만약 시그널이 프로세스에서 캐치될 경우, 시그널이 도착하면 시그널 핸들러가 불리고 시그널핸들러가 다 불리고난뒤에 pause함수가 리턴된다. 

 

8.21 예제는 내가 원하는 시그널이 도착할 때까지 기다리도록 짠 거다. 윗줄은 critical section으로 만들고 sigreceived를 0으로 선언을 했다. 여기선 나와있지 않지만 시그널이 올경우에 내가 원하는 시그널이 오면 sigreceived값을 1로 바꾸는 시그널 핸들러가 등록이 되어있는 상황이다. 그래서 원하는 시그널이 오게 되면  1로 변수를 바꾸고 while문을 빠져나가게 된다. 그러나 이 코드는 문제가 발생할 수 있는 코드이다.

 

sigreceived변수를 테스트하는 부분과 pause함수를 호출하는 부분 사이에 타깃시그널이 도착하게 되면 무슨 일이 발생할까? 도착한 시그널은 pause함수에 영향을 주지 못한다. 왜냐하면 시그널이 도착하고 난 다음에 pause함수가 불렸기 때문이다. 그래서 다음 시그널이 도착할 때까지 리턴되지 않기 때문에 다음시그널이 오지 않게 된다면 깨어나지 못하고 다음작업을 수행하지 못할 것이다.

 

이 문제를 해결하기 위해서  시그널을 unblocking 하고 pause함수를 호출하는 작업을 한 번에 수행을 하면 해결할 수 있다. 예를 들어서 8.21 예제에서 사이공간에 sigpromask함수를 써서 타깃시그널을 막는다. 막은 시그널을 unblock(풀고) pause함수를 호출하면 되는데, unblock 하고 pause함수를 부르기 전에 또 시그널이 도착할 수 있는 문제가 있다. 그래서 이 사이에 끼어들지 못하도록 동시에 수행하도록 하자. (sigsuspend 함수를 쓰자)

파라미터로는 sigmask타입의 변수가 온다. 파라미터로 넘긴 sigset으로 sigmask를 설정을하고 프로세스를 suspending하는 작업을 동시에 한다. 이 함수도 프로세스가 시그널을 캐치하면 리턴이 된다. 그래서 sigset타입의 파라미터인 sigmask가 타깃시그널을 unblock하는 용도로 사용된다.먼저 타깃시그널을 signalmask로 막아두고 그다음에 풀면서 suspend를 하기위해서 sigsuspend함수를 호출을 할때 파라미터로 타깃시그널을 뺀 signalset을 넣어준다. 그래서 sigsuspend함수를 부르면서 타깃 시그널을 unblock한다. sigsuspend가 리턴이 되면 다시 원래상태로 signalmask가 자동으로 복구가 된다.
핵심 부분은 if문이다. sigreceived를 0으로 선언하고 sigset변수를 3가지를 준비했다. maskall은 모든 시그널을 담고 maskmost는 타깃시그널만 빼둔 시그널이다.그래서 모든 시그널을 호출하고 시그널을 뺸 maskmost도 호출한다음에 두개를 뺴서 타깃시그널만 남는다. 그래서 sigpromask함수를 maskall로 호출해서 모든 시그널을 다 막고 pending시킨다. 원래상태로 돌리기위해 maskold를 사용함. 그다음에 sigsuspend를 호출하면서 maskmost를 지정을 해서 타깃시그널만 빠진 설정으로 바꾼다. 즉 타깃시그널만 올 수 있다.
이 버전은 다른시그널도 통과되도록 수정한것이다. 다른시그널도 도착할수 있기에 while문으로 바꿨음. 처음에 sigpromask함수를 써서 maskblocked와 maskunblocked의 sigset의 현재 시그널 상태값을 가져온다.maskblocked에는 오리지널sigmask에 타겟시그널을 추가를 한다. maskunblocked에는 오리지널 sigmask에서 타깃 시그널은 뺸다. 그리고 sigpromask함수를 써서 타깃시그널을 막고 while문을 돌린다.(코드 사이에 타깃시그널이 오면 문제가 발생하기 때문)그 다음 while문에서 프로세스를 suspend시킴과 동시에 막아뒀던 타깃시그널을 sigmask에서 빼고 프로세스는 잠든다. 그래서 다른 시그널도 도착을 하면서 타깃시그널이 오면 기다렸던 task를 수행할 수 있다.33
위의 예시는 잘못사용한 예시이다.결과적으로sigmost라는 sigset은 다른 모든 시그널들은 다 들어가 있고 타깃 시그널만 빠져있는 상황이다.이 상태로 만들어두고나서 suspend함수를 호출을하자. 그러나 여기코드 윗부분에 suspend함수를 호출하기 전에 타깃시그널이 올 수 있는 상황이라 일단 sigpromask함수로 타깃 시그널을 막아서 pending이 되도록 한 다음에 아래작업을 해야한다.

즉, 모든 시그널을 다 막고 타깃만 열든가, 모든 시그널을 열고 그 대신 타깃 시그널만 막았다가 suspend로 기다려서 정해야 한다.

 

또 다른 시그널을 기다르는 함수로 Sigwait함수가 있다.

이 함수를 호출하면 프로세스는 블락이 된다. 언제까지 블락이 되냐면 첫번째 파라미터에서 지정한 시그널중에서 아무거나 pending이 되면 sigwait함수는 pending된 시그널을 pending list에서 삭제를 하고 리턴을 한다. 리턴을 하면서 pending되서 삭제한 시그널 번호를 두번째 파라미터로 반환한다. 그러나 시그널 핸들러가 호출되는 것은 아니다. 왜냐하면 삭제된 시그널이 프로세스로 전달되지 않기 때문이다. 이 함수가 성공하면 0 실패시 -1을 리턴한다. 그럼 sigwait함수를 가지고 어떻게 내가 원하는 시그널이 도착할때까지 기다리지?

sigsuspend와 다른 점은 sissuspend함수의 파라미터에는 타깃 시그널을 빼고 파라미터에 집어넣었는데, sigwait함수의 파라미터에는 내가 타깃 시그널을 넣어두고 호출을 한다. 호출할 때도 동일하게 sigpromask함수에서 타깃시그널을 막고 시작한다. sigsuspend함수는 signalmask를 직접적으로 수정했는데 sigwait는 건드리는 일이 없다. 

 

예제를 한번보자. 타깃시그널은 SIGUSR1이다. sigwait함수를 호출할때 우리가 타깃시그널을 sigmask에 넣어서 블럭시키고 호출해야한다. 그래서 sigaddset으로 타깃시그널을 추가하고 sigpromask함수를 SIG_BLOCK으로 호출을 한다. 즉, SIGUSR1이 오면 pending시키겠다는 말임. for문으로 들어가서 sigwait함수를 호출한다. 그래서 SIGUSR1이 pending되면 리턴된다. 그래서 삭제한 시그널 번호를 리턴해서 두번째 파라미터인 &signo에 반환을 해준다. 그래서 sigwait함수는 타깃시그널이 와서 pending되면 리턴이된다.(여기서 시그널 번호는 프로세스에 전달되지 않음). 깨어나고 난 다음에시그널 카운터라는 변수를 하나 증가시킨다.

시그널 핸들러를 사용하지 않고 sigwait함수가 sigmask를 건드리지 않는다는 점이 sigsuspend와의 차이점이다. 


Error and Async-signal safety

시그널 핸들링을 할 때 고려해야 할 점 3가지

1. 시그널에 의해서 인터럽트 되는 POSIX함수는 다시 시작을 해야 하는지 말아야 하는지

  • 리턴값이 -1이고 에러코드가 EINTR이라면 그 함수는 중간에 자기 task를 수행하지 못하고 외부 시그널 요인에 의해서 인터럽트 돼서 -1을 리턴하는 함수도 있고 그렇지 않은 함수도 있으니 인터럽트 될 수 있는 함수라면 다시 호출을 하든지 r_로 개선을 하는 함수를 써야 한다. 먼저 내 함수가 인터럽트 되는지 안되는지 확인을 해야겠다.

2. 시그널 핸들러가 nonreentrant 한 함수를 호출할 경우에 위험할 수 있다.

  • nonreentrant 함수란 함수가 동작이 끝나지 않았는데 다른 곳에서 호출을 하더라도 문제없이 실행되는 것을 의미한다. 결국 reentrant 한 함수를 호출해야 한다. 시그널 핸들러 안에서 호출을 해도 안전한 함수를 async-signal safe 한 함수라고 정의한다. POSIX에서 제공하는 함수들 중에서 의외로 많은 함수들이 async-signal safe가 많지 않다. ex) printf -> write함수로 변경하자.

3. 시그널핸들러 안에서 errno변수를 다룰 때 주의해야 할 사항이 있다. 

  • main함수에서 에러가 발생해서 에러코드에 리턴하고, 시그널 핸들러에서 systemcall함수를 호출했는데 에러가 발생하고 에러코드를 설정을 했는데 그 에러코드가 main함수에 있는 에러코드를 덮어써버릴 수가 있다. 그래서 main의 에러코드가 없어져서 시그널핸들러의 에러코드를 main에서 보게 된다.  이러한겻을 피하기 위해서 errno변수를 임의의 변수에 저장을 하고 난 뒤에 사용을 하자. 

 

시그널 핸들링을 할 때 유용한 규칙들

1. 내가 호출한 systemcall 함수가 시그널에 의해서 인터럽트 됐을 때, 의심스러운 상황일 경우에는 restart library함수를 호출을 하든지 다시 시작을 해라.

2. 시그널핸들러 함수에서 사용하는 라이브러리 함수들을 확인을 해서 그 함수들이 async-signal safe 한 함수인지 확인을 하고 시그널핸들러 안에서 사용을 해라

3. 시그널 핸들러와 메인프로그램 코드 부분에서 공유할 수 있는 사항은 충돌이 나지 않도록 조심해라(sigmask를 제어하든가 sig_atomic타입으로 변수를 선언을 하자)

4.  시그널핸들러 안에서 errno를 사용할 때 임시로 저장하고 저장한 변수를 다시 복구해서 사용하자.


Program control

 

프로그램들이 실행구문을 점프할 때가 때때로 필요할 때가 있다. 프로그램들이 때때로 시그널을 이용을 해서 에러를 핸들링하는 데 사용할 수 있다. 

ex) 긴 계산작업을 하는 프로그램이 인터럽트가 되어서 오랫동안 작업한 내용을 잃어버리고 종료되는 경우가 발생할 때, 이 상황을 피하기 위해서 인터럽트가 되면 처음 부분으로 돌아가는 것처럼 실행시퀀스를 변경을 하자.

간접적인 방법 : flag변수를 설정을 해서 이 변수가 무슨 값이냐에 따라서 무슨 프로그램이 실행되는지 짤 수 있다. 그러나 복잡하다.

직접적인 방법 : POSIX에서 제공하는 sigsetjmp, siglongjmp 두 개의 함수를 사용해서 우리가 원하는 지점으로 점프를 하는 함수를 사용할 수 있다. 

sigsetjmp : 점프할 지점을 설정하는 함수

siglongjmp : 아까 설정했던 지점으로 점프시키는 함수 

처음에 점프할 지점을 설정할떄 sigsetjmp를 사용하고 이 함수의 첫번째 파라미터에는 점프했을때의 필요한 내용을 저장한다.두번째 파라미터에는 savemask의 값이 0이 아니라면 현재 sigmask의 상태도 저장을 한다.sigsetjmp함수가 다음에 점프할 위치를 설정하면 0을 리턴한다. 나중에 다시점프해서 돌아온 경우에는 siglongjmp에서 지정한 값이 리턴될 수도 있다. siglongjmp의 첫번째 파라미터는 어디로 점프할지를 지정한다. 두번째파라미터로는 리턴될 값을 지정한다.
예제를 보자.


programming with Asynchronous I/O

 

지금까지 했던 함수들은 기본적으로 synchronous이다. synchronous는 호출을 하면 응답을 기다리는 함수이다. read, write함수들은 한바이트라도 읽거나 쓰면 태스크를 수행했다고 봤었다. 그러나 이 함수들을 asynchronous로 호출을 하고 읽거나 쓸 바이트가 없다면 바로 리턴을 해버린다. 리턴을 하지만 OS에 의해서 백그라운드로 읽거나 쓰는 동작은 계속한다.

 

장점 : 기다리지 않고 다른 태스크를 할 수 있음

단점 : 백그라운드로 진행된 함수가 완료됐는지 알 수 있는 방법이 필요하다.

 

POSIX에선 aio_read, aio_write, aio_return, aio_error 같은 함수들이 있다. 이러한 함수들은 asynchronous 하게 요청을 해도 백그라운드로 완료된 정보를 알려준다. 대신 이러한 함수들은 synchronous함수보다 로직이 복잡해질 수 있다는 단점이 있다. 

 

read와 write함수 모두 3개의 파라미터가 필요했다.(fd, buf, nbytes) 그러나 asynchronous한 함수들은 aioch타입의 구조체로 이루어져있다. 이 구조체안에 3개의 파라미터가 들어가 있다.

aio_read함수는 읽기 작업을 위한 요청이 queue에 들어가고 바로 리턴이 된다.

aio_write함수도 쓰기 작업을 위한 요청이 queue안에 들어가고 바로 리턴이 된다.

 

구조체 안에 들어가있는 field부분이다.aio_sigevent필드를 사용할 때가 있다. 다른 task를 하다가 백그라운드로 돌아가는 함수가 됐는지 안됐는지 확인할때 사용한다. 즉, I/O가 완료됐는지 이거로 통보받을건지 아닌지 결정하고 몇번 시그널로 받을건지도 정할 수 있다. sigev_notify필드값을 SIGEV_NONE으로 하면 통지받지 않겠다는 거고 SIGEV_SIGNAL로 하면 통지를 받겠다는거고 aio_sigevent.sigev_signo에 번호를 등록을하면 어느번호로 받을지 정할 수 있다.

 

이 두개의 함수는 완료된 I/O의 리턴값을 따로 받기위해 호출하는 함수이다. aio_return함수를 호출하기 위해서는 I/O가 완료됐는지 알아야한다.시그널로 통지를 받을수도있고 수동으로 aio_error함수(I/O의 진행사항을 모니터링하는 함수)를 호출해서 진행사항을 파악할 수 있다.aio_error함수의 리턴값이 0이면 성공이고 여전히 실행중이면 EINPROGRESS가 반환되고 문제가 있으면 에러코드가 직접 리턴이 된다.
이 함수를 호출하면 파라미터로 지정한 asynchronous한 I/O가 완료될때까지 아무것도 안하고 기다린다라고 할떄 호출할 수 있는함수이다. 첫번째 파라미터에는 aioch구조체의 array, 크기, timeout값을 넘겨준다.
asynchronous하게 요청한 I/O를 중단하고 싶을때 호출한다. 첫번째 파라미터는 cancel하고싶은 대상을 지정을 하고 실제 요청했던 aiocbp구조체를 두번째 파라미터로 지정한다. 만약 두번째 파라미터가 NULL이고 fd만 지정할 경우에 이 fd에 관한 모든 asynchronous한 요청을 취소해달라는 것이다. AIO_CANCELED값이 리턴되면 성공적이고 NOTCANCELED라면 취소되지 못한 I/O가 있다는 거고 이미 완료됐음에도 호출을 할 경우에 AIL_ALLDONE이 호출된다