Bob Cheng
System Programming

Signal

當特定事件發生時,會發送信號來通知 user process

  • 非同步 : 可以發生在任何時間、每個信號之間沒有順序之分、也不會互相等待
  • 可以被看成是另類的 IPC
  • 每個信號都有自己的代號 (SIG...) 以及代碼 (positive integer defined in <signal.h>)
  • 不同的系統支援的信號種類不同 (ex: Unix version 7 只支援 15 種信號)

信號運作流程

Signal Types

  • Terminal-generated signals
    • SIGINT (2): sent when ^c was pressed
    • SIGKILL (9): sent to terminate the process
  • Signals from hardware exceptions
    • SIGFPE (8): divided-by-zero
    • SIGSEGV (11): illegal memory access (==Segmentation Fault==)
  • Signals generated by software conditions
    • SIGPIPE (13): a process that writes to a pipe that has no reader
    • SIGALRM (14): expiration of an alarm clock

more: https://dsa.cs.tsinghua.edu.cn/oj/static/unix_signal.html

Signal Disposition/Action

對每個 process 而言,可以告訴 kernel 要對不同信號做何種行為 :

  1. ignore 該信號
  2. 用 process 自身的 signal handler 來 catch 該信號
  3. 執行預設動作

Program Start-up

  • fork(): child 會繼承 parent 的 signal dispositions
  • exec(): process 會 reset 成 default handler

不同信號的預設 disposition

core

  • 當 program crashes 或 exits abnormally 時,便會產生 core dump file 來記錄 program states
  • 通常被用來協助 debug (ex: loaded to gdb)
  • core dump file 不會產生的情況 :
    • Set-UID/GID process: real UID/GID != program’s UID/GID
    • No write permission to the directory
    • 空間不夠
  • 在 Linux 裡面該檔案名稱被 /proc/sys/kernel/core_pattern 所指定

設定 disposition : signal()

// returns the old disposition of the signal signum if OK, SIG_ERR on error
typedef void (*sighandler_t) (int); // data type of signal handlers
sighandler_t signal(int signum, sighandler_t handler);

signal() 用來設定信號 signum 的 signal handler handler

  • handler : signal handler function 的指標,可以是 自定義函數 / SIG_IGN:ignore / SIG_DFL":default

SIGKILLSIGSTOP 只能執行預設動作,分別是 terminate process 和 stop process

Interrrupted System Calls

當 process 在做 system call 時,一個信號可能被傳遞給 process 並打斷 system call,有兩種結果 :

  • 如果建立 signal handler 時設定了 SA_RESTART,則當 singanl handler 返回後,system call 會自動重做
  • system call 返回錯誤errno = EINTR 可以使用以下程式來重做 system call :
    again:
    	if ((n = read(fd, buf, BUFFSIZE)) < 0) {
    		if (errno == EINTR)
    			goto again;
    	}

Reentrant Functions

由於 signal handler 無法知道在收到信號之前 process 運行到何處,因此有可能同一個 function 在主程式被呼叫後,又被 signal handler 呼叫一次

舉例來說,getpwnam 會將 struct passwd 存在一個 static memory 裡面,並返回該 struct 的 pointer,因此有可能在連續呼叫兩個 getpwnam 時被覆蓋

static void my_alarm(int signo) {
struct passwd *rootptr;

printf("in signal handler\n");
if ((rootptr = getpwnam("root")) == NULL)
	err_sys("getpwnam(root) error");
alarm(1);
}

int main(void) {
struct passwd *ptr;

signal(SIGALRM, my_alarm);
alarm(1);
for ( ; ; ) {
	if ((ptr = getpwnam("sar")) == NULL)
		err_sys("getpwnam error");
	if (strcmp(ptr->pw_name, "sar") != 0)
		printf("return value corrupted!, pw_name = %s\n", ptr->pw_name);
}
}

當一個 function 像這樣被遞迴呼叫時會產生問題,我們稱它是 non-reentrant function

怎樣會讓一個 function non-reentrant ?

  • 使用 static/global variables & data 來和其他 functions 互動
  • 使用 malloc() or free() : 因為它們運用 static global data structure
  • 在沒有備份的情況下更改 errno
  • 呼叫 non-reentrant functions

因此,如果一個 function 可以被遞迴呼叫而不會有問題,則稱為 reentrant function

怎樣讓一個 function reentrant ?

  • 不要用 static/global variables & data,不要返回 static memory 的 pointer
  • 在使用 signal handler 前先配置好相關的 memory (不要用 malloc())
  • 在更改前先儲存 errno
  • 不要呼叫任何 non-reentrant functions

Signal 的操作

  • a signal is pending : 以該 process 為目標的信號已被產生,但 process 還未處理該信號
  • a signal is delivered to the process : process 已經在處理該信號
  • process 可以選擇 blocking 信號的傳送 : 如果被 block 的信號的目標是該 process,並且該 process 對於該信號的 disposition 並非 ignore,則該信號會 pending 直到
    • process unblocks the signal
    • 對該信號的 disposition 變成 ignore (可以透過 sigpending() 來確認目前該信號的狀態)
  • 每個 process 都有一個 signal mask 來決定要被 blocking 的 signal set (可以透過 sigprocmask() 來獲取或更改 signal mask)

如果相同信號在被 unblock 之前產生了很多次

  • POSIX.1 允許系統可以傳遞多次信號 (信號會排隊)
  • Linux 不會讓相同信號進入 queue,如果許多相同的信號在 pending,只有一個可以被 delivered to the process

如果有很多信號都準備好被 delivered to the process

  • POSIX.1 沒有規定信號傳遞的順序
  • POSIX.1 建議跟 current state 有關的信號 (e.g. SIGSEGV) 應該被優先傳遞

signal sets

// All return: 0 if OK, -1 on error
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);

// Return 1 if true, 0 if false, -1 on error
int sigismember(const sigset_t *set, int signo);

更改 signal mask : sigprocmask()

// Returns 0 if OK, -1 on error
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
  • how :
    • SIG_BLOCK : set 包含要被 block 的信號 (加入現有 signal mask)
    • SIG_UNBLOCK : set 包含要被 unblock 的信號 (移出現有 signal mask)
    • SIG_SETMASK : set 就是新的 signal mask
  • set : 包含要更動的信號 (如果為 NULL 則 signal mask 不會被更動)
  • oset : 儲存之前的 signal mask (設成 NULL 來忽略)

在呼叫 sigpromask() 之後,如果有任何正在 pending 的信號被 unblock,則在 sigpromask() 回傳之前至少有一個信號會被 delivered 給 process

獲取 pending 信號 : sigpending()

// Returns 0 if OK, -1 on error
int sigpending(sigset_t *set);

sigpending() 會將目前正在 pending 的 signal 存入 set

傳遞信號 : kill()

// Returns 0 if OK, -1 on error
int kill(pid_t pid, int signo);

kill() 會傳遞信號 signopid

  • pid > 0 : 傳給 process pid
  • pid == 0 : 傳給所有和 sender 有相同 gid 的 process
  • pid < 0 : 傳給所有 gid == |pid| 的 process
  • pid == -1 : 傳給所有 process

可以透過將 signo 設成 0 (null signal) 來測試 process 是否存在

  • 不存在 : kill() return -1,errno = ESRCH
  • 存在 : kill() return 0,但實際上不會有任何信號被傳遞

這項測試並非 atomic : 當 kill() return 時,process 可能已經 exited

權限問題

  • superuser 可以傳信號給任何 process
  • sender’s real UID or effective UID == receiver’s real UID or effective UID
    • 如果系統支援 _POSIX_SAVED_IDS : receiver’s saved-set-UID is checked instead of effective UID

傳遞信號給自己 : raise()

// Returns 0 if OK, -1 on error
int raise(int signo);

相當於 kill(getpid(), signo)

設定 alarm : alarm()

unsigned int alarm(unsigned int seconds);

alarm() 會設定一個 alarm,並在 seconds 秒後過期並傳遞 SIGALRM 給 caller

  • 一個 process 同時間只能有一個 alarm,當 seconds 不為 0 時,舊的 alarm 會被覆蓋
  • 如果 seconds 為 0,則所有在 pending 的 alarm 被取消

alarm() 會回傳當前的 alarm 還剩多少秒,如果沒有 alarm 則回傳 0

default action for SIGALRM is terminate the process,記得在呼叫 alarm() 之前註冊 signal handler !

暫停 process : pause()

int pause(void);

pause() 會暫停 caller 直到==某個信號被 catch== (terminate process 或觸發 signal handler)
只有當 signal handler 被觸發並返回後,pause() 才會返回 -1,errno = EINTR

更改 disposition : sigaction()

// Returns 0 if OK, -1 on error
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);

我們無法更改 SIGKILLSIGSTOP 的 action

sigaction() 用來檢查或更改信號 signo 的 action

  • act : 新的 action (如果是 NULL 則不更改)
  • oact : 用來儲存之前的 action
struct sigaction {
	void       (*sa_handler)(int);
	sigset_t   sa_mask;
	int        sa_flags;
	void (*sa_sigaction)(int, siginfo_t *, void *);
}
  • sa_handler : 指向 signal handler function 的指標,或是 SIG_DFLSIG_IGN
  • sa_mask : 在 signal handler 的執行過程中要 block 的其他信號
    • 在呼叫 signal handler 之前 : 將 現在要被 delivered 的信號 + sa_mask 內的信號 加入 signal mask
    • signal handler 結束後 : 回復成原本的 signal mask
  • sa_flags : 用來調整信號的行為 (詳細見 manual)
  • sa_sigaction : 用來指定另外的 signal handler function,如果 sa_flags 包含 SA_SIGINFO,則使用 sa_sigaction 而非 sa_handler
    • siginfo : 包含了信號產生原因等資訊 (詳細見 manual)

Example of alarm() & pause()

#include <signal.h>
#include <unistd.h>
static void sig_alrm(int signo){
	/* nothing to do, just return to wake up the pause */
}

unsigned int sleep1(unsigned int seconds){
	if (signal(SIGALRM, sig_alrm) == SIG_ERR)
		return(seconds);
	alarm(seconds);    /* start the timer */
	pause();           /* next caught signal wakes us up */
	return(alarm(0));  /* turn off timer, return unslept time */
}

這段程式有三個問題 :

  1. 呼叫者之前的 alarm 會被 alarm() 給覆蓋掉
    solution: 查看 alarm() 的返回值
    ...
    if((oldalarm = alarm(0)) > 0){
    	if(oldalarm < seconds){
    		alarm(oldalarm);
    		pause();
    		return(alarm(0));
    	}
    	else{
    		alarm(seconds);
    		pause();
    		return(alarm(oldalarm - seconds));
    	}
    }
    ...
  2. 呼叫者之前對 SIGALRM 的 disposition 會被 signal() 給覆蓋
    solution: 儲存之前的 disposition 並在結束時復原
    ...
    if((oldhandler = signal(SIGALRM, sig_alrm)) == SIG_ERR){
    	return seconds;
    }
    ...
    signal(SIGALRM, oldhandler);
  3. alarm()pause() 之間可能發生 race condition : alarm()pause() 之前就過期了
    solution: use signal
    static jmp_buf  env;
    
    static void sig_alrm(int signo){
    	longjmp(env, 1);
    }
    
    unsigned int sleep1(unsigned int seconds){
    	if (signal(SIGALRM, sig_alrm) == SIG_ERR)
    		return(seconds);
    	if(setjmp(env) == 0){
    		alarm(seconds);    /* start the timer */
    		pause();           /* next caught signal wakes us up */
    	}
    	return(alarm(0));  /* turn off timer, return unslept time */
    }

fork() v.s. exec() — signal

fork()exec()
signal maskinheritremain
pending alarms, signalsclear and not inheritremain
signal dispositioninheritif the signal is not ignored, then reset to default ; if ignored, then left ignored

Nonlocal Jump

在 c 語言裡面,我們不能 goto 位在其他 function 的 label
nonlocal jumps 讓程式可以 goto 程式的任意位置

Stack Frame

Calling Convention :
用來規範呼叫函數時,傳遞的參數要存在哪,返回值要返回到哪等資訊
同時約束 caller (呼叫其他 function 的 function) 和 callee (被呼叫的 function,正在執行的都是 callee)

Stack Frame :
複習 memory layout
當一個 function 被呼叫時,stack 裡面會保留一段空間,也就是 stack frame,給該 function,用來存放以下資訊 :

  • return address (回傳給 caller)
  • arguments (從 caller 傳過來的參數,舉例來說 do_line(char *ptr)ptr)
  • saved registers
  • automatic variables (在程式流進入和離開變數的範圍時,會自動 allocate 和 deallocate)

當該 function 返回後,stack frame 就會被釋放

int main(void) {
  char line[MAXLINE];
  while (fgets(line, MAXLINE, stdin) != NULL)
    do_line(line);
  exit(0);
}

char *tok_ptr; /* global pointer for get_token() */

/* process one line of input */
void do_line(char *ptr) {
  int cmd;
  tok_ptr = ptr;
  while ((cmd = get_token()) > 0) {
    switch (cmd) { /* one case for each command */
    case TOK_ADD:
      cmd_add();
      break;
    }
  }
}

void cmd_add(void) {
  int token;
  token = get_token();
  /* rest of processing for this command */
}

int get_token(void) {
  /* fetch next token from line pointed to by tok_ptr */
}

執行 nonlocal jump - setjmp(), longjmp()

int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int val);

setjmp() 會建立待會要跳轉的目標

  • 將目前的 calling environment (stack, CPU registers) 記錄到 env 裡面
  • env 通常是 global variable,由 setjump() 建立再由 longjmp() 使用
  • 被直接呼叫時會返回 0 ; 經由 longjmp() 被呼叫則返回非 0

longjmp() 會執行跳轉的動作

  • 使用 env 將程式跳轉到先前 setjmp() 被呼叫的時候,並==將 environment 回復成呼叫== ==setjmp()== ==時的情況==
  • 如果跳轉成功,則程式會像是 setjmp() 返回了第二次後繼續執行
  • val 就會是 setjmp() 返回的值

注意

  • 呼叫一次 setjmp() 後,我們可以呼叫 longjmp() 多次,並用不同的 val 來辨別從何處返回
  • 不同的 setjmp() 最好使用不同的 jmp_buf,否則可能會被覆蓋掉

Problem of Nonlocal Jump

  • 變數是否會回復取決於變數的儲存位置
    • memory : 會維持在呼叫 longjmp() 時的狀態
      • static int, valotile int, global variable
    • CPU, registers : 會回復到首次呼叫 setjmp() 時的狀態
      • int, register int
  • 我們只能對已被呼叫但還沒結束的 function 做 longjump(),舉例 :
    jmp_buf  env;
    
    P1(){
    	P2(); P3();
    }
    
    P2(){
    	if(setjmp(env)){
    		/* long jump to here */
    	}
    }
    
    P3(){
    	longjmp(env, 1); /* can't jump to P2 cuz it's terminate */
    }
  • POSIX 沒有規範 setjmp() 是否要將 signal mask 存到 env 裡面
    • FressBSD 8.0 and Mac OS X 10.6.8 : setjmp()longjmp() 會儲存並回復 signal mask
    • Linux : 不會

儲存 sigmask 的 jump : sigsetjmp(), siglongjmp()

// Returns: 0 if called directly, nonzero if returning from a call to siglongjmp
int sigsetjmp(sigjmp_buf env, int savemask);
void siglongjmp(sigjmp_buf env, int val);

基本上和 setjmp()longjmp() 一樣
savemask ≠ 0 的時候,sigsetjmp() 會將 signal mask 也儲存到 env 裡面

Signal Blocking

這是一個錯誤的 signal blocking 範例

sigset_t newmask, oldmask;
sigemptyset(&newmask);
sigaddset(&newmask, SIGINT);

/* block SIGINT and save current signal mask */
sigprocmask(SIG_BLOCK, &newmask, &oldmask);

/*
 *  critical region of code
 *  will not be interrupted by SIGINT
 */

/* restore signal mask, which unblocks SIGINT */
sigprocmask(SIG_SETMASK, &oldmask, NULL);

/* window is open */

pause(); /* wait for signal to occur */

/* continue processing */

問題 :
如果信號在 window is open 區塊被 delivered
pause() 就不會接收到信號並卡死

解法 :
需要一個 atomic operation 來做
回復 signal maskpause() 兩個動作
確保所有信號都在 pause() 之後才被 delivered

暫時替換 sigmask 然後 pause : sigsuspend()

\#include <signal.h>
// Returns −1 with errno set to EINTR (If it returns to the caller)
int sigsuspend(const sigset_t *sigmask);

sigsuspend() 會先將目前的 signal mask 替換成 sigmask,接著暫停 process 直到兩種情況 :

  1. ==caught 到某個信號== → 在 signal handler 之後返回
  2. ==terminate process 的信號發生== → 不返回 當 sigsuspend() 返回時,它會將 signal mask 復原成呼叫 sigsuspend() 之前

正確的範例:

// supose signal() is implemented by sigaction()
sigset_t  newmask, oldmask, waitmask;

signal(SIGINT, sig_handler);
signal(SIGUSR1, sig_handler);

sigemptyset(&waitmask);
sigemptyset(&newmask);
sigaddset(&waitmask, SIGUSR1);
sigaddset(&newmask, SIGINT);

sigprocmask(SIG_BLOCK, &newmask, &oldmask);

/*
 *  critical region of code
 *  will not be interrupted by SIGINT
 */

sigsuspend(&waitmask);
/* handl SIGINT here */

sigprocmask(SIG_SETMASK, &oldmask, NULL);

正確範例的 signal blocking 順序

Example of Parent-Child Sync

之前我們用 IPC 來處理 現在我們也可以用 signal 來達成 synchronization

static volatile sig_atomic_t sigflag;
static sigset_t newmask, oldmask, zeromask;

static void sig_usr(int signo){
	sigflag = 1; /* one signal handler for SIGUSR1 and SIGUSR2 */
}

void TELL_WAIT(void){
	if (signal(SIGUSR1, sig_usr) == SIG_ERR)
		err_sys("signal(SIGUSR1) error");
	if (signal(SIGUSR2, sig_usr) == SIG_ERR)
		err_sys("signal(SIGUSR2) error");

	sigemptyset(&zeromask);
	sigemptyset(&newmask);
	sigaddset(&newmask, SIGUSR1);
	sigaddset(&newmask, SIGUSR2);

	/* Block SIGUSR1 and SIGUSR2, and save the current signal mask */
	if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)
		err_sys("SIG_BLOCK error");
}
void TELL_PARENT(pid_t pid){
	/* tell parent we’re done */
	kill(pid, SIGUSR2);
}

void WAIT_PARENT(void){
	while (sigflag == 0)
		sigsuspend(&zeromask);
	sigflag = 0;

	/* Reset signal mask to original value */
	if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
		err_sys("SIG_SETMASK error");
}
void TELL_CHILD(pid_t pid){
	/* tell child we’re done */
	kill(pid, SIGUSR1);
}

void WAIT_CHILD(void){
	while (sigflag == 0)
		sigsuspend(&zeromask);
	sigflag = 0;

	/* Reset signal mask to original value */
	if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
		err_sys("SIG_SETMASK error");
}

terminate process abnormally : abort()

// The function never returns
void abort(void);

abort() 會讓 process terminate abnormally

  • unblock SIGABRT 然後對 caller process 發送 SIGABRT

  • SIGABRT 的 default action 是 terminate the process

    • 如果 SIGABRT 被 ignored 或是被其他 handler catch,abort() 會復原 SIGABRT 的 default action 然後再次發送 SIGABRT
  • POSIX.1 裡面的 abort()

    void abort(void){
    	sigset_t mask;
    	struct sigaction action;
    
    	/* Caller can’t ignore SIGABRT; if so reset to default */
    	sigaction(SIGABRT, NULL, &action);
    	if (action.sa_handler == SIG_IGN) {
    		action.sa_handler = SIG_DFL;
    		sigaction(SIGABRT, &action, NULL);
    	}
    
    	/* For default action, POSIX requires flushing all open stdio streams */
    	if (action.sa_handler == SIG_DFL)
    	fflush(NULL);
    
    	/* Caller can’t block SIGABRT; make sure it’s unblocked */
    	sigfillset(&mask);
    	sigdelset(&mask, SIGABRT);               /* mask has only SIGABRT turned off */
    	sigprocmask(SIG_SETMASK, &mask, NULL);
    	kill(getpid(), SIGABRT);                 /* send the signal */
    
    	/* If we’re here, caller process has caught SIGABRT and returned */
    	fflush(NULL);                            /* flush all open stdio streams */
    	action.sa_handler = SIG_DFL;
    	sigaction(SIGABRT, &action, NULL);       /* reset to default */
    	sigprocmask(SIG_SETMASK, &mask, NULL);   /* just in case ... */
    	kill(getpid(), SIGABRT);                 /* and one more time */
    	exit(1);                                 /* this should never be executed ... */
    }

暫停 process 一段時間 : sleep()

// Returns: 0 or the number of unslept seconds
unsigned int sleep(unsigned int seconds);

sleep() 會暫停 caller process 直到 :

  • 實際時間已經過了 seconds 秒 → 返回 0
  • process catch 到某個信號 → 返回 seconds left

用 pause() 來實作 sleep()

unsigned int sleep(unsigned int seconds){
  struct sigaction newact, oldact;
  sigset_t newmask, oldmask, suspmask;
  unsigned int unslept;

  /* set our handler, save previous information */
  newact.sa_handler = sig_alrm;
  sigemptyset(&newact.sa_mask);
  newact.sa_flags = 0;
  sigaction(SIGALRM, &newact, &oldact);

  /* block SIGALRM and save current signal mask */
  sigemptyset(&newmask);
  sigaddset(&newmask, SIGALRM);
  sigprocmask(SIG_BLOCK, &newmask, &oldmask);
  alarm(seconds);
  suspmask = oldmask;

  /* make sure SIGALRM isn’t blocked */
  sigdelset(&suspmask, SIGALRM);

  /* wait for any signal to be caught */
  sigsuspend(&suspmask);

  /* some signal has been caught, SIGALRM is now blocked */
  unslept = alarm(0);
  sigaction(SIGALRM, &oldact, NULL); /* reset previous action */
  sigprocmask(SIG_SETMASK, &oldmask, NULL);
  return(unslept);
}