linux系统编程 信号

简介: linux系统编程 信号

一、信号

1.1 什么是信号

信号是一种向进程发送通知,告诉其某件事情发生了的一种简单通信机制。

1.2 信号的命名

Linux下边定义了很多的信号,所有的信号都是一个整数编号,不过为了好辨识,Linux系统给这些整
数编号都定义了对应的宏名,宏名都是以SIG开头,比如SIGABRT,
  SIG:signal的缩写
  ABRT:abort的缩写
  宏名SIG***,***是对信号的描述,ABRT表示放弃的意思,向进程发送SIGABRT信号,进程会被信号
异常终止。在前面的课程我们就讲过,当我们调用abort函数时,该函数就会向进程发送一个SIGABRT信号
,你一看到ABRT就知道与abort函数有关系。
疑问:这么多的信号,都有哪些呢?记不住怎么办?
答:不用担心,后面会详细解释这个问题。

1.3 谁会向进程发送信号

总结起来,会有三个“人”会向进程发送信号,分别是“另一进程”、“OS内核”、“硬件”。
(1)另一个进程发送信号
      比如在命令行终端窗口通过kill命令向某个进程发送一个信号将其终止。
      演示:
      kill PID
(2)内核发送信号
      发生了某个事件,Linux内核可能会发送该事件对应的信号给某个进程
      进程从管道文件读取数据,但是管道文件的读权限被关闭了,进程会被内核发送一个SIGPIPE
    信号,提示读管道出错了。
      后面讲进程间通信时,会讲到这个问题。
(3)底层硬件发送信号
    底层硬件发生了某个事件,会向进程发送对应的某个信号
    比如按下ctrl+c按键终止进程时,内核收到ctrl+c按键后,会向正在运行的进程发送SIGINT信
  号,将其异常终止。
    不管进程是被哪一个信号给终止了,只要是被信号终止的,都是异常终止。

1.4 进程收到信号后,进程会如何处理

三种处理方式,分别是忽略、捕获、默认。
忽略
    忽略的意思就是说,进程就当信号从来没有发生过。
  这就好比别人送了封信给你,但是你忽略这封信的存在,那么这封信将不会对你产生任何影响。
捕获
  捕获的意思就是说,进程会调用相应的处理函数,进行相应的处理。
默认
  如果不忽略也不捕获的话,此时进程会使用系统设置的默认处理方式来处理信号。

1.5 都有哪些信号

1.5.1 信号列表

1) SIGHUP       2) SIGINT    3) SIGQUIT         4) SIGILL      5) SIGTRAP
6) SIGABRT      7) SIGBUS    8) SIGFPE          9) SIGKILL      10) SIGUSR1
11) SIGSEGV     12) SIGUSR2   13) SIGPIPE       14) SIGALRM    15) SIGTERM
16) SIGSTKFLT  17) SIGCHLD    18) SIGCONT      19) SIGSTOP    20) SIGTSTP
21) SIGTTIN     22) SIGTTOU   23) SIGURG        24) SIGXCPU      25) SIGXFSZ
26) SIGVTALRM  27) SIGPROF  28) SIGWINCH        29) SIGIO        30) SIGPWR
31) SIGSYS        34) SIGRTMIN  
35) SIGRTMIN+1    36) SIGRTMIN+2   37) SIGRTMIN+3    38) SIGRTMIN+4
39) SIGRTMIN+5    40) SIGRTMIN+6    41) SIGRTMIN+7    42) SIGRTMIN+8  
43) SIGRTMIN+9    44) SIGRTMIN+10   45) SIGRTMIN+11   46) SIGRTMIN+12
47) SIGRTMIN+13   48) SIGRTMIN+14   49) SIGRTMIN+15 50) SIGRTMAX-14 
51) SIGRTMAX-13   52) SIGRTMAX-12   53) SIGRTMAX-11 54) SIGRTMAX-10 
55) SIGRTMAX-9    56) SIGRTMAX-8    57) SIGRTMAX-7   58) SIGRTMAX-6 
59) SIGRTMAX-5    60) SIGRTMAX-4    61) SIGRTMAX-3   62) SIGRTMAX-2
63) SIGRTMAX-1    64) SIGRTMAX  
在命令行执行kill -l,可以显示所有的信号列表。  
  总共62个信号,也就是说每个进程可以接收的信号种类有62种,1~64为信号的编号,SIG***为信号
的宏名。
(1)为什么这么多信号?
  每个信号代表着某种事件,一般情况下,当进程收到某个信号时,就表示该信号所代表的事件发生了。
  可能发生的事件会有很多种,所以弄出了这么多的信号,每个信号对应着自己特有的事件。  
(2)这么多信号,我记不住怎么办?
  (1)35~64:这些信号是Linux后期增设的信号,这些个信号不需要关心,所以不用了解。
  (2)1~34:也不是所有的信号都要掌握,我们只关心其中常用的信号
  (3)就算对1~34中的于常用信号,我们的要求是理解,而不是记忆,因为我也记不住,当你忘记了信
  号名字时,kill -l查看即可
      总之对于这些个信号,理解才是关键。

1.5.2 常用信号

信号宏名    信号编号      说明                  系统默认处理方式
--------------------------------------------------------------
SIGABRT     6         终止进程,调abort函数是产生    终止,产生core文件
SIGALRM     14          超时,调用alarm函数时产生     终止
SIGBUS      7         硬件故障            终止,产生core文件
SIGCHLD     17          子进程状态改变         忽略
SIGINT      2         终止进程(ctrl+c)        终止
SIGIO     29          异步通知信号            终止    
SIGKILL     9         无条件终止一个进程,不可以被捕获或忽略 终止
SIGPIPE     13          写没有读权限的管道文件时      终止
SIGPOLL     8         轮询事件,涉及POLL机制       终止        
SIGQUIT     3         终止进程(ctrl+\)      终止,产生core文件
SIGSEGV     11          无效存储访问(指针错误)    终止,产生core文件   
SIGTERM     15          终止,kill PID时,默认发送的就是这个信号  终止    
SIGUSR1     10          用户自定义信号1          终止  
SIGUSR2     12          用户自定义信号2          终止
(1)为什么当进程收到某些信号时,会被终止呢?
  1)比如ctrl+c发送SIGINT给进程时
  2)比如操作一个错误地址,内核发送SIGSEGV信号给进程时
  3)比如ctrl+\发送SIGQUIT信号给进程时
  4)比如在命令行使用kill pid,默认发送SIGTERM信号给进程时
    因为你发送的这些信号的处理方式是终止,所以进程会被终止掉。
(2)kill命令
  1)kill的作用
  (a)kill -l:查询信号列表
  (b)通过PID向进程发送一个信号
    kill只是发送信号,至于进程会不会被终止,这就看信号的处理方式,处理方式如果是终止,那
  么就会终止进程。所以进程是否会被信号终止,是由信号的处理方式来决定的,而不是kill命令,当
  初起名字的时候,如果把kill起名为send估计更好理解些,因为kill所起到的作用只是发送信号。
    发送信号的完整格式:kill -信号编号 PID
    信号编号写数字和宏名都可以。
    如果不写明信号编号的话:kill PID,默认发送的是15(SIGTERM)信号,等价于kill 
  -SIGTERM PID或者kill -15 PID
    只有发送15这个信号时才能省略信号编号,发送其它信号时必须写明信号编号。
  (c)为什么kill发送15(SIGTERM)这个信号时,信号编号可以省略?
    我们说每个信号都有自己的特有用途,当初定义15(SIGTERM)信号的目的,就是专门给我们从
  命令行终端窗口,发送该信号去终止某个进程用的,也就是说如果你想在命令行使用kill命令去中断某
  个进程时,专用信号就是15。
    其实一般只要默认处理方式是终止的信号,我们都可以使用Kill命令将其发送给某个进程,这些
  信号都可以将进程终止掉,但是既然每个信号有自己的专门用途,那么在命令行去终止某个进程时,我
  们最好是使用专门的SIGTERM信号,这样显得更专业些。
    为了方便我们使用,Linux允许发送15这个信号时,不用设置信号编号,kill pid时会帮你默认
  发送这个信号。
  2)pkill
      kill命令的兄弟,用法与kill差不多,只不过kill是按照PID来识别进程的,pkill是按
    照名字来识别进程的。
      Pkill -信号编号 名字:pkill -SIGINT  a.out
      同样的,如果不写明信号编号的话,默认发送的是15(SIGTERM)这个信号。
(3)信号的发送与接收
  1)发送
    一般来说,大多数发送信号的原因,都是因为内核、硬件发生了某些事件时,才会向某个进程发送
  该事件专用的信号,告诉该进程这个事件发生了。
    不过对于我们自己写的进程来说,其实更多是接收信号,而不是发送信号。
  我们自己发送信号的原因无非如下几种情况:
  (a)ctrl+C、ctrl+\发送信号,终止正在运行进程
  (b)命令行执行kill命令发送信号,终止跑飞的进程,通常都是发送15这个信号
      15这个信号的名字叫SIGTERM,TERM就是terminal的缩写,其实就是命令行终端的意思。
      当无法使用ctrl+c、ctrl+\来终止进程时,往往就使用kill命令来终止进程。
      那么什么样的进程会使用crtl+c、ctrl+\来终止?
      只有当进程有占用命令行终端时,才能crtl+c、ctrl+\来终止。
  (c)因为某些特殊需求,往往在我们自己的程序里面,需要调用kill函数向另一个进程发送某个信号
      · 什么特殊需求呢?
        比如A进程和B进程需要协同工作,A进程将相应事件准备好以后,可能需要发送
      一个信号给B进程,通知B进程,B进程收到信号后,就知道该事件已经准备好,可以
      配合A进程做事了。
      · 至于说自己写的进程发送什么信号合适呢?
        其实发送哪一种信号都行,但是我们说几乎每一种信号都有自己特定的用途。
        所以我们自己的程序在发送信号时,如果发送别人专用事件的信号的话,这样显得很不
      正规,而且很可能造成误会,比如我的A进程给另一个进程发送了一个SIGSEGV信号,B进
      程还以为发生了指针错误事件,其实发生并不是指针错误事件,而是其它事件,这就造成了
      理解的错误。
        因此自己进程调用kill函数发送信号时,我们可以发送的是SIGUSR1、SIGUSR2这两
      个自定义信号,所谓自定义就是,信号所代表的事件,可以由程序员自己根据实际情况来
      规定。
  2)接收
      对于我们自己写的进程来说,最常见信号操作的还是接收信号,不过在一般情况下,我们进
    程并不会去重新设置信号的处理方式,而是使用信号的默认处理方式来处理信号。
      虽然很少重新设置信号的处理方式,但是总还是有这种需求的,所以我们后面会介绍,如何
    调用API来重新设置信号的处理方式。
(4)core文件
  1)什么是core文件
    用于保存程序(进程)在当前结束的这一刻,进程在内存中的代码和数据,core文件可以用于分
  析进程在结束时的状况,不过由于进程代码和数据都是二进制的,所以把core文件直接打开后我们
  是看不懂的,一般需要特殊软件翻译后才能看懂。
  2)并不是所有的信号在终止进程时都会产生core文件
    只有某个些信号在终止进程时才会产生core文件,不过一般情况下并不会创建这个文件,因为系
  统默认将产生core的设置给关闭了,只有打开后这个设置后才会保存core文件。
    所以当你看到提示core dumped,这就表示这个信号终止进程时,会产生core文件,只不过由于
  关闭了设置,因此core文件被丢弃了,dumped就是丢弃的意思。           
      演示:
        例子1:crtl+\ 发送SIGQUIT信号
        例子2:操作错误地址
  3)如果你不想丢弃core文件怎么办?
    对相关的系统文件进行设置就可以了,core文件一般默认保存在当前路径下。
    由于core在实际开发中基本用不到(除非某些很特殊的场合),所以我们这里不再深入介绍
  core文件。

二、signal函数

2.1 函数原型

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
(1)功能:设置某个信号的处理方式。
  处理方式可以被设置为忽略,捕获,默认。
  进程的进程表(task_struct)中会有一个“信号处理方式登记表”,专门用于记录信号的处理方式,
调用signal函数设置某个信号的处理方式时,会将信号的处理方式登记到该表中。
  每个进程拥有独立的task_struct结构体变量,因而每个进程的“信号处理方式登记表”都是独立的,
所以每个进程对信号的处理方式自然也是独立的,互不干扰。
(2)参数
  1)signum:信号编号。
  2)handler:信号处理方式。
    sighandler_t是被typedef后的类型,原类型 void (*)(int),这是一个函数指针类型。
    sighandler_t handler也有直接写成void (*handler)(int)。
    sighandler_t signal(int signum, void (*handler)(int));
    (a)忽略:SIG_IGN
    (b)默认:SIG_DFL
    (c)捕获:填写类型为void (*)(int)的捕获函数的地址,当信号发生时,会自动调用捕获函数
    来进行相应的处理。
          当然这个捕获函数需要我们自己来实现,捕获函数的int参数,用于接收信号编号。
          捕获函数也被称为信号处理函数。
          void signal_fun1(int signo)
          {
            ...
          }
          void signal_fun2(int signo)
          {
            ...
          }
          int main(void)
          {
            signal(SIGINT, signal_fun1);
            signal(SIGSEGV, signal_fun2);
            return 0;
          }
          捕获函数什么时候被调用?
          进程接收到信号时就调用,调用时会中断进程的正常运行,当调用完毕后再会返回
        进程的正常运行。
  (3)返回值
      成功:返回上一次的处理方式
      失败:返回SIG_ERR宏值,并且设置errno。

2.2 调用捕获函数的过程

当信号没有发生时,进程正常运行,当信号发生时,进程的正常运行会被中断,然后去处理信号,一看
信号的处理方式是捕获,就会从“信号处理方式登记表”中将捕获函数的地址取出并执行捕获函数,捕获函数
执行完毕后,恢复进程的正常运行。
  不过当信号来时,如果当前有一条指令正在运行,会先等这条指令运行执行完毕后再去调用信号处理
函数。       
  不过如果捕获函数有调用exit或者_exit的话,进程会被终止,不过是正常终止。
  如果信号处理函数有提前执行return的话,会提前返回到主线。

2.3 值得强调的地方

(1)信号被设置为SIG_DFL时,表示将处理方式设置为默认
      其实在不做任何处理方式设置的情况下,信号的处理方式就是系统设置的默认处理方式。
(2)信号被设置为SIG_IGN(忽略)时
    进程将不会再接收到这个信号,这信号对进城没有任何影响。
(3)设置为捕获时,需要将handler设置为捕获函数的地址,类型为void (*)(int)
  为了确保和捕获函数的类型统一,SIG_DFL、SIG_IGN和SIG_ERR宏的类型也必须是void (*)(int)。
    #define SIG_DFL ((void (*)(int))0)  
    #define SIG_IGN ((void (*)(int))1)
    #define SIG_ERR ((void (*)(int))-1)
    验证这些值。
    这几个宏定义在了<signal.h>头文件中。
(4)除了SIGKILL这两个信号外,其它所有的信号都可被忽略和捕获。
      之所以不能忽略的原因,就是怕你把所有的信号都给忽略后,当你的程序跑飞后,除了重启
    机器外,你还就真没有办法终止跑飞的程序了,所以Linux规定SIGKILL这两个一定不能被忽略
    和捕获,至少还有一个保底操作。
(5)在windows下结束进程是怎么回事
      在任务管理器里面结束任务,其实就是向进程发送一个信号,进程收到这个信号后,就会被
    信号终止掉。这一点其实与Linux是一样的。

三、子进程对父进程信号的继承情况

在上一章里面我们讲过,父进程fork出子进程时,子进程会继承父进程很多的属性,其中就包括信号,
那么本小节我们就来看一看,子进程对父进程信号的继承情况。

3.1 fork创建子进程,但是没有exec加载新程序时,信号的继承情况

在fork子进程之前,如果父进程调用signal设置了某个信号的处理方式的话,那么fork出的子进程会
继承父进程对该信号设置的处理方式,比如fork前,父进程将SIGINT设置为了捕获或者忽略,子进程
将继承设置的这个处理方式。
  父进程将信号的处理方式设置为捕获时,捕获函数对子进程也是有效的。
  再次强调,只有在fork之前,父进程所设置的信号处理方式,才会被子进程继承。
(1)为什么捕获函数在子进程里面依然有效。
    因为子进程复制了父进程的代码和数据,子进程自然也会包含信号处理函数的代码,所在子进程中
  依然有效。
(2)子进程可不可以自己调用signal函数,修改掉所继承的处理方式。
    当然可以。
(3)那如果父进程是在if(ret > 0){}里面设置得呢?
    这就是父进程自己的设置,跟子进程没有关系。

3.2 当有调用exec加载新程序时

fork之前,父进程设置的处理方式是忽略 或 默认时
  exec加载新程序后,忽略和默认设置依然有效。
fork之前,父进程设置处理方式是捕获时
  新程序的代码会覆盖子进程中原有的父进程的代码,信号捕获函数的代码也会被覆盖,既然捕获函数已
经不存在了,捕获处理方式自然也就没有意义了,所以信号的处理方式会被还原为默认处理方式。
  终之,如果子进程所继承的信号处理方式是捕获的话,exec加载新程序后,捕获处理方式会被还原为
默认处理方式。 
我就想让新程序去捕获某个信号怎么办?
  在新程序里面独立的设置。

3.3 总结

仅fork时
  子进程会继承父进程fork之前所设置的信号处理方式。
当有exec加载新程序时
  (1)子进程继承的处理方式是忽略 或 默认处理方式时,exec新程序后设置依然有效。
  (2)如果子进程继承是捕获处理方式时,exec新程序后将被还原为默认处理方式。

四、kill、raise、alarm、pause、abort函数

4.1 kill、raise

函数原型:

#include <sys/types.h>
  #include <signal.h>
  int kill(pid_t pid, int sig);
  kill命令就是调用这个函数来实现。
  #include <signal.h>
  int raise(int sig);
(1)功能
    1)kill:向PID所指向的进程发送指定的信号。
    2)raise:向当前进程发送指定信号。              
(2)返回值
    1)kill:成功返回0,失败返回-1,errno被设置。
    2)rasie:成功返回0,失败返回非0。
  raise函数用的比较少,不过当多个进程协同工作时,kill函数有时还是会用到的。比如向其它进程
发送某信号,通知其某件事情发生了,其它进程收到这个信号后,就会调用信号处理函数进行相应的
处理,以实现协同工作。

4.2 alarm、pause

函数原型:

#include <unistd.h>
  unsigned int alarm(unsigned int seconds);
  int pause(void);
(1)功能
    1)alarm
        设置一个定时时间,当所设置的时间到后,内核会向调用alarm的进程发送SIGALRM
      信号。SIGALRM的默认处理方式是终止。
    2)pause函数
        调用该函数的进程会永久挂起(阻塞或者休眠),直至被信号(任意一个信号)唤醒
      为止。
(2)返回值
    1)alarm:返回上一次调用alarm时所设置时间的剩余值。
        如果之前没有调用过alarm,又或者之前调用alarm所设置的时间早就到了,那么返回
      的剩余值就是0。
    2)pause:
        只要一直处于休眠状态,表示pause函数一直是调用成功的。
      当被信号唤醒后会返回-1,表示失败了,errno的错误号被设置EINTR(表示函数被信
      号中断)。
    alarm函数用的不多,pause在实际开发中也用的不多,不过在开发中往往会使用pause()函数
  来帮助调试,比如我想让程序运行到某位置时就停下,然后分析程序的打印数据,此时就是可以
  关键位置使用pause函数将程序休眠(停下)。不想继续休眠时使用信号唤醒即可。

4.3 abort函数

我们前面的课程介绍过,这个函数也被称为叫自杀函数,之所以称为自杀函数,是因为调用该函数时,
会向当前进程发一个SIGABRT信号,这个信号的默认处理方式是终止,因此如果不忽略和捕获的话,会
将当前进程终止掉。可以把这个函数理解为是raise函数的特例,当raise函数中的参数传递的是abort对应
的信号时,两者所达成的效果是一样的。

五、使用信号唤醒休眠函数

5.1 会导致休眠的函数

我们调用sleep、pause等函数时,这些函数会使进程进入休眠状态,如果你不想继续休眠时怎么办?
  可以使用信号将其唤醒。

5.2 唤醒的方法

给信号登记一个空捕获函数即可,当然你也可以在捕获函数写你要的代码,不过如果仅仅只是用于唤醒
的话,捕获函数的内容一般都是空的。

5.3 唤醒的过程

当信号发送给进程后,会中断当前休眠的函数,然后去执行捕获函数,捕获函数执行完毕返回后,不再
调用休眠函数,而是执行休眠函数之后的代码,这样函数就被唤醒了。

5.4 我想继续休眠怎么办

我希望长期休眠的,但是不小心被别人发送的信号给唤醒了,我想继续休眠怎么办?
  自己手动重新启动休眠函数(重新调用休眠函数)。 
  演示:

例子1:手动重启pause

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
void signal_fun(int signo)
{
  printf("!!!!!!!\n");
}
int main(int argc, char **argv, char **environ)
{
  int ret = 0;  
  signal(SIGINT, signal_fun);
lable:  ret = pause();
  //可以根据pause函数的返回值来判断pause是否被打断了,pause被打断会返回-1,并且
  //errno被置为EINTR,这里使用goto语句让程序即使被信号打断之后继续休眠
  if(ret == -1 && errno == EINTR)
  {
    goto lable;
  }
  printf("hello\n");
  while(1);
  return 0;
}

例子2:手动重启sleep

与pause同理,判断一下sleep的返回值!=0并且使用goto语句即可,因为sleep函数如果时间到了
会返回0,否则返回剩余的时间,所以这里使用!=0来判断就行。

5.5 休眠函数自动重启

比如使用read从键盘读取数据,当键盘没有输入任何数据时,read会休眠,不过函数被信号唤醒后,
会自动重启read的调用。
  注意:
    read函数读数据时,并不一定会休眠,读硬盘上的普通文件时,不管文件有没有数据,read都不会
  休眠,而是会返回继续向下运行,如果read读的是键盘的话,如果键盘没有数据时read就会休眠。
    当read函数休眠时,如果被信号唤醒了,当捕获函数返回后,read会自动重启。
  我们需要记住那些函数是需要手动重启的,哪些函数是需要自动重启的?
    不需要,你只需要记住,对于绝大多数休眠函数来说,被信号中断后,如果你想继续休眠的话,
  需要自己去手动重启,否则就会继续向后运行。
    如果你拿不准是自动重启的,还是需要手动重启的,有有两个方法来判断:
    · 自己去测试一下,如果被信号中断后,后续代码不会被执行的,就是自动的重启的,否者就是
  手动重启的
    · 看函数手册里面返回值的描述,如果描述里面有明确说明该函数可以被信号中断的话,这个函数
  就是手动重启的

六、信号的发送、接收和处理的过程

6.1 信号屏蔽字

6.1.1 信号屏蔽字的作用,以及它被放在了哪里

(1)作用
      屏蔽子的作用就是用来屏蔽信号的,有点像公司前台,信号来了先问前台(屏蔽字),我能
    被立即处理不,能就立即处理,不能就暂不处理。
      每个进程能够接收的信号有62种,信号屏蔽字的每一位记录了每个信号是被屏蔽的还是被
    打开的。如果是打开的就立即处理。如果是屏蔽的就暂不处理。
(2)屏蔽字放在了哪里
    每一个进程都有一个信号屏蔽字,它被放在了进程表(task_struct结构体变量)中。

6.1.2 屏蔽字张啥样子

为了方便理解,我们简单地认为屏蔽字就是一个64位的unsigned int数,每一位对应着一个信号,
如果这一位为0,表示信号可以被立即处理,如果为1表示该信号被屏蔽了,暂不处理。
  1   2   3   4   5           61  62  63  64             
  *   *   *   *   *  ......   *   *   *   * 
  比如:
(1)第1位:对应编号为1(SIGHUP)的信号,该位为
    1)0:表示1(SIGHUP)这个信号是打开的,可以被立即处理
    2)1:表示信号被屏蔽了,暂时不能处理
(2)第2位:对应编号为2(SIGINT)的信号
    1)0:表示2这个信号可以被立即处理
    2)1:表示信号被屏蔽了,暂时不能处理
        ...
        ...

6.1.3 我们可不可以自己修改信号屏蔽字,实现某个信号的打开和屏蔽呢?

可以,后面会专门介绍对应的API,这些API就是用于修改信号屏蔽字的。
  只不过在默认情况下,信号屏蔽字中所有的位都为0,也就说默认将所有的信号都打开了。

6.2 未处理信号集

6.2.1 作用

跟屏蔽字一样,也一个64位的无符号整形数,专门用于记录未处理的信号。
  “未处理信号集”同样也是被放在了进程的进程表中(task_struct)。

6.2.2 什么时候会记录

信号来了,当进程的信号处理机制,检查该信号在屏蔽字中的对应位时发现是1,表示该信号被屏蔽了,
暂时不能被处理,此时就会将“未处理信号集”中该信号编号所对应的位设置为1,这个记录就表示,有一个
信号未被处理。
  这就有点像你去访问领导,前台(屏蔽字)跟你说领导正忙,请你到休息室(未处理信号集)休息。
  如果该信号发送了多次,但是每一次都因为被屏蔽了而无法处理的话,在“未处理信号集”中只记录一次。
  这就有点像别人欠你钱,你去催债,别人还的的慢了,所以你催了好多回,但是不管催多少回,人家只
还你一次。

6.2.3 什么时候处理记录的“未处理信号”

当屏蔽字中该信号的位变成0时(被打开了),此时就回去检查“未处理信号”,看该信号有没有未决的
情况,有的话就处理它。

6.3 信号处理的完整过程

有了屏蔽字和未处理信号集的铺垫,现在就可以来看一看进程处理信号的完整过程了。

图:

6.4 代码演示

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
void signal_fun(int signo)
{
  printf("hello\n");
  sleep(3);
  printf("world\n");  
}
int main(int argc, char **argv, char **environ)
{
  pid_t ret = 0;
  signal(SIGINT, signal_fun);
  while(1);
  return 0;
}

分析:

当程序运行起来时,我连续按多次Ctrl+C,并不会打印对应这么多次的"hello\nworld\n",就是因为
信号屏蔽字的原因,当我第一次按下Ctrl+C,信号屏蔽字会把对应的Ctrl+C信号位置为1(直到信号处理
函数处理完成才会把Ctrl+C信号位置置为0),因为信号处理函数有个3秒的sleep,所以距离第一次三秒之
内的Ctrl+C信号都会被屏蔽。又因为同样的信号在未处理信号集中只会被记录一次,所以加入3秒内按了多次
Ctrl+C,最终会打印两次"hello\nworld\n"。

七、修改信号屏蔽字的API

7.1 修改的原理

(1)定义一个64位的与屏蔽字类似的变量
(2)将该变量设置为要的值
    将某信号对应的位设置为0或者为1。
(3)使用这个变量中的值来修改屏蔽字
  修改方法有三种,当然以下这三种修改方法,我们并不需要自己亲自操作,只需要调用相应的API,
API就会自动的实现。
  1)第一种:完全的替换
    使用变量的值去完全替换掉屏蔽字
    比如:
    屏蔽字 = 变量(1111111...11111)
    屏蔽所有信号,当然里面的SIGKILL和SIGSTOP信号是不能被屏蔽,就算在屏蔽字中它们对应的位
  设置为了1,也不会起到屏蔽的作用。
  2)第二种:使用|操作,将对应的位设置为1,只屏蔽某个或者某两个信号
        屏蔽字 = 屏蔽字 | 变量
        比如:
        屏蔽字 = 屏蔽字 | 0000...10
        将编号为2(SIGINT)的信号,在屏蔽字中对应的位设置为1,屏蔽字中其它的位不变。
  3)第三种:使用位&操作,将对应的位清0,打开信号
      屏蔽字 = 屏蔽字 & (~变量)
      比如:屏蔽字 = 屏蔽字 & (~0000...10)
            屏蔽字 = 屏蔽字 & 1111...01,
      将编号为2(SIGINT)的信号,在屏蔽字中对应的位清0,其它位不变。

7.2 设置变量的API

函数原型
    #include <signal.h>
    int sigemptyset(sigset_t *set);
    int sigfillset(sigset_t *set);
    int sigaddset(sigset_t *set, int signum);
    int sigdelset(sigset_t *set, int signum);
    set就是我们前面说的变量,至于变量名也可以定义为其它的名字,不一定非要叫set。
  (1)功能:设置变量的值
      1)sigemptyset:将变量set的64位全部设置为0。
      2)sigfillset:将变量set的64位全部设置为1。
      3)sigaddset:将变量set中,signum(信号编号)对应的那一位设置为1,其它为不变。
      4)sigdelset:将变量set的signum(信号编号)对应的那一位设置为0,其它位不变。
  (2)返回值
      调用成功返回0,失败返回-1,并且errno被设置。

7.3 使用变量修改屏蔽字的API

7.3.1 函数原型

#include <signal.h>
  int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
  (1)功能:使用设置好的变量set去修改信号屏蔽字。
  (2)参数
    1)how:修改方式,前面说过有三种修改方式。
      (a)SIG_BLOCK:屏蔽某个信号
          屏蔽字=屏蔽字 | set
      (b)SIG_UNBLOCK:打开某个信号(不要屏蔽),实际就是对屏蔽字的某位进行清0操作。
          屏蔽字=屏蔽字&(~set)
      (c)SIG_SETMASK:直接使用set的值替换掉屏蔽字
    2)set:set的地址
    3)oldset:保存修改之前屏蔽字的值
          如果写为NULL的话,就表示不保存。
  (3)返回值:函数调用成功返回0,失败返回-1。

7.3.2 代码演示

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
void signal_fun(int signo)
{
  sigset_t set, oldset;
  printf("hello\n");
  sigemptyset(&set);
  sigaddset(&set, SIGINT);
  sigprocmask(SIG_UNBLOCK, &set, NULL);
  sleep(3);
  printf("world\n");  
}
int main(int argc, char **argv, char **environ)
{
  pid_t ret = 0;
  signal(SIGINT, signal_fun);
  while(1);
  return 0;
}

分析:

上述代码就完成了手动把信号屏蔽字中的Ctrl+C信号位置为0,从而保证我每按下一次Ctrl+C信号处理
函数就处理一次。从而解决上一个代码块中默认处理的问题。

八、sigaction函数

sigaction函数相当于是signal函数的复杂版,不过这个函数在平时用的非常少,因此我们这里不做
详细讲解,了解有以下即可。
目录
相关文章
|
10天前
|
存储 缓存 监控
Linux缓存管理:如何安全地清理系统缓存
在Linux系统中,内存管理至关重要。本文详细介绍了如何安全地清理系统缓存,特别是通过使用`/proc/sys/vm/drop_caches`接口。内容包括清理缓存的原因、步骤、注意事项和最佳实践,帮助你在必要时优化系统性能。
128 78
|
13天前
|
Linux Shell 网络安全
Kali Linux系统Metasploit框架利用 HTA 文件进行渗透测试实验
本指南介绍如何利用 HTA 文件和 Metasploit 框架进行渗透测试。通过创建反向 shell、生成 HTA 文件、设置 HTTP 服务器和发送文件,最终实现对目标系统的控制。适用于教育目的,需合法授权。
52 9
Kali Linux系统Metasploit框架利用 HTA 文件进行渗透测试实验
|
9天前
|
存储 监控 Linux
嵌入式Linux系统编程 — 5.3 times、clock函数获取进程时间
在嵌入式Linux系统编程中,`times`和 `clock`函数是获取进程时间的两个重要工具。`times`函数提供了更详细的进程和子进程时间信息,而 `clock`函数则提供了更简单的处理器时间获取方法。根据具体需求选择合适的函数,可以更有效地进行性能分析和资源管理。通过本文的介绍,希望能帮助您更好地理解和使用这两个函数,提高嵌入式系统编程的效率和效果。
62 13
|
1月前
|
Ubuntu Linux 网络安全
linux系统ubuntu中在命令行中打开图形界面的文件夹
在Ubuntu系统中,通过命令行打开图形界面的文件夹是一个高效且实用的操作。无论是使用Nautilus、Dolphin还是Thunar,都可以根据具体桌面环境选择合适的文件管理器。通过上述命令和方法,可以简化日常工作,提高效率。同时,解决权限问题和图形界面问题也能确保操作的顺利进行。掌握这些技巧,可以使Linux操作更加便捷和灵活。
39 3
|
10天前
|
Ubuntu Linux C++
Win10系统上直接使用linux子系统教程(仅需五步!超简单,快速上手)
本文介绍了如何在Windows 10上安装并使用Linux子系统。首先,通过应用商店安装Windows Terminal和Linux系统(如Ubuntu)。接着,在控制面板中启用“适用于Linux的Windows子系统”并重启电脑。最后,在Windows Terminal中选择安装的Linux系统即可开始使用。文中还提供了注意事项和进一步配置的链接。
28 0
|
1月前
|
Linux
在 Linux 系统中,`find` 命令
在 Linux 系统中,`find` 命令
39 1
|
1月前
|
网络协议 Linux 虚拟化
如何在 Linux 系统中查看进程的详细信息?
如何在 Linux 系统中查看进程的详细信息?
93 1
|
21天前
|
存储 Oracle 安全
服务器数据恢复—LINUX系统删除/格式化的数据恢复流程
Linux操作系统是世界上流行的操作系统之一,被广泛用于服务器、个人电脑、移动设备和嵌入式系统。Linux系统下数据被误删除或者误格式化的问题非常普遍。下面北亚企安数据恢复工程师简单聊一下基于linux的文件系统(EXT2/EXT3/EXT4/Reiserfs/Xfs) 下删除或者格式化的数据恢复流程和可行性。
|
6月前
|
消息中间件 存储 缓存
【嵌入式软件工程师面经】Linux系统编程(线程进程)
【嵌入式软件工程师面经】Linux系统编程(线程进程)
133 1
|
7月前
|
Linux 调度 数据库
Linux下的系统编程——线程同步(十三)
Linux下的系统编程——线程同步(十三)
126 0
Linux下的系统编程——线程同步(十三)