在Linux系统中,使用fork创建子进程,是简单方便地进行多进程开发的方法。
fork的原型如下:
#include
pid_t fork(void);
fork被调用以后,会有三种返回值。
0,表示子进程创建成功,当前进程在子进程中。返回值为子进程的pid。
如:
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;
}
在Linux中,可以给一个进程发送信号。
进程收到信号以后,则会执行程序注册的信号处理函数。
可以使用kill命令加-s [SIGNAL NAME] [pid]
给一个进程发送信号。如果没有-s
参数,则会发送默认地SIGINT信号。
默认地,程序收到SIGINT信号之后,会退出执行。但是我们可以实现自己的信号处理函数,改变这个默认行为。
注册信号处理函数的函数为signal,它的原型为:
#include
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
可以看到,signal的使用只需要一个信号值和一个回调函数的参数。非常直观,就是什么信号要执行什么函数。
信号值为SIG开头的一系列值,可以通过kill -L
命令列表查看。
回调函数为sighandler_t
,即void sighandler(int)
的形式。
比如,我们可以简单地注册一个处理SIGINT的函数:
#include
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;
}
信号处理函数可能在很多极端条件下被调用,如:
所以信号处理过程中不能调用非异步信号安全(async-signal-safe)的函数。
不能调用的常见函数包括:
更好的实践是,在信号处理函数中只设置值,之后快速返回。
在较新的代码中,已经不推荐使用signal,而是使用sigaction了。
因为signal是老接口,功能相对简单。而sigaction 是POSIX标准,提供更完整的信号处理控制。
如:
sigaction的原型为:
#include
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
只影响当前线程。
pthread还实现了fork的回调函数注册机制pthread_atfork
,可以在创建子进程的之前、之后,执行自定义的函数。
pthread_atfork
的原型为:
#include
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);
}