Linux 进程信号

目录

一.信号

1.介绍

2.信号概念

3.查看系统定义的信号列表

4.信号处理的方式

二.信号产生前

1.用户层产生信号的方式

三.产生信号

1.通过按键产生信号

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

(1)kill 

(2)raise 

(3)abort

3.软体条件产生信号

4.硬件异常产生信号

四.阻塞信号

1.信息其它相关概念

2.信号在内核中的表示:

3.sigset_t

4.信号集操作函数

5.sigprocmask

6.sigpending

五.捕捉信号

1.如何实现信号的捕捉

2.sigaction

六.可重入函数

 七.volatile

八.SIGCHLD


前言:这一篇将介绍Linux进程信号,包括产生信号前、产生信号时和产生信号后3个阶段,已经可重入函数、volatile等。            

一.信号

1.介绍

        信号是给进程发送的,进程要具备处理信号的能力。

① 该能力一定是预先早就已经有了的(程序员写的 ---- OS提供)

② 进程能够识别对应的信号

③ 进程能够处理对应信号

对于进程来讲,即使是信号还没有产生,进程已经具有识别和处理这个信号的能力了。

#include <iostream>
#include <unistd.h>

int main()
{
    while(1)
    {
        printf("hello linux\n");
        sleep(1);
    }

    return 0;
}

正常运行一个进程,直接用ctrl+c就可以终止,并且按任何命令都是没有效果的。

但是如果我们用在后面加一个&,就会将这个进程变成后台进程了:

        看这张图,在后面加了&后,会发现按ctrl+c无法终止。但是按命令是有效果的,因为这是后台进程,不会影响命令。

        如果想要查看该后台进程,可以按jobs,看到当前后台进程。

        如果想要将后台进程变为前台进程,可以按fg [jobs中对应的号],这里对应的是1,就是fg 1。这时,按ctrl+c就可以结束进程了。

jobs

fg [jobs中对应的号]

        下面我们去掉打印。

#include <iostream>
#include <unistd.h>

int main()
{
    while(1)
    {
        // printf("hello linux\n");
        sleep(1);
    }

    return 0;
}

这里可以通过jobs查看所有创建的后台进程,然后这里把3进程转为前台进程,并结束。

        如果按了fg之后,又不想转换成后台进程了,就可以先按ctrl+z,这里通过jobs可以看到进程从Running变成了Stopped,然后再用bg [jobs中对应的号],即可变回Running。

bg [jobs中对应的号]

① Ctrl-c 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。

② Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl-C 这种控制键产生的信号。

③ 前台进程在运行过程中用户随时可能按下 Ctrl-C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的。

2.信号概念

        信号是进程之间事件异步通知的一种方式,属于软中断。

3.查看系统定义的信号列表

kill -l

注意看这里31到34之间没有信号,32、33都是没有的,0号信号也没有。

因此呢,这个信号是可以分成两组的,一组是1号到31号,另一组的34号到64号。

1号到31号是普通信号,34号到64号是实时信号。

4.信号处理的方式

        因为信号产生是异步的,所以当信号产生的时候,对应的进程可能正在做更重要的事情,而这时我们进程就可以暂时不处理这个信号。

        进程暂时不处理这个信号,不代表这个信号不会被处理。OS是一定会记住这个信号的。

        信号的捕捉,递达处理动作(信号的处理):

① 默认动作

② 忽视

③ 自定义动作

二.信号产生前

1.用户层产生信号的方式

        信号是会被进程记住的,这个信号会被记录在进程的PCB中,包括了这个信号的所有属性。

        信号有没有产生:比特位在位图中的内容是1代表产生,0代表没有

        什么信号产生:比特位在位图中的位置

take_struct{

        uint32_t sig

}

        只有OS能之间修改这个take_struct内的数据位图。

        OS是进程的管理者,进程的所有的属性的获取和设置,只能由OS来操作。

        无论信号怎么产生,最终一定只能是OS来进行信号的设置。

三.产生信号

1.通过按键产生信号

 ctrl-c其实就是一种信号,是2号信号

sighandler_t signal(int signum, sighandler_t handler)

sighandler_t:返回值为void,参数为int的一个函数指针

signum:对哪个信号设置捕捉动作

handler:是一个函数指针,这个函数允许用户自定义对信号的处理动作

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

using namespace std;

void handler(int signo)
{
    cout << "我是一个进程,刚刚获取了一个信号: " << signo << endl;
}

int main()
{
    // 2号信号
    // 这里不是调用hander方法,这里只是设置了一个会回调,让SIGINT(2)产生的时候,该方法才会被调用
    // 如果不产生SIGINT,该方法不会被调用
    // ctrl+c : 本质就是给前台进程发送2号信号给目标进程,目标进程默认对2号信号的处理,是终止自己
    // 现在更改了对2号信号的处理,设置了用户自定义处理动作
    signal(SIGINT, handler);

    sleep(3);
    cout << "进程已经设置完了" << endl;

    sleep(3);

    while (true)
    {
        cout << "我是一个正在运行中的进程: " << getpid() << endl;
        sleep(1);
    }


    // while(1)
    // {
    //     // printf("hello linux\n");
    //     sleep(1);
    // }

    return 0;
}

这里的SIGINT就是2号信号: 

结果为: 


    signal不是调用hander方法,这里只是设置了一个会回调,让SIGINT产生的时候,该方法才会被调用
    如果不产生SIGINT,该方法就不会被调用
    ctrl+c : 本质就是给前台进程发送2号信号给目标进程,目标进程默认对2号信号的处理,是终止自己
    现在更改了对2号信号的处理,设置了用户自定义处理动作

下面再试一下3号信号

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

using namespace std;

void handler(int signo)
{
    cout << "我是一个进程,刚刚获取了一个信号: " << signo << endl;
}

int main()
{
    // 2号信号
    // 这里不是调用hander方法,这里只是设置了一个会回调,让SIGINT(2)产生的时候,该方法才会被调用
    // 如果不产生SIGINT,该方法不会被调用
    // ctrl+c : 本质就是给前台进程发送2号信号给目标进程,目标进程默认对2号信号的处理,是终止自己
    // 现在更改了对2号信号的处理,设置了用户自定义处理动作
    signal(SIGINT, handler);

    sleep(3);
    cout << "进程已经设置完了" << endl;

    sleep(3);

    while (true)
    {
        cout << "我是一个正在运行中的进程: " << getpid() << endl;
        sleep(1);
    }


    // while(1)
    // {
    //     // printf("hello linux\n");
    //     sleep(1);
    // }

    return 0;
}

这里我们就会发现无论是按ctrl+c,还是按ctrl+\都无法结束进程,都会去调用自定义处理动作。 

那么如何结束呢:

        如果把9号信号也给signal自定义处理动作了,那就无法kill -9来结束了,那么把1到31的信号全部都用signal,自定义处理动作了,我们如何结束?

        其实这个是无法对9号进程signal自定义处理的,OS决定的。

用户层产生信号的方式:

① 键盘产生

        注意这个是键盘产生的信号,不是键盘发送的信号,是OS发送的信号。

那么OS是怎么发送信号的呢?

        OS能找到每个进程的take_struct,也能找到当前显示器上前台进程的take_struct,每一个进程的take_struct内部都有一个位图,OS在拿到了对应的信号后,将这个对应的位置由0设为1,OS就完成了信号的发送(OS发送信号,也可以说成是写入信号)

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

(1)kill 

这个kill不只是一个指令,也是一个函数,下面来介绍该函数:

int kill(pid_t pid, int sig)

作用:向指定进程发送指定信号

成功了就是0,失败了就是-1 

接下来实现一个我们自己的kill:

#include <iostream>
#include <cstdlib>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>

using namespace std;

static void Usage(const std::string& proc)
{
    cerr << "Usage:\n\t" << proc << " signo pid" << endl;
}

// 写一个kill命令,mykill 9 ...
int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }

    if(kill(static_cast<pid_t>(atoi(argv[2])), atoi(argv[1])) == -1)
    {
        cerr << "kill: " << strerror(errno) << endl;
        exit(2);
    }
}

结果如下: 

通过运行我们的进程,并且输入对应的参数,就可以实现kill指令的作用。

如果我们随便kill一个进程,是没有权限的。

(2)raise 

int raise(int sig);

作用:给自己发任意信号

#include <iostream>
#include <cstdlib>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>

using namespace std;

void handler(int signo)
{
    cout << "我是一个进程,刚刚获取了一个信号: " << signo << endl;
}

int main()
{

    signal(2, handler);// 自定义捕捉动作handler
    
    while(1)
    {
        sleep(1);
        raise(2);
    }
}

这里可以看到,使用了raise,就一直给自己发送上面自定义捕捉动作的2号信号。

(3)abort

void abort(void);

作用:终止进程(给自己发6号信号: SIGABRT)

#include <iostream>
#include <cstdlib>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>

using namespace std;

void handler(int signo)
{
    cout << "我是一个进程,刚刚获取了一个信号: " << signo << endl;
}

int main()
{

    signal(2, handler);// 自定义捕捉动作handler
    
    while(1)
    {
        sleep(1);
        abort(2);
    }
}

调用abort,之间就终止了进程。

        这个6号信号是可以被捕捉的,但是捕捉后依旧会退出。

3.软体条件产生信号

unsigned int alarm(unsigned, int seconds);

作用:调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发送SIGALRM信号,该信号的默认处理动作是终止当前进程。

#include <iostream>
#include <cstdlib>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>

using namespace std;

int cnt = 0;

int main()
{
    // 统计这个进程1s,cnt++多少次
    signal(SIGALRM, handler);
    alarm(1);
    while(1)
    {
        printf("hello : %d\n", cnt++);
    }
}

这里可以看到,printf了3万多次后,就结束了,说明在这1s内进行了3万多次的printf

我们试试不带IO,测试一下:

#include <iostream>
#include <cstdlib>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>

using namespace std;

int cnt = 0;

void handler(int signo)
{
    cout << "我是一个进程,刚刚获取了一个信号: " << signo << " cnt: " << cnt << endl;
    exit(1);
}

int main()
{
    // 统计这个进程1s,cnt++多少次
    signal(SIGALRM, handler);
    alarm(1);
    while(1)
    {
        cnt++;
    }
}

这个次数已经达到了4亿多,是带printf的1万多倍,这完全可以说明IO的慢。

4.硬件异常产生信号

我们一定见过进程崩溃的样子,那么崩溃的本质是是什么呢?

        进程崩溃的本质,是该进程收到了异常信号。

下面介绍2个因为硬件异常,而导致OS向目标进程发送信号,进而导致进程终止的现象:

#include <iostream>
#include <cstdlib>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>

using namespace std;

void handler(int signo)
{
    cout << "我是一个进程,刚刚获取了一个信号: " << signo << endl;
    exit(1);
}

// 写一个kill命令,mykill 9 ...
int main(int argc, char *argv[])
{
    // 进程崩溃的本质,是该进程收到了异常信号
    for(int sig = 1; sig <= 31; sig++)
    {
        signal(sig, handler);
    }

    // int a[10];
    // a[10000] = 1000;

    // int *p = nullptr;
    // *p = 100;

    // int b = 10;
    // b /= 0;

}

这里我们依次测试下面注释掉的内容: 

        根据测试结果,我们可以看到,第一个越界和第二个野指针错误,都是11号信号,而第三个除零错误是8号信号。

除零:

        当我们除零的时候,CPU内的状态寄存器会被设置成为:有报错:浮点数越界

        CPU的内部寄存器是硬件,OS就会识别到CPU内有报错了。

        OS知道了是谁报的错,并且是什么报错后,就会让OS->构造信号->向目标进程发送信号->目标进程在合适的时候,处理信号->终止进程

越界&&野指针:

        在语言层面使用的地址(指针),其实都是虚拟地址->转换成物理地址->访问物理内存->读取对应的数据和代码

        如果虚拟地址有问题,地址转换的工作是由(MMU(硬件) + 页表(软件))做的,转化过程就会引起问题->表现在硬件MMU上->OS就发现硬件出现了问题

        OSOS知道了是谁出的问题,并且是什么问题后,就会让OS->构造信号->向目标进程发送信号->目标进程在合适的时候,处理信号->终止进程

那么崩溃了,一定会导致进程终止吗?

        不一定。

#include <iostream>
#include <cstdlib>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>

using namespace std;

void handler(int signo)
{
    cout << "我是一个进程,刚刚获取了一个信号: " << signo << endl;
    //exit(1);
}

int main(int argc, char *argv[])
{
    // 进程崩溃的本质,是该进程收到了异常信号
    for(int sig = 1; sig <= 31; sig++)
    {
        signal(sig, handler);
    }

    int b = 10;
    b /= 0;

    return 0;
}

这里我们把自定义捕捉动作中的exit去掉

根据结果可以看到,虽然崩溃了,但是进程一直在运行。 

        因为我们对其进行了自定义捕捉,修改了默认动作,所以进程没有退出。如果我们不对其进行捕捉,采用默认动作,那么进程还是会退出的。

        因此,出现了异常,进程是否被终止,并不是完全由OS决定的,是由用户来决定的。

        在C++的异常中,我们一般抛出异常后就直接调用exit等函数退出了,那么如果我们不退出会怎么样呢?

        进程就不会被终止,需要我们手动对进程进行终止。

四.阻塞信号

1.信息其它相关概念

递达:实际执行信号的处理动作称为信号递达(Delivery)

未决:信号从产生到递达之间的状态,称为信号未决(Pending)。

阻塞:进程可以选择阻塞 (Block )某个信号。被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.

注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作

2.信号在内核中的表示:

block:阻塞信号集,和pending都是位图,对应的比特位为1,就会拦截对应的信号去递达对应的方法,即使pending为1也没用。

pending:如果pending为1,表示该信号不能被递达,不会被处理。如果pending为0,表示该信号可以被递达,可以被处理。

handler:用来处理信号,信号的编号就作为这个函数指针的数组下标,直接可以访问到对应的自定义的方法,或者系统默认的处理方法。

这3个应该先看pending位,如果pending为1,再去看block,如果block为0,再去看handler。

        这里提到了阻塞信号,而我们之前学知道忽略信号,这两个有什么区别呢?

忽略信号:处理信号的一种,处理的方式是忽略,什么都不做,将pending位图由1置0。

阻塞信号:不是处理信号,是拦截信号,不允许去处理信号。

3.sigset_t

        每个信号只有一个bit的未决标志,非0即1,不会记录该信号产生了多少次,阻塞标志也是这样表示的。
        因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的信号屏蔽字,这里的“屏蔽”应该理解为阻塞而不是忽略。

4.信号集操作函数

        sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的。

① 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);

        第1个是清空位图,第2个是将位图全置1,第3个是添加一个信号,第4个是删除一个信号,第5个是判断某个。

5.sigprocmask

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

作用:可以读取或更改进程的信号屏蔽字(阻塞信号集)

第一个参how数有3个选择:

① SIG_BLOCK:添加信号屏蔽字,相当于 mask = mask | set

② SIG_UNBLOCK:从信号屏蔽字中解除阻塞的信号,相当于mask = mask & ~set

③ SIG_SETMASK:设置当前信号屏蔽字为set所值的值,相当于mask = set

第二个参数set是输入型参数:代表的是新增、删除或覆盖式的重新添加。

第三个参数oset是输出型参数:返回老的信号屏蔽字

        如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。

6.sigpending

int  sigpending(sigset_t *set);

作用:获取进程的pending信号集,通过set参数传出。

调用成功就返回0,出错则返回-1

下面我们来看一下当前进程的pending信号集:

        如果直接获取2号,会因为递达的太快,我们没等看到打印出来,就已经结束了,所以这里我们利用信号屏蔽字,把2号信号屏蔽,来看:

#include <iostream>
#include <cstdlib>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>

using namespace std;

void handler(int signo)
{
    cout << "我是一个进程,刚刚获取了一个信号: " << signo << endl;
    exit(1);
}

static void showPending(sigset_t *pendings)
{
    for(int sig = 1; sig <= 31; ++sig)
    {
        if(sigismember(pendings, sig))
        {
            cout << "1";
        }
        else
        {
            cout << "0";
        }
    }

    cout << endl;
}

int main()
{
    // 3.屏蔽2号信号
    sigset_t bsig, obsig;
    sigemptyset(&bsig);
    sigemptyset(&obsig);
    // 3.1添加2号信号到信号屏蔽字中
    sigaddset(&bsig, 2);
    // 3.2设置用户级的信号屏蔽字到内核中,让当前进程屏蔽到2号信号
    sigprocmask(SIG_SETMASK, &bsig, &obsig);
    
    // 2.signal
    signal(2, handler);
    // 1.不断的获取当前进程的pending信号集
    sigset_t pendings;
    while(true)
    {
        // 1.1 清空信号集
        sigemptyset(&pendings);
        // 1.2 获取当前进程(谁调用,获取谁)的pending信号集
        if(sigpending(&pendings) == 0)
        {
            // 1.3 打印当前进程的pending信号集
            showPending(&pendings);
        }
        sleep(1);
    }

    return 0;
}

        这里就可以看到,我们调用2号信号时,因为信号屏蔽字的原因,pending里对应的位置变成了1,但是却并没有去调用2号信号,因为2号信号被屏蔽了。

        那么如果我们把信号全部屏蔽了会怎么样呢?

        从1开始屏蔽,一直到8都可以成功,但是到9的时候,就会直接把进程killed,而不会被屏蔽。

这里我们把信号全部屏蔽了,然后后面再把2号信号解除,来看一下测试结果:

#include <iostream>
#include <cstdlib>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>

using namespace std;

int cnt = 0;

void handler(int signo)
{
    cout << "我是一个进程,刚刚获取了一个信号: " << signo << endl;
    // exit(1);
}

static void showPending(sigset_t *pendings)
{
    for(int sig = 1; sig <= 31; ++sig)
    {
        if(sigismember(pendings, sig))
        {
            cout << "1";
        }
        else
        {
            cout << "0";
        }
    }

    cout << endl;
}

int main()
{
    cout << "pid: " << getpid() << endl;
    // 3.屏蔽2号信号
    sigset_t bsig, obsig;
    sigemptyset(&bsig);
    sigemptyset(&obsig);
    // // 3.1添加2号信号到信号屏蔽字中
    // sigaddset(&bsig, 2);

    // 全屏蔽
    for(int sig = 1; sig <= 31; ++sig)
    {
        sigaddset(&bsig, sig);
        signal(sig, nullptr);
    }

    // 3.2设置用户级的信号屏蔽字到内核中,让当前进程屏蔽到2号信号
    sigprocmask(SIG_SETMASK, &bsig, &obsig);

    // 2.signal
    // signal(2, handler);
    // 1.不断的获取当前进程的pending信号集
    sigset_t pendings;
    while(true)
    {
        // 1.1 清空信号集
        sigemptyset(&pendings);
        // 1.2 获取当前进程(谁调用,获取谁)的pending信号集
        if(sigpending(&pendings) == 0)
        {
            // 1.3 打印当前进程的pending信号集
            showPending(&pendings);
        }
        sleep(1);
        
        ++cnt;
        if(cnt == 20)
        {
            cout << "解除对2号信号的block..." << endl;
            sigset_t sigs;
            sigemptyset(&sigs);
            sigaddset(&sigs, 2);
            sigprocmask(SIG_UNBLOCK, &sigs, nullptr);
        }
    }

    return 0;
}

        根据结果可以看到,我们在前面kill 一直到了7都没有任何反应,而等到了20s之后,就解除了对2号信号的屏蔽,2号信号就可以递达了,显示器上便出现了自定义捕捉动作的2号信号。

五.捕捉信号

1.如何实现信号的捕捉

进程处理信号,不是立即处理的,是在合适的时候。

那么这个合适的时候,是什么时候呢?

        当 当前进程从内核态,切换会用户态的时候,进行信号的检测与处理。

        OS是在内存中被加载的。

        无论进程怎么切换,都可以找到内核的代码和数据,前提是你只要能够有权利访问。

当前进程如何具备权利,访问这个内核页表,乃至访问内核数据呢?

        要进行身份切换。

        进程如果是用户态的--只能访问用户级页表。

        进程如果是内核态的--访问内核级和用户级的页表。

那么我们怎么知道自己是用户态还是内核态的呢?

        CPU内部有对应的状态寄存器CR3,有比特位标识当前进程的状态,0:内核态;3:用户态

什么时候能切换到内核态呢?

        ① 系统调用的时候

        ② 时间片到了,进程间切换

        ③ 等等

内核态:可以访问所有的代码和数据(不是意味着它一定要访问所有的) -- 具备更高权限

用户态:只能访问自己的。

        我们的进程,会无数次直接或者间接的访问系统级软硬件资源(管理者是OS),本质上,我们并没有自己去操作这些软硬件资源,而是必须通过OS->无数次的陷入内核(1.切换身份 2.切换页表)->调用内核的代码->完成访问的动作->结果返回给用户(1.切换身份 2.切换页表)->得到结果。

        即使是像while(1)这样的代码也是会进行内核态和用户态的切换的。

        因为它有自己的时间片->时间片到了的时候->内核态,更换内核级页表->保护上下文,执行调度算法->选择了新的进程->恢复新进程的上下文 ->用户态,更换成用户级页表->CPU执行的就是新进程的代码。        

        如果有一些系统调用,就会从用户态切换到内核态。

        调用完该接口后,去识别信号,如果pending表为1,block表为0,会去进行自定义处理动作,就会从内核态切换到用户态(注意:这里一定要切换,虽然内核态可以完成用户态的操作,但是如果用户写的是一段恶意代码,那么因为内核态的权限过大,无论什么代码都会执行,就会导致OS受到恶意攻击,而切换成用户态就可以因为权限小而不会去执行该代码)。

        把该动作由用户态执行完,就又会从用户态切换回内核态。

        都处理完之后,就要返回了,而这返回又要从内核态切换回用户态。

简而言之,上面的过程就相当于数学中的无穷大符号∞

2.sigaction

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

作用:检查并且更改一个信号的动作。(设置对特定信号的特定处理的动作)

signo:对哪个信号实施信号自定义捕捉

act:要设置成什么动作

oact:老的动作,如果不要就设为nullptr;如果要就传入一个对象

void (*sa_handler) :对信号的处理方法,充当回调

sa_mask:需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。

        mask作用:当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字。当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段。

 下面我们来使用这个sigaction:

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

using namespace std;

void handler(int signo)
{
    cout << "获取到一个信号,信号的编号是: " << signo << endl;
}

int main()
{
    struct sigaction act, oact;
    act.sa_handler = handler;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);

    sigaction(2, &act, &oact);

    while(true)
    {
        sleep(1);
    }

    return 0;
}

成功对2号信号进行了自定义捕捉。

下面改一下act.sa_handler:

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

using namespace std;

void handler(int signo)
{
    cout << "获取到一个信号,信号的编号是: " << signo << endl;
}

int main()
{
    struct sigaction act, oact;
    act.sa_handler = SIG_IGN;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);

    sigaction(2, &act, &oact);

    while(true)
    {
        sleep(1);
    }

    return 0;
}

这里无论怎么产生2号信号,都会忽略,SIG_IGN就是忽略该信号。

再改一下act.sa_handler:

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

using namespace std;

void handler(int signo)
{
    cout << "获取到一个信号,信号的编号是: " << signo << endl;
}

int main()
{
    struct sigaction act, oact;
    act.sa_handler = SIG_DFL;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);

    sigaction(2, &act, &oact);

    while(true)
    {
        sleep(1);
    }

    return 0;
}

这里产生2号信号就直接退出了,SIG_DFL就是采取默认动作。

        上面说到当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,这里来测试一下:

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

using namespace std;

void handler(int signo)
{
    cout << "获取到一个信号,信号的编号是: " << signo << endl;
    sigset_t pending;
    // 增加handler信号的时间,永远都会处理2号信号
    while(true)
    {
        cout << "." << endl;
        sigpending(&pending);
        for(int i = 1; i <= 31; ++i) 
        {
            if(sigismember(&pending, i))
            {
                cout << "1";
            }
            else
            {
                cout << "0";
            }
        }
        cout << endl;
        sleep(1);
    }
}

int main()
{
    struct sigaction act, oact;
    act.sa_handler = handler;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);

    sigaction(2, &act, &oact);

    while(true)
    {
        cout << "main running" << endl;
        sleep(1);
    }

    return 0;
}

这里虽然pending中有了,但是被屏蔽了,无法发送信号。

再把3号信号加入进去:

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

using namespace std;

void handler(int signo)
{
    cout << "获取到一个信号,信号的编号是: " << signo << endl;
    sigset_t pending;
    // 增加handler信号的时间,永远都会处理2号信号
    while(true)
    {
        cout << "." << endl;
        sigpending(&pending);
        for(int i = 1; i <= 31; ++i) 
        {
            if(sigismember(&pending, i))
            {
                cout << "1";
            }
            else
            {
                cout << "0";
            }
        }
        cout << endl;
        sleep(1);
    }
}

int main()
{
    struct sigaction act, oact;
    act.sa_handler = handler;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask, 3);
    sigaction(2, &act, &oact);

    while(true)
    {
        cout << "main running" << endl;
        sleep(1);
    }

    return 0;
}

可以看到ctrl+\也因为被屏蔽无法退出了。

#include <iostream>
#include <vector>
#include <unistd.h>
#include <signal.h>

using namespace std;


// 局限性,需要对所有的信号进行捕捉
std::vector<sighandler_t> handler;

void Handler2()
{
    cout << "hello 2" << endl;
}
void Handler3()
{
    cout << "hello 3" << endl;
}
void Handler4()
{
    cout << "hello 4" << endl;
}
void Handler5()
{
    cout << "hello 5" << endl;
}
void Handler(int signo)
{
    switch (signo)
    {
    case 2:
        Handler2();
        break;
    case 3:
        Handler3();
        break;
    case 4:
        Handler4();
        break;
    case 5:
        Handler5();
        break;
    default:
        break;
    }
}

int main()
{
    signal(2, Handler);
    signal(3, Handler);
    signal(4, Handler);
    signal(5, Handler);

    return 0;
}

这种写法可以让耦合性更低。

六.可重入函数

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

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

如果一个函数符合以下条件之一则是不可重入的:

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

 七.volatile

先看下面这段代码:

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

int flags = 0;

void handler(int signo)
{
    flags = 1;
    printf("更改flags: 0->1\n");
}

int main()
{
    signal(2, handler);

    while(!flags);
    printf("进程是正常退出的!\n");

    return 0;
}

这里的结果和我们的预期是相同的,因为发送了2号信号,就将flags由0置1了。

但是这是在gcc编译器中,如果在别个编译器中就不一定是这种效果了。

        某些编译器会把这个flags保存到CPU的寄存器中,去调用flags时也时去调用这个寄存器里的,而这里更改的是内存中的flags,不是寄存器中的,这时就与我们的预期不同。

这里我们更改一下编译器的编译层度:

这时就无法按我们预期进行了。 

volatile的作用就是保持内存的可见性:

        告诉编译器,不准对flags做任何优化

        每次CPU计算的时候,都必须从内存中拿数据。

这里加上了volatile:

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

// 保持内存的可见性
volatile int flags = 0;

void handler(int signo)
{
    flags = 1;
    printf("更改flags: 0->1\n");
}

int main()
{
    signal(2, handler);

    while(!flags);
    printf("进程是正常退出的!\n");

    return 0;
}

结果就又与预期相同了。

volatile和const也并不冲突,可以同时使用。

八.SIGCHLD

子进程退出的时候,不是默默的退出,会自动给父进程发送SIGCHLD

下面我们来验证一下:

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

using namespace std;

void handler(int signo)
{
    cout << "子进程退出了,我收到了信号: " << signo << "我是" << getpid() << endl;
}

int main()
{
    signal(SIGCHLD, handler);
    pid_t id = fork();
    if(id == 0)
    {
        while(true)
        {
            cout << "我是子进程: " << getpid() << endl;
            sleep(1);
        }
        exit(0);
    }

    // parent
    while(true)
    {
        cout << "我是父进程: " << getpid() << endl;
        sleep(1);
    } 

    return 0;
}

 

可以看到,子进程被杀掉之后,确实向父进程发送了SIGCHLD信号。

其实,不只是子进程退出的时候会发送SIGCHLD信号,暂停和唤醒都会发送: 

这里的 kill -19就是暂停,kill -18就是运行,都发送了SIGCHLD信号。

之前我们所写的父子进程的进程等待,父进程是要自己主动等待的:

#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>

using namespace std;


int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        // 子进程
        int cnt = 10;
        while(cnt)
        {
            cout << "我是子进程, pid: "<< getpid() << " 当前的cnt: " << cnt-- << endl;
            sleep(1);
        }
        cout << "子进程退出,进入僵尸状态" << endl;
        exit(0);
    }

    // 父进程,是要自己等待的
    if(waitpid(id, nullptr, 0) > 0)
    {
        cout << "父进程等待子进程成功" << endl;
    }

    return 0;
}

这里父进程会一直等待子进程退出。

那么我们怎么才能让父进程不只是在等待子进程,可以让父进程去做别的事呢:

#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <signal.h>
#include <cassert>

using namespace std;

void FreeChild(int signo)
{
    assert(signo == SIGCHLD);
    pid_t id = waitpid(-1, nullptr, 0);
    if(id > 0)
    {
        cout << "父进程等待成功, child pid: " << id << endl; 
    }
}

int main()
{
    signal(SIGCHLD, FreeChild);
    pid_t id = fork();
    if(id == 0)
    {
        // 子进程
        int cnt = 10;
        while(cnt)
        {
            cout << "我是子进程, pid: "<< getpid() << " 当前的cnt: " << cnt-- << endl;
            sleep(1);
        }
        cout << "子进程退出,进入僵尸状态" << endl;
        exit(0);
    }

    while(true)
    {
        cout << "我是父进程,我正在运行: " << getpid() << endl;
        sleep(1);
    }

    return 0;
}

        按照上面的写法,是可以做到父进程不只是在等待子进程的,但是这种写法是有bug的,如果多个进程同时运行,就容易出现:

#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <signal.h>
#include <cassert>

using namespace std;

void FreeChild(int signo)
{
    assert(signo == SIGCHLD);
    pid_t id = waitpid(-1, nullptr, 0);
    if(id > 0)
    {
        cout << "父进程等待成功, child pid: " << id << endl; 
    }
}

int main()
{
    signal(SIGCHLD, FreeChild);
    for(int i = 0; i < 10; ++i)
    {
        pid_t id = fork();
        if(id == 0)
        {
            // 子进程
            int cnt = 10;
            while(cnt)
            {
                cout << "我是子进程, pid: "<< getpid() << " 当前的cnt: " << cnt-- << endl;
                sleep(1);
            }
            cout << "子进程退出,进入僵尸状态" << endl;
            exit(0);
        }
    }

    while(true)
    {
        cout << "我是父进程,我正在运行: " << getpid() << endl;
        sleep(1);
    }

    return 0;
}

        这时,我们子进程正常是应该已经全部退出了的,但是看右边,会发现还有好几个子进程处于僵尸状态,因为在同时退出的时候,一下发送了很多个SIGCHLD信号,当正在处理某一个信号时,其它一些信号就可能会被丢掉,导致bug。

这里又会出现子进程没有都退出,但是父进程却不再运行的bug了。

        因为waitpid在等待子进程的时候,把前7个信号全部回收了,当它回收第8个进程时,第8个进程并没有退出,这时调用waitpid会被阻塞住。

这里我们可以给waitpid传入WNOHANG,然后再进行修改,即可处理:

#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <signal.h>
#include <cassert>

using namespace std;

void FreeChild(int signo)
{
    assert(signo == SIGCHLD);
    while(true)
    {
        // 如果没有子进程了,waitpid 才会调用失败
        // -1: 等待任意一个子进程
        pid_t id = waitpid(-1, nullptr, WNOHANG);
        if(id > 0)
        {
            cout << "父进程等待成功, child pid: " << id << endl; 
        }
        else if(id == 0)
        {
            // 还有子进程,但是现在没退出
            cout << "还有子进程,但是现在还没退出,父进程要去做自己的事情了" << endl;
            break;
        }
        else
        {
            cout << "父进程等待所有子进程结束" << endl;
            break;
        }
     }
}

int main()
{
    signal(SIGCHLD, FreeChild);
    for(int i = 0; i < 10; ++i)
    {
        pid_t id = fork();
        if(id == 0)
        {
            // 子进程
            int cnt = 0;

            if(i < 7)
                cnt = 5;
            else
                cnt = 20;
            while(cnt)
            {
                cout << "我是子进程, pid: "<< getpid() << " 当前的cnt: " << cnt-- << endl;
                sleep(1);
            }
            cout << "子进程退出,进入僵尸状态" << endl;
            exit(0);
        }
    }

    while(true)
    {
        cout << "我是父进程,我正在运行: " << getpid() << endl;
        sleep(1);
    }

    return 0;
}

        如果我们不想麻烦,不想获取状态,也不想自己去管,想让子进程退出时直接退出僵尸进程,就可以这么写:

 signal(SIGCHLD, SIG_IGN);

#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <signal.h>
#include <cassert>

using namespace std;

void FreeChild(int signo)
{
    assert(signo == SIGCHLD);
    while(true)
    {
        // 如果没有子进程了,waitpid 才会调用失败
        // -1: 等待任意一个子进程
        pid_t id = waitpid(-1, nullptr, WNOHANG);
        if(id > 0)
        {
            cout << "父进程等待成功, child pid: " << id << endl; 
        }
        else if(id == 0)
        {
            // 还有子进程,但是现在没退出
            cout << "还有子进程,但是现在还没退出,父进程要去做自己的事情了" << endl;
            break;
        }
        else
        {
            cout << "父进程等待所有子进程结束" << endl;
            break;
        }
     }
}

int main()
{
    signal(SIGCHLD, SIG_IGN);
    for(int i = 0; i < 10; ++i)
    {
        pid_t id = fork();
        if(id == 0)
        {
            // 子进程
            int cnt = 0;

            if(i < 7)
                cnt = 5;
            else
                cnt = 20;
            while(cnt)
            {
                cout << "我是子进程, pid: "<< getpid() << " 当前的cnt: " << cnt-- << endl;
                sleep(1);
            }
            cout << "子进程退出,进入僵尸状态" << endl;
            exit(0);
        }
    }

    while(true)
    {
        cout << "我是父进程,我正在运行: " << getpid() << endl;
        sleep(1);
    }

    return 0;
}

        既然子进程退出的时候,默认的信号处理就是忽略,那么我们调用signal/sigaction SIG_IGN的意思在哪呢?

        SIG_IGN手动设置还是与默认的不一样的,默认的可能还会给父进程发送信号,但是手动设置,让子进程退出时,就不会给父进程发送信号了,并且会自动释放。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冰果滴

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值