上次寫了一個水文簡單聊聊進程中的信號處理 ,師父看了後把我怒斥了一頓,表示上篇水文中的例子太 old style, too simple ,too naive。如果未來出了偏差,我也要負責任的。嚇得我連和妹子周年慶的文章都沒寫,先趕緊來重新水一篇文章,聊聊更優秀,更方便的信號處理方式
前情提要#
首先來看看,之前那篇文章中的例子
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
void deletejob(pid_t pid) { printf("delete task %d\n", pid); }
void addjob(pid_t pid) { printf("add task %d\n", pid); }
void handler(int sig) {
int olderrno = errno;
sigset_t mask_all, prev_all;
pid_t pid;
sigfillset(&mask_all);
while ((pid = waitpid(-1, NULL, 0)) > 0) {
sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
deletejob(pid);
sigprocmask(SIG_SETMASK, &prev_all, NULL);
}
if (errno != ECHILD) {
printf("waitpid error");
}
errno = olderrno;
}
int main(int argc, char **argv) {
int pid;
sigset_t mask_all, prev_all;
sigfillset(&mask_all);
signal(SIGCHLD, handler);
while (1) {
if ((pid = fork()) == 0) {
execve("/bin/date", argv, NULL);
}
sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
addjob(pid);
sigprocmask(SIG_SETMASK, &prev_all, NULL);
}
}
再來複習下幾個關鍵的 syscall
- signal1: 信號處理函數,使用者可以通過這個函數為當前進程指定具體信號的 Handler。當信號觸發時,系統會調用具體的 Handler 進行對應的邏輯處理。
- sigfillset2: 用於操作 signal sets(信號集)的函數之一,這裡的含義是將系統所有支持的信號量添加進一個信號集中
- fork3: 大家比較熟悉的一個 API 了,創建一個新的進程,並返回 pid 。如果是在父進程中,返回的 pid 是對應子進程的 pid。如果子進程中,pid 為 0
- execve4: 執行一個特定的可執行文件
- sigprocmask5:設置進程的信號屏蔽集。當傳入第一個參數為 SIG_BLOCK 時,函數會將當前進程的信號屏蔽集保存在第三個參數傳入的信號集變量中,並將當前進程的信號屏蔽集設置為第二個參數傳入的信號屏蔽集。當第一個參數為 SIG_SETMASK 時,函數會將當前進程的信號屏蔽集設置為第二個參數設置的值。
- wait_pid6: 做一個不精確的概括,回收並釋放已終止的子進程的資源。
好了,複習完關鍵點之後,開始進入本文的關鍵部分。
更優雅的信號處理手段#
更優雅的 handler#
首先再來看看上面信號處理部分的代碼
void handler(int sig) {
int olderrno = errno;
sigset_t mask_all, prev_all;
pid_t pid;
sigfillset(&mask_all);
while ((pid = waitpid(-1, NULL, 0)) > 0) {
sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
deletejob(pid);
sigprocmask(SIG_SETMASK, &prev_all, NULL);
}
if (errno != ECHILD) {
printf("waitpid error");
}
errno = olderrno;
}
這裡我們為了保證 handler
不被其餘的信號打斷,所以我們在處理的時候使用 sigprocmask
+ SIG_BLOCK
來做信號屏蔽。這樣看起來邏輯上沒啥問題,但是有個問題。當我們有其餘很多不同 handler
的時候,我們勢必會生成很多重複冗餘的代碼。那么我們有沒有更優雅的方法來保證我們的 handler
的安全呢?
有(超大聲(好,很有精神!(逃。隆重介紹一個新的 syscall -> sigaction7
廢話不多說,先上代碼
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
void deletejob(pid_t pid) { printf("delete task %d\n", pid); }
void addjob(pid_t pid) { printf("add task %d\n", pid); }
void handler(int sig) {
int olderrno = errno;
sigset_t mask_all, prev_all;
pid_t pid;
sigfillset(&mask_all);
while ((pid = waitpid(-1, NULL, 0)) > 0) {
deletejob(pid);
}
if (errno != ECHILD) {
printf("waitpid error");
}
errno = olderrno;
}
int main(int argc, char **argv) {
int pid;
sigset_t mask_all, prev_all;
sigfillset(&mask_all);
struct sigaction new_action;
new_action.sa_handler=handler;
new_action.sa_mask=mask_all;
signal(SIGCHLD, handler);
while (1) {
if ((pid = fork()) == 0) {
execve("/bin/date", argv, NULL);
}
sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
addjob(pid);
sigprocmask(SIG_SETMASK, &prev_all, NULL);
}
}
好!很有精神!大家可能發現了,我們這段代碼相較於之前的代碼增加了關於 sigaction 相關的設置。難道?
yep,在 sigaction 中,我們可以通過設置 sa_mask
來設置當信號處理函數執行期間,進程將阻塞哪些信號。
你看,這樣我們的代碼是不是相較於之前更為優雅了。當然,sigaction 還有很多其餘很有用的設置項,大家可以下來了解一下。
更快速的信號處理方式#
在我們上面的例子中,我們已經解決了優雅的設置信號處理函數這樣的問題,那麼我們現在又面臨了一個全新的問題。
如上面所說,我們信號處理函數在執行時,我們選擇阻塞了其餘的信號。那麼這裡存在一個問題,當我們在信號處理函數中的邏輯耗時較長,且不需要原子性(即需要和信號處理函數保持同步),而且系統中的信號發生頻率較高。那麼我們這樣的做法將會導致進程的信號隊列不斷增加,進而導致不可預料的後果。
那麼我們這裡有什麼更好的方法來處理這件事呢?
假設,我們打開一個文件,在信號處理函數中只完成一件事,就是往這個文件中寫一個特定的值。然後我們輪詢這個文件,如果一旦發生變化,那麼我們讀取文件中的值,判斷具體的信號,做具體的信號處理,這樣是不是既保證了信號的妥投,又保證我們信號處理邏輯將阻塞信號的代價降至最低了?
當然,當然,社區知道大家嫌寫代碼難,所以專門給大家提供了一個船新的 syscall
-> signalfd8
老規矩,先來看看例子
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/signalfd.h>
#include <sys/wait.h>
#define MAXEVENTS 64
void deletejob(pid_t pid) { printf("delete task %d\n", pid); }
void addjob(pid_t pid) { printf("add task %d\n", pid); }
int main(int argc, char **argv) {
int pid;
struct epoll_event event;
struct epoll_event *events;
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGCHLD);
if (sigprocmask(SIG_SETMASK, &mask, NULL) < 0) {
perror("sigprocmask");
return 1;
}
int sfd = signalfd(-1, &mask, 0);
int epoll_fd = epoll_create(MAXEVENTS);
event.events = EPOLLIN | EPOLLEXCLUSIVE | EPOLLET;
event.data.fd = sfd;
int s = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sfd, &event);
if (s == -1) {
abort();
}
events = calloc(MAXEVENTS, sizeof(event));
while (1) {
int n = epoll_wait(epoll_fd, events, MAXEVENTS, 1);
if (n == -1) {
if (errno == EINTR) {
fprintf(stderr, "epoll EINTR error\n");
} else if (errno == EINVAL) {
fprintf(stderr, "epoll EINVAL error\n");
} else if (errno == EFAULT) {
fprintf(stderr, "epoll EFAULT error\n");
exit(-1);
} else if (errno == EBADF) {
fprintf(stderr, "epoll EBADF error\n");
exit(-1);
}
}
printf("%d\n", n);
for (int i = 0; i < n; i++) {
if ((events[i].events & EPOLLERR) || (events[i].events & EPOLLHUP) ||
(!(events[i].events & EPOLLIN))) {
printf("%d\n", i);
fprintf(stderr, "epoll err\n");
close(events[i].data.fd);
continue;
} else if (sfd == events[i].data.fd) {
struct signalfd_siginfo si;
ssize_t res = read(sfd, &si, sizeof(si));
if (res < 0) {
fprintf(stderr, "read error\n");
continue;
}
if (res != sizeof(si)) {
fprintf(stderr, "Something wrong\n");
continue;
}
if (si.ssi_signo == SIGCHLD) {
printf("Got SIGCHLD\n");
int child_pid = waitpid(-1, NULL, 0);
deletejob(child_pid);
}
}
}
if ((pid = fork()) == 0) {
execve("/bin/date", argv, NULL);
}
addjob(pid);
}
}
好了,我們來介紹下這段代碼中的一些關鍵點
- signalfd 是一類特殊的文件描述符,這個文件可讀,可 select 。當我們指定的信號發生時,我們可以從返回的 fd 中讀取到具體的信號值。
- signalfd 優先級比信號處理函數低。換句話說,假設我們為信號 SIGCHLD 註冊了信號處理函數,同時也為其註冊了 signalfd 那麼當信號發生時,將優先調用信號處理函數。所以我們在使用 signalfd 時,需要利用 sigprocmask 設置進程的信號屏蔽集。
- 如前面所說,該文件描述符可 select ,換句話說,我們可以利用 select9, poll10, epoll1112 等函數來對 fd 進行監聽。在上面的的代碼中,我們就利用 epoll 對 signalfd 進行監聽
當然,這裡額外要注意的一點是,很多語言不一定提供了官方的 signalfd 的 API(如 Python),但是也有可能提供了等價的替代品,典型的例子就是 Python 中的 signal.set_wakeup_fd13
在這裡也給大家留一個思考題:除了利用 signalfd ,還有什麼方法可以實現高效,安全的信號處理?
總結#
私以為信號處理是作為一個研發的基本功,我們需要安全,可靠的處理在程序環境中遇到的各種信號。而系統也提供了很多設計很優秀的 API 來減輕研發的負擔。但是我們要知道,信號本質上是通訊手段的一種。而其天生的弊端便是攜帶的信息較少。很多時候,當我們有很多高頻的信息傳遞需要去做的時候,這個時候可能利用信號並不是一個很好的選擇。當然這個並沒有定論。只能 case by case 的去做 trade-off。
差不多就這樣吧,本周第二篇水文混完(逃
Reference#
- [3]. Linux man page: fork
- [10]. Linux man page: poll