Linux的多进程开发与信号处理
fork创建子进程
在Linux系统中,使用fork创建子进程,是简单方便地进行多进程开发的方法。
fork的原型如下:
#include <unistd.h> pid_t fork(void);
fork被调用以后,会有三种返回值。
-
0,表示子进程创建成功,当前进程在子进程中。返回值为子进程的pid。
- == 0,表示子进程创建成功,当前进程在父进程中。返回值为0。
- < 0,表示子进程创建失败。
如:
int
main(int argc, char *argv[])
{pid_t ret;if ((ret = fork ()) == 0) { printf ("now in parent process !\n"); } else if (ret > 0) { printf ("now in child process, pid is: %d !\n", ret);} else { printf("fork() error: %s\n", strerror (errno)); return -1;}return 0;
}
signal注册信号处理函数
在Linux中,可以给一个进程发送信号。
进程收到信号以后,则会执行程序注册的信号处理函数。
可以使用kill命令加-s [SIGNAL NAME] [pid]
给一个进程发送信号。如果没有-s
参数,则会发送默认地SIGINT信号。
默认地,程序收到SIGINT信号之后,会退出执行。但是我们可以实现自己的信号处理函数,改变这个默认行为。
注册信号处理函数的函数为signal,它的原型为:
#include <signal.h> typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);
可以看到,signal的使用只需要一个信号值和一个回调函数的参数。非常直观,就是什么信号要执行什么函数。
信号值为SIG开头的一系列值,可以通过kill -L
命令列表查看。
回调函数为sighandler_t
,即void sighandler(int)
的形式。
比如,我们可以简单地注册一个处理SIGINT的函数:
#include <signal.h>static void
int_han(int sig)
{// 注意这里仅为示例所用,实际开发中,应该避免在信号处理函数中执行这类函数。下面详细说明。printf("received sigint signal !\n");
}int
main (int argc, char *argv[])
{signal (SIGINT, int_han);sleep (60);return 0;
}
信号处理函数注意事项
信号处理函数可能在很多极端条件下被调用,如:
- 在另一个信号处理函数执行过程中
- 在 malloc/free 等内存分配函数执行过程中
- 在标准 I/O 函数执行过程中在信号处理函数中。
所以信号处理过程中不能调用非异步信号安全(async-signal-safe)的函数。
不能调用的常见函数包括:
- 标准IO,如printf、scanf等
- 内存分配释放,如malloc、free等
- system、exit、abort等
更好的实践是,在信号处理函数中只设置值,之后快速返回。
sigaction的使用
在较新的代码中,已经不推荐使用signal,而是使用sigaction了。
因为signal是老接口,功能相对简单。而sigaction 是POSIX标准,提供更完整的信号处理控制。
如:
- signal 在信号处理函数执行时,会临时将信号处理方式重置为默认行为。而sigaction可以指定 SA_RESTART标志,使被信号中断的系统调用自动重启。
- signal不能设置信号屏蔽字,而sigaction可以通过sa_mask 设置信号屏蔽字,防止信号处理函数被其他信号中断。
- 在回调函数的执行中,signal只能得到信号的编号,而sigaction可以通过siginfo_t获取更多信号相关的信息,甚至可以通过最后一个context参数,获取到程序运行相关的信息,比如程序堆栈等。
sigaction的原型为:
#include <signal.h>struct sigaction { void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; int sa_flags; void (*sa_restorer)(void);
};int sigaction(int signum, const struct sigaction *_Nullable restrict act, struct sigaction *_Nullable restrict oldact);
以上的signal实现,替换为sigaction则为:
static void
int_han(int signo, siginfo_t *info, void *context)
{printf("received sigint signal !\n");
}int
main (int argc, char *argv[])
{struct sigaction sa;sa.sa_sigaction = sig_han;sa.sa_flags = SA_SIGINFO | SA_RESTART;sigemptyset(&sa.sa_mask);sigaction(SIGINT, &sa, NULL);sleep(60);return 0;
}
随父进程退出
当fork执行成功以后,子进程会清零PR_SET_PDEATHSIG
,以至于父进程退出以后,子进程也不关心。
但是,如果我们希望父进程退出的时候,子进程也一并退出,可以使用prctl函数,注册一个父进程中止时的信号。
prctl (PR_SET_PDEATHSIG, SIGTERM);// 或者
prctl (PR_SET_PDEATHSIG, SIGKILL);
监控子进程退出
当子进程退出的时候,会向父进程发送SIGCHLD信号。
父进程可以通过这个信号,使用wait取得子进程的退出状态。
如:
void
sig_child (int sig)
{int status; pid_t pid; // 循环读取到至今所有的子进程退出事件while ((pid = waitpid (0, &status, WNOHANG)) > 0) {if (WIFEXITED (status)) printf ("child process: %d exit with %d\n", pid, WEXITSTATUS (status));else if (WIFSIGNALED (status)) ldebug ("child process: %d killed by the %dth signal\n", pid, WTERMSIG (status));}
}int
main (int argc, char *argv[])
{signal (SIGCHLD, sig_child);
}
信号的屏蔽
屏蔽信号,可以使用sigprocmask
。
sigprocmask的原型为:
int sigprocmask(int how, const sigset_t *_Nullable restrict set, sigset_t *_Nullable restrict oldset);
其中,how的可选值为SIG_BLOCK
或者SIG_UNBLOCK
,set与oldset都是sigset_t
结构体。
操作sigset_t
的函数有:
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set); int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum); int sigismember(const sigset_t *set, int signum);
分别表示置空、所有、添加、删除以及是否包含。
如以下调用将屏蔽所有信号:
sigset_t mask;sigfillset(&mask);
sigprocmask(SIG_BLOCK, &mask, NULL);
还可以使用SIG_UNBLOCK随时恢复:
sigset_t mask;
sigfillset(&mask);
sigprocmask(SIG_UNBLOCK, &mask, NULL);
但是,更建议使用pthread提供的pthread_sigmask
函数,因为sigprocmask在多线程条件下行为未定义,但是pthread_sigmask
是线程安全的。
另外,pthread_sigmask
只影响当前线程。
fork的回调
pthread还实现了fork的回调函数注册机制pthread_atfork
,可以在创建子进程的之前、之后,执行自定义的函数。
pthread_atfork
的原型为:
#include <pthread.h> int pthread_atfork(void (*prepare)(void), void (*parent)(void),
void (*child)(void));
其中,prepare是fork之前执行的回调函数,parent是fork之后父进程执行的函数,child是fork之后子进程执行的函数。
比如以下代码,实现了在fork之前屏蔽掉信号处理,fork之后再恢复,避免在fork过程中因为信号处理而出现异常。
// fork之前,屏蔽掉SIGHUP信号
void
pre_fork()
{sigset_t mask;sigemptyset(&mask);sigaddset(&mask, SIGHUP);pthread_sigmask(SIG_BLOCK, &mask, NULL);
}// fork之后,父进程打开SIGHUP信号
void
post_fork_parent()
{sigset_t mask;sigemptyset(&mask);sigaddset(&mask, SIGHUP);pthread_sigmask(SIG_UNBLOCK, &mask, NULL);
}void post_fork_child()
{// 子进程不使用SIGHUP信号
}// 在程序初始化时注册fork处理函数
int
main(int argc, char *argv[])
{pthread_atfork(pre_fork, post_fork_parent, post_fork_child);
}