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

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);
}
http://www.xdnf.cn/news/1859.html

相关文章:

  • Windows 10 上运行 Ollama 时遇到 llama runner process has terminated: exit status 2
  • 金仓数据库 KingbaseES 产品深度优化提案:迈向卓越的全面升级
  • GIS开发笔记(15)基于osg和osgearth实现三维地图上添加路网数据(矢量shp)
  • Node.js神器rimraf:10倍速删除node_modules的跨平台解决方案
  • 资源获取:项目成功的关键要素
  • Android WindowManagerService(WMS)框架深度解析
  • Python命名参数的使用
  • 从『玩意儿』代码综观『解决问题』的方案设计逻辑——开放/细致/『明理』(DeepSeek)
  • 基于javaweb的SSM+Maven红酒朔源管理系统设计与实现(源码+文档+部署讲解)
  • 3000年不识伪全等直线段使数学一直有将两异直线段误为同一线段的重大错误——百年病态集论的症结
  • DeepSeek回答过于笼统,提示词如何优化
  • 【金仓数据库征文】-数据库界新兴前列者,本篇带你速懂金仓数据库!
  • 深度学习之卷积神经网络入门
  • 使用idea打包maven项目的时候因为java文件多导致java.lang.OutOfMemoryError: Java heap space
  • 【金仓数据库征文】——选择金仓,选择胜利
  • 【论文推荐】人工智能在滑坡风险评估三大核心领域的应用:人工智能技术在滑坡风险评估中的方法学综述
  • 前端基础之《Vue(10)—过滤器》
  • Linux命令行基础入门详解
  • Python3(8) 字符串
  • fastjson使用parseObject转换成JSONObject出现将字符特殊字符解析解决
  • attention-transformer-test
  • Agent智能体应用详解:从理论到实践的技术探索
  • AD16批量修改PCB元件序号的高度和宽度
  • Python 学习路线与笔记跳转(持续更新笔记链接)
  • 接口测试和单元测试详解
  • 浔川代码编辑器v2.0(测试版)更新公告
  • 手搓实时操作系统实践:从零构建属于自己的系统世界
  • maven构建时报错:was cached in the local repository...
  • Spring Boot知识点详解
  • 简单场景下的目标关联算法:GNN全局最近邻与匈牙利算法