进程信号

        在学习进程间通信时,我们曾经学过一种方式叫信号量,它就相当于一个计数器,而当时我还在想信号量与信号有什么关系呢?其实信号量与信号是两件截然不同的事物。接下来我们就来学习一下信号,然后就会发现信号和信号量到底有什么不同?

       在我们生活中,关于信号的例子特别多,比如红绿灯啊,手机、闹钟铃声啊,十二点的下课铃声啊等等都是一种信号。

1、信号的基本概念

(1)信号的引入

       其实,在Linux中信号无处不在,举个栗子:

  •  当用户输入一条命令时,在shell下启动一个前台进程;
  • 当用户按Ctrl +C组合键时,键盘产生了一个硬件中断;
  • 如果CPU当前正在执行这个进程的代码,则该进程的用户空间代码暂停执行,CPU从用户态转至内核态去处理硬件中断;
  • 终端驱动程序将Ctrl+C解释成一个SIGINT信号,记在该信号的PCB中(也可以说发了一个信号给该进程);
  • 当某个时刻要吃内核态返回到该进程的用户空间代码继续执行之前,首先检查PCB中是否有记录的信号,当发现有一个SIGINT信号待处理,而这个信号的默认处理动作是终止程序,所以直接终止进程而不再返回它的用户空间代码执行。

   但是有几点注意

  •  Ctrl+C产生的信号只能发给前台进程。一个命令后面加个&就可以让程序在后台进行运行,然后你就可以运行其他的程序而不用担心只能运行一个程序了。
  • shell只能在前台运行一个进程,但可以有多个后台进程。      
  • 前台进程在运行过程中用户可以随时按下Ctrl+C进而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到SIGINT信号使进程终止,所以信号对于进程的控制流程来说是异步的。

 (2)信号的种类

    在Linux中,信号的种类有很多,我们可以通过一条命令来查看

    用kill  -l 命令来查看系统定义的信号列表:


        如果你细心的话,你回发现其实编号到64的信号表没有32号和33号信号,所以其实一共只有62个信号。每一个信号都有自己的编号和一个宏定义名称,这些宏定义可以在头文件signal.h中找到。其实这些信号我们可以对他们进行分类,1~31号为普通信号,34~64号为实时信号。目前,我们只讨论普通信号。

      在此我必须要说明一下,9号信号是不能被捕获的。

      如果你要是对每个信号有好奇心,你可以使用命令man  7  signal进行查看每个信号的意义以及产生条件等详情。

(3)信号的产生方式

  •  用户在终端按下某些键时,终端驱动程序会发送信号给前台进程。例如Ctrl-C产生SIGINT信号,Ctrl-\产生SIGQUIT信号,Ctrl-Z产生SIGTSTP信号(可使前台进程停止)
  •  硬件异常产生信号。这些条件由硬件检测到并通知内核,然后内核想当前进程发送相应信号。例如 当前进程执行力除以零的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号  发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个解释为SIGEGV信号发送给进程。
  • 一个进程调用kill(2)函数可以发送信号给另一个进程  。可以用kill(1)命令发送信号给某个进程,kill(1)命令也是调用kill(2)函数实现的,如果不明确指定信号则发送SIGTERM信号,该信号的默认处理动作是终止进程。当内核监测到某种软件条件发生时也可以通过信号通知进程。例如闹钟信号SIGALRM信号,向读端已经关闭的管道写数据时产生SIGPIPE信号。如果奴相按默认动作处理信号,用户程序可以调用sigaction(2)函数告诉内核如何处理某种信号(即用户自定义处理动作)。
  • 软件条件产生。

(4)信号的处理方式

      信号有产生,就意味着肯定有相应的处理方式,就像人别给你打一个电话你肯定会有所行动,要么接要么拒绝,信号也一样,信号也有三种处理方式;

  • 忽略此信号;
  • 执行该信号的默认动作(绝大多数信号的默认动作为终止程序(进程))
  • 用户自定义一个函数,当内核在处理该代码时从内核态到用户态切换的时候,进行判断是否有信号的产生,有就执行用户自定义的函数来进行信号处理,这一动作也叫做信号的捕捉
2、信号的产生

       在我们的生活中,信号随处可见,比如说“红绿灯”、“铃声”等,这些信号的产生都是有规律的,有的是人为设置的,那么在计算机中的信号是如何产生的呢?

       计算机中产生信号的方式有四种,接下来我们就来看看信号的具体产生方式;

  (1)通过终端按键产生信号

      在我们允许程序时,假如我们要观察某个现象或者结果时,我们可能会写个死循环来让程序一直跑起来,然后我们按Ctrl-C时,进程终止,实质上是产生了SIGINT信号,而SIGINT信号的默认动作就是终止进程;也可以按Ctrl-\,不过该组合键产生的是SIGQUIT信号SIGQUIT信号的默认处理动作是终止进程并且Core Dump

     接下来我们就来验证一下:

#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<signal.h>

void handler(int sig)
{
	printf("get a %d signal\n",sig);
}
int main()
{
	signal(SIGINT,handler);
	while(1)
	{
		sleep(2);
		printf("hello ,I'm yezi \n");
	}
	return 0;
}

     通过程序运行结果,我们可以看出,Contrl+C 确实是2号信号SIGINT,而当我们对SIGINT信号的默认动作进行重写时,它的默认动作已经变为执行用户自定义的捕捉函数;而我们对Contrl+\没有进行用户自定义改写,所以它任然执行他的默认动作终止进程并且产生Core Dump。

       那么Core Dump又是什么呢?我们经常听到大家说到程序core掉了,需要定位解决,这里说的大部分是指对应程序由于各种异常或者bug导致在运行过程中异常退出或者中止,并且在满足一定条件下会产生一个叫做core的文件。 通常情况下,core文件会包含了程序运行时的内存,寄存器状态,堆栈指针,内存管理信息还有各种函数调用堆栈信息等,我们可以理解为是程序工作当前状态存储生成第一个文件,许多的程序出错的时候都会产生一个core文件,通过工具分析这个文件,我们可以定位到程序异常退出的时候对应的堆栈调用等信息,找出问题所在并进行及时解决。

(2)调用系统函数向进程发信号

      kill命令是调用kill函数来实现的。kill函数可以给一个指定的进程发送知道的信号。raise函数可以给当前进程(也就是自己给自己)发送知道的信号。

       #include <signal.h>

        int kill(pid_t pid,int signo);       给任意进程发送指定信号

        int raise(int signo);       自己给自己发送指定信号

        这两个函数的返回值相同,都是成功返回0,错误返回-1;

     还有一个abort函数也能够终止进程,不过abort函数是使当前进程收到信号而异常终止

        #include<stdlib.h>

         void abort(void);       给自己发送6号信号

         该函数跟exit函数相似,abort函数总是会成功的,所有没有返回值。

(3)由软件条件产生信号

       由软件条件产生的信号其实也有很多,就像我们以前学管道时遇到的SIGPIPE(13号信

号),不过这次我们来学习另外一个alarm函数和SIGALRM信号。

        #include<unistd.h>

        unsigned int alarm(unsigned int seconds);

        调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒后给当前进程发送

SIGALRM信号,而该信号的默认动作是终止当前进程;

         这个函数的返回值是0或者是以前设定的闹钟时间还剩下的秒数。

接下来我们就来设置一个闹钟,一秒内计算time加加了多少次?

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int time=0;

int main()
{
    alarm(1);
    while(1)
    {
        time++;
        printf("time = %d\n",time);
    }
    return 0;
}

(4)由硬件异常产生的信号

          由硬件异常产生的信号是由硬件检测到并通知内核,然后内核向当前进程发送适当的信

号。比如当前进程访问了非法内存地址,,MMU()会产生异常,内核将这个异常解释为发送了

SIGSEGV信号(11号信号)给进程,再例如当前进程出现了除零错误,CPU的运算单元会产生

异常,内核将这个异常解释为发送了SIGFPE信号(8号信号)给进程。 

3、阻塞信号

 (1)信号的相关概念

  •   实际执行信号的处理动作称为信号递达(Delivery)。
  • 信号从产生到递达的状态,称为信号未决(Pending)。
  • 进程可以选择阻塞(Block)某个信号。
  • 被阻塞的信号产生时将保持在未决状态,直到进程接触对此信号的阻塞,才执行递达的处理动作。
  • 在这里,要强调的是,忽略和阻塞是不同的,只要信号被阻塞就不会被递达,而忽略是在递达之后的一种处理动作。

(2)信号在内核中的表示

        信号的发送实质上就是把进程PCB中的信号位图中的比特位进行修改,当为0时,表示未收到该信号,当把比特位由0改为1 时表示收到了该信号,比特位的位数为信号的标号,第一个比特位代表1号信号,以此类推。信号在内核中的表示如下图:


      每个信号都有两个标志物分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才消除该标志

。在上图的栗子中,信号1未阻塞也未产生过,当它递达时执行默认动作。信号2产生过,但正在被阻塞,所以暂时不能递达。虽然他的默认动作是忽略,但在没接触阻塞之前,该信号不能被忽略,因为进程人有机会改变处理动作之后再接触阻塞。信号3未产过,一旦产生它将被阻塞,它的处理动作是用户自定义函数,如果在进程解除对某信号的阻塞之前该信号产生过很多次,将会如何处理呢?POSIX.1允许系统递送该信号一次或多次。而在Linux写,普通信号在递达之前产生多次只能算一次。

(3)信号集操作函数

        在上图中未决和阻塞标志可以用相同的数据类型sigset_t来存储.sigset_t实际上是一个长整型数组,数组的每个元素的每个位表示一个信号。这种定义方式和文件描述符集fd_set类似我们用这个类型可以表示每个信号的“有效”或“无效”状态。在阻塞信号集其含义是该信号是否被阻塞;在未决信号集中就代表该信号是否处于未决状态。 阻塞信号集也叫当前进程的信号屏蔽字(Signal Mask)。

         sigset_t类型对于妹子信号用一个bit表示“无效”或“有效”状态,至于这个类型内部如何存储这些bit则依赖于系统函数实现,从使用者的角度这些都无从重要,使用者只能调用一下函数来操作sigset_t变量,而不应该对它的内部数据做任何解释。  

      接下来就来了解一下这些操作函数:

     #include <signal.h>

     int sigemptyset(sigset_t *set);    清空信号集

     int sigfillset(sigset_t *set);    在信号集中初始化所有信号

     int sigaddset(sigset_t *set, int signum);   将信号signum添加至信号集set中

     int sigdelset(sigset_t *set, int signum);    将信号signum从信号集set中删除

     int sigismember(const sigset_t *set, int signum);   测试signum是否在信号集set中 

    返回值:前四个函数都是成功返回0,失败返回-1;

                 最后一个函数是判断某个信号是否在信号集中函数,存在返回1,不存在返回0,

出错返回-1

               在使用sigset_t类型的变量之前,⼀定要调用sigemptyset或sigfillset做初始化,使信

号集处于确定的状态。  除此之外,系统还提供了两个函数用来读取或更改当前进程的信号屏

蔽字(block表)和未决信号集(pending表)。

       Sigprocmask

       调用 Sigprocmask可以读取或者更改进程的信号屏蔽字(阻塞信号集);

       #include <signal.h>

       int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

       参数:how:如何更改;set:指定新的信号屏蔽字;oldset:保存原来的信号屏蔽字

       返回值:成功返回0,失败返回-1

                    如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。 

                    如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。

     列个表格说明一下how的取值: 


     sigpending        

      #include <signal.h>

      int sigpending(sigset_t *set);   读取当前的未决信号集

     参数:set:保存当前进程的未决信号集;

     返回值:成功返回0,失败返回-1

       下面以实例形式展示:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>

//捕捉2号信号,方便我们查看解除阻塞后的信号状态
void handler(int signo)
{
    printf("Get a %d signal\n",signo);
}
//显示未决信号表
void printsigset(sigset_t *set)
{
    int i=0;
    for(i=0;i<32;i++)
    {
        if(sigismember(set,i))//判断信号是否在信号集中
        {
            printf("1");
        }
        else
        {
            printf("0");
        }
        printf(" ");
    }
    printf("\n");
}


int main()
{
    sigset_t s,p;
    sigemptyset(&s);//清空信号集
    sigemptyset(&p);

    sigaddset(&s,2);//将2号信号加入信号集中
    sigprocmask(SIG_BLOCK,&s,&p);//设置信号屏蔽字,阻塞2号信号
    signal(2,handler);//当2号信号解除阻塞后捕捉它
    sigset_t  pset;
    int count=0;
    while(1)
    {
        sigpending(&pset);//读取信号未决表
        printsigset(&pset);
        sleep(1);
        if(count++==5)//count累加到5时,解除阻塞
        {
            printf("unblock signal\n");
            //恢复信号屏蔽字
            sigprocmask(SIG_SETMASK,&p,NULL);
        }
    }
    return 0;
}

 4、信号的捕捉

        信号的处理方式中由用户自定义的信号处理函数,当内核从内核态到用户态切换时,检测是否有收到信号,若有信号的产生,则要先进行对信号的处理,即执行用户自定义的信号处理函数,这就叫做对信号的捕捉。信号的捕捉说明了也就是一种信号处理的方式。

 (1)内核实现信号捕捉

        接下来我们就用图来了解一下信号的捕捉过程:


       由图我们可以看出,信号捕捉过程中,一共从内核态转至用户态,再从用户态转至内核态一共进行了四次转换,其中交点处为信号的检查。

(2)信号捕捉函数

      sigaction()函数:

      #include <signal.h> 

      int sigaction(int signo, const struct sigaction *act,  struct sigaction *oact);

      参数:signo:要捕捉的信号

                act:若非空,表示根据act修改信号signo的处理动作

                oact:保存该信号原来的处理动作 

       返回值:成功返回0,失败返回-1

       sigaction函数可以读取和修改与指定信号相关联的处理动作。

       pause()函数

       #include <unistd.h>

       int pause(void);

      返回值:如果信号的处理动作是终止进程则进程终止。pause函数没有机会返回 

                   如果信号的处理动作是忽略,则进程继续处于挂起状态。pause不返回 

                   如果信号的处理动作是捕捉,则调用了信号处理函数之后pause返回-1

      pause函数使调用它的进程挂起,直到有信号递达

     接下来我们用alarm和pause函数来实现一个sleep(3)函数,称为mysleep.

#include<stdio.h>
#include<signal.h>
#include<unistd.h>

void sig_alarm(int signo)
{
     //捕捉闹钟信号但是不做任何事
}

unsigned int mysleep(unsigned int seconds)
{
	struct sigaction new,old;
	unsigned int unslept=0;
	new.sa_handler=sig_alarm;
	sigemptyset(&new.sa_mask);
	new.sa_flags=0;
	sigaction(SIGALRM,&new,&old);   注册信号处理函数
	alarm(seconds);     设置闹钟
	pause();
	unslept=alarm(0);    清空闹钟
	sigaction(SIGALRM,&old,NULL);       恢复信号默认处理动作
	return  unslept;
}

int main()
{
	while(1)
	{
		mysleep(5);
		printf("5 seconds paused \n");
	}
	return 0;
}

       不过,这个代码也有点小问题,你们先自行琢磨。提醒一下,是时序问题哦。

  5、可重入函数

           什么是可重入函数呢?接下来我们先看一个例子。     


      上面的图说明一个什么看懂了吗?我来捋一捋吧。

       main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚把第一步做完,然后因为硬件中断使进程切换到内核,再次回用户态之前检测有信号待处理,于是切换到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入的两步都做完之后,从sighandler函数返回内核态,再次回到用户态就从刚才没完成的main函数调用的insert函数中继续往下执行,进行insert函数的第二步,结果就是,main函数和sighandler函数前后向向链表中插入两个节点,而最终只有一个节点插入成功。

         就如上图中所展示的例子,insert函数被不同的控制流程调用,有可能在第一次调用还没结束返回就再次进入该函数,这就称为重入。insert函数访问一个全局链表,有可能因为重入而造成错乱,这样的函数称为不可重入函数,反之,如果一个函数值访问自己的局部变量或参数,则称为可重入函数。

    当一个函数符合以下条件之一就不是可重入函数:

  • 调用malloc或free。因为malloc也是用全局链表来实现管理堆的。
  • 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

接下来我们再来学习一个关键字:volatile

      相信在学习C语言的你也见过这个关键字,这个关键字有什么作用呢?你可能想都不想就会说保证内存的可见性。那么保证内存的可见性到底是什么呢?就拿上图的例子来说,main函数和sighandler函数都调用insert函数就有可能出现链表的错乱,其根本原因 在于对全局变量的插入操作要分两步完成,不是一个原子操作,假如这两个操作必定一起完成,中间不会被打断,那么就不会出现问题。

       在C语言中,一个简单赋值语句只有一句,但若把他写成汇编来看,则就有三句与之对应,由此可见这也不是原子操作。

        对于程序中存在多个执行流程访问同一全局变量时,volatile限定符是必要的。此外,虽然程序只有单一的执行流程,但是变量属于以下情况之一的也需有volatile限定符限定:1、变量的内存单元中的数据不需要写操作就可以自己发生变化,每次读上来的值都不一样;2、即使多次向内存单元中写数据,只写不读,也并不是在做无用功,而是有特殊意义的,比如映射到内存地址空间的硬件寄存器

      竞态条件这个概念就是在上面的mysleep函数中提出的,其实也就是由时序问题而导致错误,这就是竞态条件。

6、SIGCHLD

       在前面学习进程的时候我们提过通过wait和waitpid这两个函数可以回收清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞地查询是否有子进程结束等待清理(也就是轮询)。采用第一种方式,父进程阻塞了就不能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时的轮询一下,程序实现太复杂。

        其实,子进程在终止时会给子进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不用操心子进程的状态了,子进程退出时会通知父进程,然后父进程在循环处理函数中调用wait函数清理子进程即可。      

       


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值