Linux之————信号

一. 信号

我们在shell下运行起来一个程序,可以在这个进程正在运行的时候键盘输入一个Ctrl+C,就会看到这个进程被终止掉了,其实当我们键入Ctrl+C的时候是向进程发送了一个SIGINT信号,这时候产生了硬件中断则系统会从执行代码的用户态切入到内核态去处理这个信号,而一般这个信号的默认处理动作是终止进程,因此正在运行的进程就会被终止了。像SIGINT就是系统中定义的一个信号,除此之外还有其他信号,可以通过 kill -l 命令来查看:

如上图中有从1到64的各种信号,但是32和33号信号并没有,因此系统中一共有62个信号,其中1至31号是系统中的普通信号,而34到64是实时信号,这里暂不讨论。

-------------------------------------------------------------------------------------------

二. 信号的产生

那么,像上面的那些信号都是如何产生的呢?

  • 首先,是由终端输入产生的,就像刚才举的栗子,从键盘输入一条命令其实就是在向一个进程发送特定的信号,但是像Ctrl+C这条命令只能发给前台进程,对后台进程是不起作用的,一个shell可以同时运行一个前台进程和多个后台进程;
  • 其次,可以调用系统函数向进程发送信号,比如kill命令其实是终止一个进程,但kill命令是调用kill函数来实现的,kill函数可以给一个指定的进程发送特定的信号,其命令使用如下:

在上面的命令中,signal是为指定要发的信号,可以是信号的名字也可以是上面图片中每个信号前面的代码号;而pid就表示要发送信号的进程的pid号;

下面就可以写一个死循环的程序并且放在后台运行,当Ctrl+c对后台进程不起作用时就可以使用kill命令来给指定的进程发送2号信号也就是SIGINT信号也就是Ctrl+c,信号的处理方式为默认的也就是终止一个进程:

当输入一行命令"kill -2 6156"时按一下回车并没有结果,是因为当按下回车时要先一步回到bash等待用户输入下一条命令,这里并不希望命令的结果和用户输入的下条命令产生冲突显示错乱,因此要按两次回车才能看到进程被中断的结果;

除了kill函数还有一个函数raise函数,该函数可以给当前进程发送信号也就是自己给自己发送信号,这两个函数都是成功返回0,失败返回-1:

类似的abort函数是向当前进程发送6号信号也就是SIGABRT信号,该信号是使进程异常终止,和exit函数一样,该函数一定会成功因此并没有返回值;

  • 还有一种是由软件条件产生的信号,比如以前谈论匿名管道的时候,当管道的读端关闭写端仍继续往里写入的时候进程就会接收到系统发来的一个SIGPIPE信号,该信号的默认处理动作是终止进程;

    然而这里谈论一个函数alarm函数,该函数用于设定特定的秒数,而当超时的时候系统就会发送一个SIGALRM信号给该进程,而这个信号的默认处理动作仍然是终止进程:

函数参数seconds是指要设定的秒数,函数的返回值为0或者返回以前设定的闹钟余下的秒数,比如当闹钟设定时间还未到但又重新设定了一个闹钟,那么原来闹钟就会返回余下的时间,而当seconds值为0时,表示清除设定的闹钟,闹钟的返回值仍然是余下的秒数;

栗子时间:

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

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

    return 0;
}

上面的程序是设定了一个闹钟,闹钟的时间为1秒,而在这一秒内不断地进行i++,当一秒钟之后,闹钟到时,系统就会给进程发送一个SIGALRM的信号,这个信号的处理动作会终止这个进程;

程序运行结果:

结果显示,也就是在一秒内i进行自加的次数为13628次;

-------------------------------------------------------------------------------------------

三. 信号的处理方式

一般来说,当一个信号产生之后,系统对其的处理方式有如下三种:

  • 可以忽略这个信号,并不执行任何动作;
  • 执行信号的默认处理动作,而大多数普通信号的默认处理动作是终止一个进程;
  • 可以执行用户自定义的处理动作,这称为信号的捕捉(catch);

信号被捕捉,也就是当产生了这个信号的时候,要执行用户自定义的函数处理动作,而捕捉一个信号的处理函数有signal和函数sigaction:

signal函数参数中,

signum是要捕捉信号的代表号码;

handler是一个函数指针,可以为用户自定义的一个函数,表示信号要处理的动作函数;

sigaction函数同样是捕捉一个信号执行用户自定义的函数,

signum同样是为要捕捉的信号号码;

act则是一个数据结构,数据结构的定义如下:

数据结构中,sa_handler和signal函数中是一样的,是一个函数指针可以指向用户自定义的函数,当sa_handler赋值为SIG_IGN的时候表示信号的默认处理动作为忽略这个信号,赋值为SIG_DFL的时候表示执行默认处理动作,而sa_mask表示需要额外屏蔽的一些信号,当信号处理函数返回时自动恢复为原来状态;sa_flag表示一些标志来修改信号的行为,这里设置为0即可;sa_sigaction表示实时信号的处理函数,这里不讨论;而sa_restorer是过时的且不应该被使用的,POSIX并不提供该元素;

oldact是指信号原来的数据结构信息,当其不为空的时候就将修改之前的信息保存其中以便日后恢复;

除了上面两种信号的捕捉函数,还有一个函数pause,该函数使进程挂起直到有信号递达,如果此号的处理动作是执行系统默认处理动作,则进程终止该函数没有返回值;如果信号的处理动作是忽略,则进程会一直处于挂起状态,pause同样没有机会返回;如果信号的处理动作是捕捉这个信号执行用户自定义的函数,则pause会出错返回-1并设置错误码,因此和程序替换函数exec有些类似,只有出错返回值;

下面举个栗子来使用信号的捕捉函数,可以自我实现一个sleep函数:

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

void handler(int sig)//用户自定义函数
{
    printf("i get a sig %d\n", sig);
}

void mysleep(unsigned int time)
{
    struct sigaction new, old;//设定两个结构体
    new.sa_handler = handler;
    sigemptyset(&new.sa_mask);
    new.sa_flags = 0;
    sigaction(SIGALRM, &new, &old);//注册信号处理函数,捕捉SIGALRM信号
    alarm(time);//设定闹钟为自定义时间
    pause();//将进程挂起直到闹钟结束向进程发送SIGALRM信号
    alarm(0);//撤销闹钟
    sigaction(SIGALRM, &old, NULL);//恢复对信号的默认处理动作
}

int main()
{
    //主函数内要实现的是每隔两秒打印一句“hello world...”
    while(1)
    {   
        mysleep(2);//实现mysleep
        printf("hello world...\n");
    }   

    return 0;
}

运行程序,结果如下:

对于上面的过程,其实是有一个重要的点要解释清楚的,那就是系统是如何完成从信号产生到信号被处理这一系列动作的;这里需要知道的是,信号并不是一产生就立即被处理的,而是有一个合适的契机来决定是否处理这个信号,具体解释如下图:

-------------------------------------------------------------------------------------------

四. 信号的状态

信号从产生到进程接收到是有一个中间过程的,当进程接收到一个信号也就是信号的递达(delivery);而信号从产生到递达这之间的状态叫信号的未决(pending);但同时,信号还有一种状态叫信号的阻塞(block),也就是信号产生处于未决状态但没有被递达而是被阻塞住了;这里需要强调的是,信号的阻塞和信号的忽略是不一样的,信号的忽略是在信号递达之后的一种处理方式,而信号的阻塞是信号还没有被递达。

信号的状态在每个进程的PCB中都有一张可以说是相对应的表来表示相应的状态,信号产生时,内核就会在进程PCB块中的未决表中设置相应的信号位,同样的,当一个信号信号被阻塞,也有一个block表来记录信号是否被阻塞,另外还有一张handler表来记录信号的处理方式;

因为我们这里讨论1~31个系统的普通信号,而每一个信号是否产生(未决)是否被阻塞都可以用两个状态0和1来记录表示,其实也就类似于32位平台下一个int类型的32个比特位的状态,这里的状态在未决表中0和1表示信号是否产生,而block表则表示信号是否被阻塞;但是,信号的产生并不影响信号的阻塞,当一个信号产生时在未被递达之前是可以被阻塞的;同样,信号是否阻塞也并不影响信号是否产生,一个信号被阻塞了,当它产生时只是不会被递达而已;两者并没有关系;

sigset_t是可以用来存储信号的未决和阻塞状态的,称为信号集。它可以被看做是由32个比特位组成的,而每一个比特位表示信号是否有效和无效,至于内部到底是如何存储的,作为使用者暂时是不用关心的;阻塞信号集也叫作当前信号的信号屏蔽字;

而关于信号集的操作函数如下:

sigemptyset函数表示清空一个信号集,该信号集中不包含任何有效信号;

sigfillset函数表示将一个信号集全部置位,该信号集包含了所有系统中的有效信号;

sigaddset函数表示将一个有效信号添加到某个信号集中;

sigdelset函数表示将一个有效信号从某个信号集中删除;

sigismember函数的返回值是一个bool类型,用于判断一个信号集中是否包含某个有效信号;

函数参数中set是指向sigset_t类型的指针,signum则是信号的号码;

这里需要强调的是,在使用sigaddset函数和sigdelset函数对一个信号集进行添加或删除某个有效信号之前,需要调用sigemptyset函数或者sigfillset函数将信号集进行初始化;

上面的函数成功返回0,失败返回-1;

函数sigprocmask函数可用于修改和读取进程的信号屏蔽字:

函数参数中,

set表示要修改的信号集;

oldset表示修改之前信号集的状态,以便恢复;

how表示如何修改,为SIG_BLOCK时,表示将set信号集中的有效信号阻塞;为SIG_UNBLOCK时,表示将信号集中的而有效信号解除阻塞,而为SIG_SETMASK时,表示值就为set;

设置了信号屏蔽字之后,同样可以读取当前进程信号的未决状态,也就是信号产生与否:

函数从内核中读取出未决信号集状态并保存到set中;

下面举个栗子使用上面的信号集操作函数:

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

void printpending(sigset_t *p) 
{
    int i = 0;
    for(i = 1; i < 32; ++i)
    {   
        if(sigismember(p, i)) //判断1~31号信号是否产生
            printf("1");
        else
            printf("0");
    }   
    printf("\n");
}

int main()
{
    sigset_t b, p;//设置两个信号集block和pending
    sigemptyset(&b);//将block信号集初始化
    sigaddset(&b, SIGINT);//将SIGINT信号也就是Ctrl+c产生的信号添加到block信号集中
    sigprocmask(SIG_BLOCK, &b, NULL);//设置进程的信号屏蔽字,阻塞SIGINT信号

    while(1)
    {   
        sigpending(&p);//每隔1秒获取当前进程的信号未决状态
        printpending(&p);
        sleep(1);
    }   
    return 0;
}

上面的程序阻塞掉了2号信号也就是SIGINT信号,因此,当键入Ctrl+c的时候,产生了SIGINT信号,但是因为信号被阻塞了因此就一直处于未决状态并不会递达,每隔一秒获取当前进程信号未决状态查看对应信号是否产生,运行程序:

可以看到,当键入Ctrl+c的时候产生了2号信号,而相应的pending信号集中的第二位就会被置为1,说明2号信号也就是SIGINT信号产生。

因为2号信号被阻塞并不会递达被处理,因此Ctrl+c并不能终止程序,可以用Ctrl+\来终止程序。

-------------------------------------------------------------------------------------------

五. 总结

  1. 信号的产生:终端输入、调用系统函数向进程发送指定的信号、软件条件产生比如错误和异常;
  2. 信号的处理方式:忽略、执行默认处理动作(一般是终止进程)、执行用户自定义动作(捕捉);
  3. 信号的状态:未决、阻塞、递达;未决和阻塞之间相互不影响,而只有信号处于未决状态且没有被阻塞时才会被递达;
  4. 对于信号的捕捉,其实是在用户和内核之间来回切换的,用户--(因为错误、异常或中断进入内核)-->内核--(在处理完异常之后检查是否有信号要处理,若被捕捉进入用户)-->用户--(执行完信号处理函数重新返回内核)-->内核--(重新返回异常产生处继续执行用户代码)-->用户。

《完》

时间: 2024-10-15 15:53:05

Linux之————信号的相关文章

Linux进程间通信——信号

一.什么是信号 用过Windows的我们都知道,当我们无法正常结束一个程序时,可以用任务管理器强制结束这个进程,但这其实是怎么实现的呢?同样的功能在Linux上是通过生成信号和捕获信号来实现的,运行中的进程捕获到这个信号然后作出一定的操作并最终被终止. 信号是UNIX和Linux系统响应某些条件而产生的一个事件,接收到该信号的进程会相应地采取一些行动.通常信号是由一个错误产生的.但它们还可以作为进程间通信或修改行为的一种方式,明确地由一个进程发送给另一个进程.一个信号的产生叫生成,接收到一个信号

转 linux 之信号

APUE讲信号真是生涩,着实读者无趣,如是找一篇总结比较好的博客. 原文http://blog.chinaunix.net/uid-24774106-id-4061386.html Linux编程,信号是一个让人爱恨交加又不得不提的一个领域.最近我集中学习了Linux的signal相关的内容,分享出来,也为防止自己忘记.     信号的本质是异步.异步一这个词,听着高端大气上档次,又让人云山雾绕,其则不然.其实我们想想,我们这个世界是异步的,每个人干事儿,并不总是 A->B->C->D这

Linux:信号下

在Linux:信号上博文中我们写了一个mysleep,但是实际上这个函数在多线程环境下是会出现错误的,也就是我们这个mysleep函数并不是可重入函数, 现在重新审视"mysleep"程序,设想这样的时序: 1. 注册SIGALRM信号的处理函数. 2. 调用alarm(nsecs)设定闹钟. 3. 内核调度优先级更高的进程取代当前进程执行,并且优先级更高的进程有很多个,每个都要执行很长时间 4. nsecs秒钟之后闹钟超时了,内核发送SIGALRM信号给这个进程,处于未决状态. 5.

linux进程信号

linux进程信号 SIGHUP 终止进程 终端线路挂断 SIGINT 终止进程 中断进程 SIGQUIT 建立CORE文件终止进程,并且生成core文件 SIGILL 建立CORE文件 非法指令 SIGTRAP 建立CORE文件 跟踪自陷 SIGBUS 建立CORE文件 总线错误 SIGSEGV 建立CORE文件 段非法错误 SIGFPE 建立CORE文件 浮点异常 SIGIOT 建立CORE文件 执行I/O自陷 SIGKILL 终止进程 杀死进程 SIGPIPE 终止进程 向一个没有读进程的

Linux之信号

产生信号五种方法: 按键产生:ctrl+c.ctrl+z.ctrl+\ 系统调用产生:如kill.raise.baort 软件条件产生:如定时器alarm 硬件异常产生:非法访问内存(段错误).除0(浮点数例外).内存对齐错误(总线错误) 命令产生:如kill命令 信号四要素: 编号.名称.事件.默认处理动作 信号的处理方式: 执行默认动作: 忽略(丢弃): 捕捉(调用用户处理函数) Linux内核的进程控制块PCB是一个结构体,task_struct除了包含进程id,状态,工作目录,用户id,

Linux之信号第二谈

重提信号概念 前一篇中提到了信号的概念,并且对信号的产生及一些属性有了一定的认识,这一次,要从更加深入的角度看待信号. 之前提到过,当我的进程在接收到信号之前,就已经知道了,当我接收到某种信号之后就要发生某一项动作,换句话说,在进程内部,一定存在这某种结构,将这些信息都记录了下来,很明显,对于进程而言,这些信息都会保存在它的PCB当中. 首先我们来认识这样几个概念: 信号递达(Delivery):执行信号的处理动作: 信号未决(Pending):信号从产生到递达之间的状态: 阻塞(Block):

Linux进程间通信 -- 信号集函数 sigemptyset()、sigprocmask()、sigpending()、sigsuspend()

我们已经知道,我们可以通过信号来终止进程,也可以通过信号来在进程间进行通信,程序也可以通过指定信号的关联处理函数来改变信号的默认处理方式,也可以屏蔽某些信号,使其不能传递给进程.那么我们应该如何设定我们需要处理的信号,我们不需要处理哪些信号等问题呢?信号集函数就是帮助我们解决这些问题的. 有关Linux进程间使用信号通信的更多内容,可以参阅我的另一篇文章,Linux进程间通信 -- 信号量函数 signal().sigaction() 下面是信号函数集: 1.int sigemptyset(si

Linux 下信号理解(一)

Linux提供了信号传递进程消息的机制,什么是信号?它是一种非常短的消息,短到只有一个数字.值得强调的是信号和信号量只少了一个字,但他们完全是不同的概念,信号量仅用于同步代码段,而信号则用于传递消息. 一 .信号的编号:通过kill -l 命令可以看到 二.信号机制 可以通过man 7 signal 三.几种默认处理信号的方式: Term表示终止当前进程. Core表示终止当前进程并且Core Dump 生成core文件用于调试(Core Dump 用于gdb调试). Ign表示忽略该信号. S

浅析 Linux/UNIX 信号机制

附件:linux man手册关于signal的介绍 信号常常被称为“软中断”,和“中断”类似,用来通知程序发生异步事件.对信号的处理一般来说有三种方式:忽略,终止进程以及使用信号处理函数.信号处理函数的方式是从一处执行流断开,转而去运行另外的一处代码(信号处理),当处理函数返回时,继续从断开的地方继续执行. 1.安装信号处理函数 在系统编程的层面上与信号的处理关系最直接相关的函数有两个,他们用来安装信号处理函数: sighandler_t signal(int signum, sighandle

Linux:信号(上)

从我的博客之前的文章中,一直都在强调操作系统是由多进程协同工作而实现整个操作系统的逻辑目的,达到一个人为操纵的系统: 之前的博文一直都在描述进程控制,进程之间的通信,还有线程控制与操作,但是却一直没有描述多个进程之间的相互是怎么通知整个系统的.对于多个不同进程之间,出现了异常或者进程间非数据传输的通信怎么实现,所以Linux系统就实现了一个概念叫做信号: 信号就相当于我们现实生活之中的通知信息,去告知自己或者别人处于某种状态,是不是很人性化,信号的概念出现,解决了进程与进程通知: 那么关于信号,