linux信号

学习思路

  1. 信号产生前,信号如何产生。
  2. 信号发送中,信号发送的本质,底层OS涉及到哪些操作。
  3. 收到信号之后如何处理。

1. 信号入门,什么是信号

  • 信号的本质是一种通知机制,用户或者操作系统通过发送一定的信号,通知进程,某些事件已经发生,进程需要在后续合适的时机进行处理。
  • 进程既然要处理信号,那么一定需要具备信号 “识别” 的能力。
  • 信号的产生是随机的,进程可能正在忙自己的事情,所以,信号的处理,不一定是立即处理,可能是延后处理。
  • 既然可能需要延时处理信号,那么就需要进程临时记录下对应的信号。
  • 信号的产生相对于进程而言是异步的。

信号产生的方式

(1). 组合键

1.通过键盘组合键ctrl+c (2号信号),ctrl+\ (3号信号):用户按键,OS解释组合键,查找进程列表当前前台正在运行的进程,OS写入对应的信号到进程PCB的位图结构中。

(2). 软件条件

  1. 由软件条件产生信号。由OS识别到某种软件条件触发,构建信号,发送给指定的进程。
  • 示例1:创建匿名管道,父进程读取,子进程写入,父进程关闭读端,子进程一直写入,OS通过发送信号SIGPIPE来终止写端子进程,父进程waitpid提取出信号。
#include <cstdio>
#include <cstdlib>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    int pipefd[2] = { 0 };
    int ret = pipe(pipefd);
    if(ret == -1){
        perror("create pipe fail...\n");
        return 1;
    }
    pid_t id = fork();
    if(id < 0){
        perror("fork error...\n");
        return 2;
    }
    else if(id == 0){
        //child process
        close(pipefd[0]);
        const char* str = "Hello World!!!";
        int cnt = 0;
        //写端一直写
        while(1)
        {
            write(pipefd[1], str, strlen(str));
            sleep(1);
            //if(++cnt == 5){
            //    break;
            //}
        }
        close(pipefd[1]);
        exit(6);
    }
    else{
        //parent process
        close(pipefd[1]);
        char buffer[1024];
        memset(buffer, 0, sizeof buffer);
        int cnt = 0;
        while(1)
        {
            ssize_t sz = read(pipefd[0], buffer, sizeof buffer);
            if(sz > 0){
                buffer[sz] = 0;
                printf("child say: %s\n", buffer); 
            }
            else if(sz == 0){
                printf("写端关闭,读端也关闭....\n");
                break;
            }
            else{
                //读取异常
            }
            //读端读取三次后关闭
            if(++cnt == 3){
                break;
            }
            sleep(1);
        }
        close(pipefd[0]);
        int status = 0;
        int ret = waitpid(id, &status, 0);
        if(ret == id){
            if(WIFEXITED(status)){
                printf("child process exit code : %d\n", WEXITSTATUS(status));
            }
            else{
                printf("child process exit signal: %d\n", status&0x7f);
            }
        }
        else{
            //wait fail...
        }
    }
    return 1;
}
  • 父进程读端关闭后,子进程收到13号信号终止。
    在这里插入图片描述
  • 示例2:调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。
  • alarm函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数。

(3). 系统调用

  1. 调用系统函数向进程发信号。
  • kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号。
  • raise函数可以给当前进程发送指定的信号(自己给自己发信号)。
  • abort函数使当前进程接收到6号信号而异常终止。
#include <signal.h>
int kill(pid_t pid, int signo);
int raise(int signo);
这两个函数都是成功返回0,错误返回-1#include <stdlib.h>
void abort(void);
就像exit函数一样,abort函数总是会成功的,所以没有返回值。

(4). 硬件异常

  1. 由于硬件异常产生信号。
  • 理解除0异常:当CPU执行进程代码发生除0错误,CPU内部的状态寄存器,有对应的状态标记位,溢出标记位,OS会自动进行计算完毕之后的检测,如果溢出标记位为1,则OS会立即识别到有溢出问题,找到当前正在运行的进程pid,向该进程的task_struct信号位图中写入信号,完成信号发送。
  • 理解空指针异常:通过虚拟地址空间+MMU (Memory Manager Unit),将虚拟地址转化为物理地址,而MMU是硬件,野指针、越界访问非法地址都会被MMU识别,当然也会被OS识别到。

总结信号的产生

  • 所有的信号产生,都有它的来源。可能是来自用户按下的组合键,可能是kill命令,或者kill、raise、abort相关系统调用接口,可能是管道通信中
    读端关闭,写端继续,或alarm()闹钟,定时器,以及由于硬件错误产生信号。但是最终所有信号全部是由OS识别,解释,并发送的。

补充核心转储

  • 系统调用接口waitpid(pid_t id, int* status, int options) 中的输出型参数status所指向的整形的第8个比特位,代表着当进程出现某种异常的时候,是否由OS将当前进程在内存中的相关数据,转存到磁盘中,此操作即为核心转储,主要是方便事后调试。
ulimit -a //查看进程的Resource Limit

ulimit -c //ulimit命令改变Shell进程的Resource Limit,允许core文件最大为1024K

(gdb)core-file core.2885 //gdb中使用core文件

2. 信号发送中

相关概念

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

在这里插入图片描述

  • 每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。
  • 信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。
  • 在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
  • SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
  • SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。
  • 如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?
  • POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。
  • 每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。
  • 因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态。
  • 在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。
  • 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。

信号集操作函数

#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
  • 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。
  • 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系统支持的所有信号。
  • 注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
  • 这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset); 
返回值:若成功则为0,若出错则为-1
  • 调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)
  • 如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。* * 如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。
  • 如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。
    在这里插入图片描述
  • 如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。
#include <signal.h>
int sigpending(sigset_t *set);
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1
  • 下面尝试阻塞1-31号信号,然后通过shell脚本给进程依次发送号信号,观察进程的未决信号集。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void print_sigset(const sigset_t* set)
{
    for(int i = 1; i < 31; i++)
    {
        if(sigismember(set, i))
        {
            putchar('1');
        }
        else
        {
            putchar('0');
        }
    }
    putc('\n', stdout);
}

int main()
{
    //定义信号集对象并清空初始化
    sigset_t s, p;
    sigemptyset(&s);

    for(int i = 1; i <= 31; i++)
    {
        //将1-31号信号全部添加至信号集对象s中
        sigaddset(&s, i);
    }
    //设置阻塞信号集
    sigprocmask(SIG_BLOCK, &s, nullptr);
    while(1)
    {
        sigpending(&p); //获取未决信号集
        print_sigset(&p); //打印未决信号集
        sleep(1);
    }
    return 0;
}
#!/bin/bash

i=1
id=$(pidof mysignal)
while [ $i -le 31 ]
do
    if [ $i -eq 9 ];then
        let i++
        continue
    fi
    if [ $i -eq 19 ];then
        let i++
        continue
    fi
    kill -$i $id
    echo "kill -$i $id"
    let i++
    sleep 1
done
  • 注意9号信号以及19号信号不会被捕捉和阻塞。

3. 信号处理

信号处理的过程与时机初略理解

  • 如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。
  • 由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下:
  • 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程。 sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。
  • 用户态是一个受管控的状态,进程执行自己的代码。
  • 内核态执行操作系统的代码,具有非常高的优先级。
  • 在内核态中,当进程从内核态返回用户态的时候,进行信号检测以及处理。系统调用,异常,中断,进程切换等都会使进程陷入内核。
  • 在cpu内部存在寄存器,例如CR3寄存器表征当前的执行权限,例如1表示内核态,3表示用户态。
    在这里插入图片描述
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
  • sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回- 1。signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非 空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体:
  • 将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函 数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。
  • 当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。

可重入函数

  • 函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入。
  • 函数访问全局数据,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入函数。

从信号角度重新理解volatile关键字

#include <signal.h>
#include <cstdio>

int flag = 0;
//volatile int flag = 0;
void handler(int sig)
{
    printf("chage flag 0 to 1\n");
    flag = 1;
}

int main()
{
    signal(2, handler);
    while (!flag)
        ;
    printf("process quit normal\n");
    return 0;
}

g++ test_volatile.cc -O2
  • 不使用volatile关键字,并且编译时加上优化选项。
    在这里插入图片描述
  • 此时发生了数据的一致性问题,优化后的代码,直接从寄存器中读取flag变量的值,此时再通过发送信号改变flag只是改变内存中的值,循环不会退出。解决办法: 加上volatile关键字,告知编译器,该变量取值必须从内存读取,保持内存的可见性。

SIGCHILD信号

  • 子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

void handler(int sig)
{
    pid_t id;
    while ((id = waitpid(-1, NULL, WNOHANG)) > 0)
    {
        printf("wait child success: %d\n", id);
    }
    printf("child is quit! %d\n", getpid());
}

int main()
{
    signal(SIGCHLD, handler);
    pid_t cid;
    if ((cid = fork()) == 0)
    { // child
        printf("child : %d\n", getpid());
        sleep(3);
        exit(1);
    }
    while (1)
    {
        printf("father proc is doing some thing!\n");
        sleep(1);
    }
    return 0;
}
[zh@VM-4-10-centos 信号]$ g++ test_SIGCHILD.cc 
[zh@VM-4-10-centos 信号]$ ./a.out 
father proc is doing some thing!
child : 27228
father proc is doing some thing!
father proc is doing some thing!
father proc is doing some thing!
wait child success: 27228
child is quit! 27227
father proc is doing some thing!
father proc is doing some thing!
father proc is doing some thing!
father proc is doing some thing!
father proc is doing some thing!
^C
[zh@VM-4-10-centos 信号]$ 
  • 要想不产生僵尸进程的办法:
  1. 父进程调用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可用。
int main()
{
    struct sigaction sig_act;
    sig_act.sa_handler = SIG_IGN;
    sigaction(SIGCHLD, &sig_act, nullptr);
    if(fork() == 0)
    {
        cout << "child: " << getpid() << endl;
        sleep(5);
        exit(0);
    }

    while(true)
    {
        cout << "parent: " << getpid() << " 执行我自己的任务!" << endl;
        sleep(1);
    }

    return 0;
}

在这里插入图片描述

  1. 通过signal注册函数,手动设置对SIGCHILD信号进行忽略。
  • 这里要理解手动设置忽略与操作系统默认忽略的区别,OS的默认忽略动作,是指不去回收子进程的内核数据,需要父进程等待回收退出信息之后,再释放空间。而手动设置忽略相当于告诉了OS我并不关心子进程的退出信息,此时子进程退出时,OS会直接释放相关内核数据结构并且不会向父进程发送SIGCHILD信号。
// 如果我们不想等待子进程,并且我们还想让子进程退出之后,自动释放僵尸子进程
int main()
{
    // OS 默认就是忽略的,OS的忽略是静静的等待父进程回收资源
    signal(SIGCHLD, SIG_IGN); // 手动设置对子进程进行忽略,手动的忽略是告知OS,我不关心子进程的退出信息

    if(fork() == 0)
    {
        cout << "child: " << getpid() << endl;
        sleep(5);
        exit(0);
    }

    while(true)
    {
        cout << "parent: " << getpid() << " 执行我自己的任务!" << endl;
        sleep(1);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值