当前位置: 首页 > news >正文

Linux的多进程开发与信号处理

fork创建子进程

在Linux系统中,使用fork创建子进程,是简单方便地进行多进程开发的方法。

fork的原型如下:

#include <unistd.h>  pid_t fork(void);

fork被调用以后,会有三种返回值。

  1. 0,表示子进程创建成功,当前进程在子进程中。返回值为子进程的pid。

  2. == 0,表示子进程创建成功,当前进程在父进程中。返回值为0。
  3. < 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);
}

相关文章:

  • 【金仓数据库征文】-《深入探索金仓数据库:从基础到实战》
  • 【Qt】文件
  • 2025上海车展:赛轮思AI携手行业领军企业展示xUI——混合式、智能体化的AI助理平台
  • 漏洞管理体系:从扫描评估到修复验证的全生命周期实践
  • RocketMQ 主题与队列的协同作用解析(既然队列存储在不同的集群中,那要主题有什么用呢?)---管理命令、配置安装
  • Spring知识点总结
  • Vue3文件上传组件实战:打造高效的Element Plus上传解决方案,可以对文件进行删除,查看,下载功能。
  • 【HTTP/2:信息高速公路的革命】
  • C++中的vector和list的区别与适用场景
  • 西门子触摸屏文本显示不全,传送字体文件到屏幕的具体操作方法
  • C++ 日志系统实战第三步:熟悉掌握各种设计模式
  • 信令与流程分析
  • 界面控件DevExpress WinForms v25.1 - 数据处理功能持续增强
  • freecad参数化三维模型装配体解析至web端,切换参数组或修改参数
  • Parasoft C++Test软件单元测试_对函数打桩的详细介绍
  • Java对接企业微信实战笔记
  • SQL注入高级绕过手法汇总 重点
  • 在java程序中,类,进程,线程他们之间的关系以及main方法与他们之间的关系
  • 找出字符串中第一个匹配项的下标
  • Xmind快捷键大全
  • 乌称泽连斯基与特朗普进行简短会谈
  • 甘肃省原副省长赵金云被开除公职,甘肃省委表态:坚决拥护党中央决定
  • 金正恩出席朝鲜人民军海军驱逐舰入水仪式
  • 永辉超市一季度净利降近八成,未来12个月至18个月是改革成果集中释放期
  • 国务院同意在海南全岛和秦皇岛等15个城市(地区)设立跨境电子商务综合试验区
  • 王旭任甘肃省副省长