Linux学习|Gentoo/Arch/FreeBSDLinuxLinux学习之路

Linux中的信号机制

2020-03-21  本文已影响0人  Leon_Geo

信号就是一条消息,通知进程系统中发生了什么事,每种信号都对应着某种系统事件。一般的底层硬件异常是由内核的异常处理程序处理的,它对用户进程来说是透明的。而信号机制,提供了一种方法通知用户进程发生了这些异常。

例如,一个进程试图除0,会引发内核向他发送SIGFPE信号;执行非法指令会引发SIGILL信号;非法内存访问引发SIGSEGV;当你从键盘上键入Ctrl + C会引发SIGINT;当某个子进程结束会引发内核向其父进程发送SIGCHLD信号,等等。具体请看下图:

1. 信号术语与原则

1.1信号发送

当内核检测到某种系统事件(除零错误或子进程终止等等)或一个进程调用了kill函数显式的要求内核发送一个信号给目的进程时,内核会通过更新目的进程上下文中的某个状态而达到向它发送一个信号的目的。发送信号的方式为:

#include <unistd.h>  
unsigned int alarm(unsigned int secs);
  //返回:待处理的闹钟在被发送前还剩余的秒数,若之前没有待处理的闹钟,则返回0
  //若secs = 0,不会调度安排新的闹钟。
#include <sys/types.h>
#include <signal.h>
    ​
int kill(pid_t pid, int sig);
    //成功返回0,失败返回-1。
  • pid > 0 :发送信号sig给进程pid;
  • pid = 0 :发送信号给自己所在进程组中的每个进程,包括自己。
  • pid < 0 :发送信号sig给进程-pid。

1.2 信号处理

当进程从系统调用返回或是完成了一次上下文切换而重新取得控制权之前,内核会检查该进程的待处理信号集(pengding&(~blocked)),如果为空则完成控制权的交接,如果不为空则会让进程响应该信号集合中信号值最小的那个信号。

目的进程收到信号后有“忽略信号”、“终止进程”和“捕获信号“这3种方式来响应。其中

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

如果handler = SIG_IGN,那么就忽略类型为signum的信号;
如果handler = SIG_DFL,那么就恢复类型为signum的信号的默认行为;
否则,handler就是用户自定义的信号处理函数地址。

#include <signal.h>
    ​
int sigprocmask(int HOW, const sigset_t *set, sigset_t *oldset);

int sigemptyset(sigset_t *set);  //初始化set集为空(set = 0);
int sigfillset(sigset_t *set);  //将所有信号都添加进set集(set = 1);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);                              
                                                        //以上5个函数成功返回0,错误返回-1
int sigismember(const sigset_t *set, int signum);
                                           //是成员返回1,不是返回0,错误返回-1</pre>

关于sigprocmask函数中的"HOW"有以下几种可能的取值:

  • SIG_BLOCK:把set集中的信号加到进程的blocked中(blocked |= set);
  • SIG_UNBLOCK:从进程的blocked中删除set集中的信号(blocked &= ~set);
  • SIG_SETMASK:忽略set集中的信号(blocked = set);
  • oldset : 如果他是非空的,则将进程原先blocked的值保存在其中。

以下示例展示了临时忽略SIGINT信号的程序片段:

sigset_t mask, oldmask;
    ​
    sigemptyset(&mask);
    sigaddset(&mask, SIGINT);
    ​
    sigprocmask(SIG_BLOCK, &mask, &oldmask);
    .
    .  //此处的所有语句将不会响应SIGINT信号
    .
    sigprocmask(SIG_SETMASK, &oldmask, NULL);
     //之后的语句将会正常响应SIGINT信号

2. 安全的信号处理函数

由于信号处理函数和主程序是并发运行的,他们享有相同的全局变量,他们的运行顺序是不可预测的,这就导致何时接收到信号的规则往往有违人们的直觉,或者说主程序和子程序间不一定会按照你预想的顺序去执行。所以为了防止竞争冒险,在编写信号处理函数时有几个保守的原则需要遵守:

为了在信号处理程序中能够打印一些简单的消息,我们可以使用一些异步信号安全的系统函数来构建自己的特有包装函数。作为例子,下面的程序展示了利用异步信号安全的系统函数write编写自己的SIO(safe I/O)函数。

ssize_t sio_puts(char s[])
 {
  int count = 0;
  char *str = s;
  if(!str)
  _exit(1);
  while(*str++)
  count++; 
  return write(STDOUT, s, count);
 }
handler_t *Signal(int signum, handler_t *handler)
    {
     struct sigaction action,oldaction;

     action.sa_handler = handler;
     sigemptyset(&action.sa_mask);
     action.sa_flags = SA_RESTART;

     if(sigaction(signum, &action, &oldaction) < 0)
     unix_error("Signal error");
     return(oldaction.sa_handler)
    }

3.信号的同步

当需要编写读写相同内存位置的并发进程,我们不得不考虑进程间的(既包括进程与进程之间,也包括主进程与子进程之间)竞争关系。这是一个很大的命题,在此限于文章主题,只讨论信号之间的竞争关系如何处理。主要分两个方面,一是隐式竞争,二是显式竞争。

3.1 避免隐式竞争

考虑一个类似shell的函数功能,父进程在一个全局作业列表中记录着它的当前子进程,每个作业一个条目。addjob和deletejob函数分别向这个作业列表中添加和删除作业。父进程每创建一个子进程就把它添加在作业列表中,每当在SIGCHLD信号处理程序中回收一个僵死的子进程时,就在job列表中删除这个子进程。

void handler(int sig)
{
 int olderrno = errno;  //保存进程的原error值
 sigset_t mask_all,prev_all;
 pid_t pid;

 sigfillset(&mask_all);  //将所有信号添加到信号集mask_all中
 while((pid = waitpid(-1, NULL, 0)) > 0){  //回收僵死子进程
 sigprocmask(SIG_BLOCK, &mask_all, prev_all);  //阻塞(屏蔽)所有信号
 deletejob(pid);      //从job列表中删除僵死的子进程条目
 sigprocmask(SIG_SETMASK, &prev_all, NULL);
 }
 if(errno != ECHILD)      //如果父进程的所有子进程都已经回收,则内核发送ECHILD错误
 Unix_error("waitpid error");
 errno = olderrno;      //恢复进程的原error值
}
​
​
int main(int argc, char **argv)
{
 int pid;
 sigset_t mask_all,mask_one,prev_one;

 sigfillset(mask_all);
 sigemptyset(mask_one);
 sigaddset(&mask_one, SIGCHLD); 
 Signal(SIGCHLD, handler);  //使用安全的Signal函数设置处理函数
 initjobs();                  //初始化工作列表

 while(1){
 /*在产生子进程前屏蔽SIGCHLD,以防止主进程还没执行到addjob就已经收到了因子进程终止
 而发来的SIGCHLD信号,进而进入handler导致在jobs中找不到要删除的子进程条目*/
 sigprocmask(SIG_BLOCK, &mask_one, &prev_one);  //频闭SIGCHLD信号
 if((pid = fork()) == 0){
 sigprocmask(SIG_SETMASK, &prev_one, NULL);  //子进程解除频闭SIGCHLD
 execve("/bin/date", argv, NULL);
 }
 sigprocmask(SIG_BLOCK, &mask_all, NULL);  //父进程屏蔽所有信号
 addjob(pid); 
 sigprocmask(SIG_SETMASK, &prev_one, NULL);  //父进程解除屏蔽
 }
 exit(0);
}

3.2 避免显式竞争

有时候主程序需要显式地等待某个信号处理运行。例如shell程序,它必须等待当前的前台进程结束,被SIGCHLD处理程序回收之后,才能继续创建另一个进程。主进程在等待的这段时间应该干些什么才最好呢?我们可以用一个无限循环语句,让主进程就在那执行。但这样也太浪费CPU的资源了;我们也可以用一个sleep或者nanosleep函数让主进程休眠,但到底休眠多长时间不好把握,间隔太小同样会造成多次循环,间隔太大,程序又会太慢。

合适的解决办法是,引入sigsuspend函数,其函数原型为:

#include <signal.h>​
int sigsuspend(const sigset_t *mask);      //返回-1

它暂时挂起调用它的进程,利用参数mask替换当前的信号阻塞集,直到收到一个信号并进入处理程序(如果是终止信号,就直接返回),处理完之后返回主进程,并恢复原来的阻塞集。

下面例子展示了主进程在创建完子进程后,如何利用该函数显式的等待SIGCHLD的到来,以达到同步的效果。

#include <signal.h>
​
volatile sig_atomic_t pid;
​
void sigchld_handler(int signum)
{
 int olderror = errno;
 pid = waitpid(-1, NULL, 0);
 int errno = olderrno;
}
​
void sigint_handler(int signum)
{

}
​
int main(int argc, char **argv)
{
 sigset_t mask,prev;

 Signal(SIGCHLD, sigchld_handler);
 Signal(SIGINT, sigint_handler);
 sigemptyset(&mask);
 sigaddset(&mask, SIGCHLD);

 while(1){
 sigprocmask(SIG_BLOCK, &mask, &prev);  //屏蔽SIGCHLD信号
 if(fork() == 0)  //子进程
 exit(0);

 pid = 0;
 while(!pid){ 
 sigsuspend(&prev);  //挂起并等待SIGCHLD信号的到来,其处理函数会使得pid大于0
 }

 sigprocmask(SIG_SETMASK, &prev, NULL);

 printf("...");
 }
 exit(0);
}

[1]: 引用:Unix Network Programming: The Sockets Networking API,第三版,第一卷
参考书目:《深入理解计算机系统》第3版 Randal E.Bryant David R.O'Hallaron著


获取更多知识,请点击关注:
嵌入式Linux&ARM
CSDN博客
简书博客

上一篇 下一篇

猜你喜欢

热点阅读