【Linux】信号

信号的原理

生活中存在信号的场景,例如信号弹、下课上课铃声、求偶、红绿灯、快递发短信取件码、旗语、狼烟、发令枪、军训哨子、闹钟等等。根据生活中的例子,可以明白信号是有人告知的,即便是没有信号产生,但是当信号产生之后,我们知道应该做些什么事情;其次,当信号产生时,我们并不会立即处理这个信号,而是在合适的时候去做这件事情,因为此时存在更重要的事情。

信号产生后------存在时间窗口------信号处理时------在这个时间窗口内必须记住信号的到来。

信号的原理:

  1. 进程必须识别并处理信号,并且信号没有产生,也要具备处理信号的能力------信号的处理能力,属于进程内置功能的一部分。
  2. 进程即便没有收到信号,也能够知道如何处理信号
  3. 当进程真的收到信号的时候,进程可以立即处理,也可能并不会立即处理这个信号,而是选择合适的时候处理信号。
  4. 一个进程必须当信号产生的时候,到信号开始被处理,就一定会有时间窗口,此时要求进程具有临时保存信号已经发生了的能力。
cpp 复制代码
int main()
{
    while(true)
    {
        cout << "I am a crazy process" << endl;
        sleep(1);
    }
    return 0;
}

问题:ctrl + c 为什么能够杀掉前台进程?

【解释】Linux中,一次登录中,一个终端,一般会配上一个bash,每一个登录,只允许一个进程是前台进程,可以允许多个进程是后台进程。

一般来讲,谁来获取键盘输入,谁就是前台进程。当执行上面的代码时,会将bash进程改变为后台进程,此时执行ls、pwd命令时,无法显现出来内容。当执行可执行程序的时候,在可执行程序后面加上&,可以将该程序在后台执行。

所以,ctrl + c是键盘输入,只有前台进程可以获取到;同时ctrl+c本质是被进程解释成为了2号信号。

指令:kill -l

【功能】查看信号列表

这其中,1~30号信号是普通信号,31~64是实时信号。


信号的处理方式:

  1. 默认动作
  2. 忽略
  3. 自定义动作------信号的捕捉

就比如,进程收到2号信号的默认动作是终止自己进程。
接口:设置自定义信号的处理方法,修改特定进程对信号的处理动作的。

参数signum:表示信号列表里面的参数。

参数handler:表示对信号进行自定义动作。

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <signal.h>

using namespace std;

// 参数表示收到哪一个信号
void myhandler(int signo)
{
    cout << "process get a signal: " << signo << endl; 
}

int main()
{
    signal(SIGINT, myhandler);
    while(true)
    {
        cout << "I am a crazy process" << endl;
        sleep(1);
    }
    return 0;
}

通过修改自定义信号的处理方式,将2号信号捕捉,修改成自定义信号(打印内容),现在只能通过kill -9 [pid]来杀掉进程。

【说明】有些信号可以自定义动作,有效信号不可以自定义动作。

【注意】signal只需要设置一次,后续的代码在运行的时候都会有效。如果在后续的代码中,产生信号才会执行自定义动作,如果没有信号产生,则后续就不会出现自定义动作。

++问题:键盘数据是如何输入给内核的,ctrl+c是如何变成信号的(硬件)++

【解释】键盘被输入,肯定是操作系统先得知的。键盘属于外设,键盘可以给CPU发送硬件中断,来让操作系统读取键盘的数据,每一个外设都会给CPU发送硬件中断,每一个硬件中的都有自己的中断号,硬件通过中断单元发送给CPU。软件中,开机时在操作系统中形成中断向量表,中断向量表存在方法的地址(直接访问外设的方法------主要是磁盘、显示器、键盘等),读取键盘的方法被保存在向量表中。一旦CPU中触发中断号,操作系统会识别CPU中的中断号,操作系统会以中断号为索引,来执行这个方法。信号就是通过软件方式对进程模拟的硬件中断。操作系统会通过分析从键盘获取到的是数据还是控制信号(例如ctrl+c),如果是控制信号,就会发送给进程。

总的来讲,就是用户按ctrl+c这个键盘输入产生一个硬件中断,被操作系统获取,解释成信号,发送给目标前台进程,前台进程因为收到信号,进而引起进程退出。

【总结】

  1. ctrl+c产生的信号只能发给前台进程。一个命令后面加&可以放到后台运行,这样shell不必等等进程结束就可以接受新的命令,启动新的进程。
  2. shell可以同时运行一个前台进程和多个后台进程,只有前台进程才能接到像ctrl+c这种控制键产生的信号。

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

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

信号的产生

键盘组合键

可以通过键盘组合键的方式(例如ctrl+c产生2号信号,ctrl+\产生3号信号,ctrl+z产生19号信号)

2号信号SIGINT的默认处理动作是终止进程,3号信号SIGQUIT的默认处理动作是终止进程并且Core Dump。

相较于2、3号信号,19号信号SIGSTOP并没有会被捕捉,所以并不是所有的信号都可以被捕捉的。

在0~31号进程中,只有9号信号和19号信号不能被捕捉,其他均可以被捕捉。

使用kill命令

指令:kill -[signal] [pid]

【功能】执行一个信号

使用系统调用接口

kill接口

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>

using namespace std;


void Usage(string proc)
{
    cout << "Usage:\n\t" << proc << " signum pid\n\n"; 
}

//  mykill signum pid
int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }

    int signum = stoi(argv[1]);
    pid_t pid = stoi(argv[2]);

    int n = kill(pid, signum);
    if(n == -1)
    {
        perror("kill");
        exit(2);
    }

    return 0;
}

raise系统调用

cpp 复制代码
int main()
{
    int cnt = 5;
    while(cnt)
    {
        cout << "process pid : " << getpid() << endl;
        if(cnt == 2)
        {
            raise(9);
        }
        cnt--;
        sleep(1);
    }

    return 0;
}

abort接口

abort也是一个封装后的函数,给自己的进程发送6号信号。abort函数除了执行6号信号,在函数内部也执行了退出进程的操作。

异常条件

信号产生无论是如何产生都是通过操作系统来发送给进程的,因为操作系统是进程的管理者。

cpp 复制代码
#include <signal.h>
#include <iostream>
#include <unistd.h>

using namespace std;

int main()
{
    cout << "div befer" << endl;
    sleep(1);
    int a = 10;
    a /= 0;
    cout << "div after" << endl;
    sleep(1);

    return 0;
}

执行上述代码的结果是:

进程收到8号信号。

没有修改8号信号的默认动作时,当出现异常,触发8号信号打印""并退出进程;当自定义8号信号的动作只是打印,进程并没有退出,而是一直出发8号信号并执行打印动作。

同样的例子还有:野指针问题,一旦发生段错误,进程就会一直触发11号信号,直到进程退出。

当进程出现异常问题时,进程并不会直接退出,而是触发某一个信号,然后执行该信号的默认动作。

【问题】为什么除零或者野指针会给进程发送信号呢?

解释:当出现除零或者野指针问题,操作系统会给进程发送对应的信号。CPU中存在一种状态寄存器,状态寄存器中有一个溢出标志位,当出现除零错误时,状态寄存器中的溢出标志位由0变成1出异常(虽然修改的时CPU内部的状态寄存器,但是只会影响自己的进程)。CPU在调度该进程时出异常,操作系统得知异常后会向该进程发送信号。

CPU是硬件,操作系统是硬件的管理者!

当出现异常的信号被捕捉后,进程不退出,当前进程会一直被调度,而CPU中的寄存器一直处于异常状态,操作系统又会触发信号。

软件条件

异常并不是只是由硬件产生的,管道中读端关闭写端一直在写,此时操作系统会发送13号信号SIGPIPE,这种情况是软件异常。

#include<unistd.h>

unsigned int alarm(unsigned int seconds);

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

cpp 复制代码
    int n = alarm(5);
    while(1)
    {
        cout  << "proc is running..." << endl;
        sleep(1);
    }

alarm的特点:设置一次闹钟只会响一次闹钟

cpp 复制代码
void handler(int signo)
{
    cout << "get a signo: " << signo << endl;
    alarm(5);
}

// 设置一次闹钟后再次设置一次闹钟
int main()
{
    signal(SIGALRM, handler);
    int n = alarm(5);
    while(1)
    {
        cout  << "proc is running..." << endl;
        sleep(1);
    }
    return 0;
}
cpp 复制代码
#include <signal.h>
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

using namespace std;

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        // child
        int cnt = 500;
        while(cnt--)
        {
            cout << "I am a child process, pid: " << getpid() << " cnt: "  << cnt << endl;
            sleep(1);
        }
        exit(0);
    }

    // father
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    if(rid == id)
    {
        cout << "child quit info, rid: " << rid 
        << " exit code: " << ((status >> 8) & 0xFF) 
        << " exit signal: " << (status & 0x7F) 
        << " core dump: " << ((status >> 7) & 1) << endl;
    }

    return 0;
}

当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这就叫做Core Dump。

指令:ulimit -c 1024

【功能】打开core dump限制,允许产生core文件。

打开系统的core dump功能,一旦进程出现异常,操作系统会将进程在内存中的运行信息,给dump(转储)到进程的当前目录(磁盘)形成core pid文件,这种功能称为核心转储。进程异常终止通常是因为有Bug,就比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清楚错误原因,这叫做Post-mortem Debug(事后调试)。

信号的保存

对于普通信号而言,信号是发送给进程的PCB结构体中使用位图操作管理信号的。

  • 位图中比特位的内容是0还是1,表明是否收到信号
  • 比特位的位置(第几个),表示信号的编号
  • 所谓的"发送信号",本质就是操作系统修改目标进程task_struct信号位图对应的比特位置。

因为操作系统是进程的管理者,只有它有资格去修改task_struct内部的属性。

进程在收到信号之后,可能不会立即处理这个信号。而信号不会被立即处理,就需要有一个时间窗口。

信号的相关概念

  • 实际执行信号的处理动作称为信号递达。
  • 信号从产生到递达之间的状态称之为信号未决。
  • 进程可以选择阻塞某一个信号。

信号的范围是[1, 31],每一种信号都要有自己的一种处理方法。这种处理方法在底层都是函数指针数组。

进程可以通过指针查找三张表,pening表可以查看是否收到信号(位图结构),handler表里保存信号指导的方法的函数指针,block表同样是利用位图的思想来选择是否选择阻塞一个信号。通过三张表可以对信号进行记录保存相关的工作。

  • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。

信号被阻塞,但是信号依旧会被发送,直到进程信号解除阻塞,才可以执行递达之后的动作。

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

信号在内存的表示

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

  1. 在上图中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
  2. SIGINT信号产生过,但正在被阻塞,所有暂时不能递达。虽然其处理动作是忽略,但在没有接触阻塞之前不能忽略这个信号,因为进程仍然有机会改变处理动作之后再解除阻塞。
  3. SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。

问题:在Linux中如果再进程接触对某个信号的阻塞之前这种信号产生过多次,将会如何处理?

【答】在Linux中常规信号在递达之前产生多次,只计一次,而实时信号在递达之前产生多次可以依次放在放在一个队列中。

sigset_t

sigset_t称为信号集,这个类型可以表示每个信号的"有效"或者"无效"状态,在阻塞信号集中"有效"和"无效"的含义是该信号是否被阻塞,而在未决信号集中"有效"和"无效"的含义是该信号是否处于未决状态。

信号集操作函数

sigset_t类型对于每种信号用一个bit位来表示"有效"或者"无效"状态,通过调用以下函数来操作sigset_t变量。

  • 头文件#include<signal.h>
  • int sigemptyset(sigset_t *set);

函数sigempty初始化set所指向的信号集,使其中所有信号的对应bit位置清零,表示该信号集不包含任何有效信号。

  • int sigfillset(sigset *set);

函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit位置置1,表示该信号集的有效信号包括系统支持的所有信号。

  • int sigaddset(sigset *set, int signo);

在信号集中添加一个信号

  • int sigdelset(sigset_t *set, int signo);

在特定的信号集中删除一个信号

  • int sigismember(const sigset_t *set, int signo);

一个信号是否存在于信号集中。

【注意】在使用sigset_t类型的变量之前,一定要调用sigemptyset或者sigfillset进行初始化,使得信号集处于确定的状态。

sigprocmask接口

通过调用函数sigprocmask可以读取或者更改进程的信号屏蔽字(阻塞信号集),也就是读取或者修改block信号集。

  • #include<signal.h>
  • int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
  • 返回值:若成功则为0,若出错则为-1。
  • how参数有三种可选参数,这三个选项只能三选一。
  • set参数是一个输入型参数,用于修改进程block表中的字段。
  • oset参数是一个输出型参数,用于保存上一次的block表的字段,以便恢复。

sigpending函数接口

  • #include<signal.h>
  • int sigpending(sigset_t *set);
  • 读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1.
cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <signal.h>

using namespace std;

void PrintPending(sigset_t &pending)
{
    for(int signo = 31; signo >= 1; signo--)
    {
        if(sigismember(&pending, signo))
        {
            cout << "1";
        }
        else
        {
            cout << "0";
        }
    }
    cout << endl;
}

void handler(int signo)
{
    cout << "catch a signo: " << signo << endl;
}

int main()
{
    // 0. 对2号信号进行自定义捕捉
    signal(2, handler);

    // 1. 先对2号信号进行屏蔽------数据预备
    sigset_t bset; // 在用户栈上的,属于用户区
    sigemptyset(&bset);
    sigaddset(&bset, 2); // 仅仅只是修改bset变量的值,并没有设置到进程的task_struct中

    // 1.2 调用系统调用
    sigset_t oset; // 用于保存上一次信号集
    sigemptyset(&oset);
    sigprocmask(SIG_SETMASK, &bset, &oset);

    // 2. 重复打印当前进程的pending 00000000 00000000 00000000 00000000
    sigset_t pending;
    int cnt = 0;
    while(1)
    {
        // 2.1 获取
        int n = sigpending(&pending);
        if(n < 0) continue;
        // 2.2 打印
        PrintPending(pending);
        sleep(1);
        cnt++;

        // 2.3 解除阻塞
        if(cnt == 20) 
        {
            cout << "unblock 2 signo" << endl;
            sigprocmask(SIG_SETMASK, &oset, nullptr);
        }
    }
    // 3. 发送2号信号 00000000 00000000 00000000 00000010

    return 0;
}

【注意】信号不能都进行屏蔽,就比如9、19号进程不可以被屏蔽

cpp 复制代码
void PrintPending(sigset_t &pending)
{
    for(int signo = 31; signo >= 1; signo--)
    {
        if(sigismember(&pending, signo))
        {
            cout << "1";
        }
        else
        {
            cout << "0";
        }
    }
    cout << endl;
}

void handler(int signo)
{
    cout << "catch a signo: " << signo << endl;
}

int main()
{
    // 测试
    sigset_t bset, oset;
    sigemptyset(&bset);
    sigemptyset(&oset);

    for(int i = 0; i <= 31; ++i)
    {
        sigaddset(&bset, i);
    }
    sigprocmask(SIG_SETMASK, &bset, &oset);

    sigset_t pending;
    while(true)
    {
        int n = sigpending(&pending);
        if(n < 0) continue;
        PrintPending(pending);
        sleep(1);
    }

    return 0;
}

信号的捕捉

当进程从内核态时返回到用户态的时候,进行信号的检测与处理。

当执行系统调用的时候,操作系统会将用户身份转换成内核身份,在内核态中处理对应的函数,然后再返回到用户态进而实行后续代码------也就是说操作系统是自动去做身份切换的"从用户到内核"。

谈一谈进程地址空间

问题:当系统中存在多个进程的时候,用户页表有几份、内核页表有几份?

【答】:用几个进程就存在几份用户级页表,因为进程具有独立性;内核级页表只有一份,每一个进程看到的内核空间都是一样的,也就意味着整个系统中,进程不管如何切换,内核空间都是不变的。在进程视角查看时,当调用系统调用接口时,就是在自己的进程地址空间中进行执行的。站在操作系统角度查看时,任何一个时刻都会有进程在执行,想要执行操作系统的代码,就可以随时访问。

操作系统的本质,是基于时钟中断的一个死循环。在计算机硬件中,有一个时钟芯片,会每隔很短的时间,都会向计算机发送时钟中断。

内核态:允许访问操作系统的代码和数据。

用户态:只能访问用户自己的代码,不能访问操作系统的代码和数据。

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。

【例子】当用户程序设置SIGQUIT信号的处理函数sighandler。当前正在执行main函数,这时发生中断或者异常切换到内核态,在中断处理完毕后要返回用户态的main函数之前会检查是否有信号递达,发现SIGQUIT信号递达后,内核决定返回用户态后不是恢复main函数的上下文执行,而是执行sighandler函数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。

信号捕捉的方法

信号捕捉除了signal函数,还有下面的函数接口:

sigaction

  • act是输入型参数,oldact是输出型参数,这两个参数都是结构体。

该函数既可以捕捉普通信号也可以捕捉实时信号,对于需要捕捉普通信号而言,只需要了解两个内容:

  • void (*sa_handler)(int);------捕捉信号需要的处理方法。
  • sigset_t sa_mask;

简单使用sigaction函数:

cpp 复制代码
#include <cstring>
#include <signal.h>
#include <unistd.h>

using namespace std;

void handler(int signo)
{
    cout << "catch a signal, signal number : " << signo << endl;
}

int main()
{
    struct sigaction act, oact;
    memset(&act, 0, sizeof(act));
    memset(&oact, 0, sizeof(oact));

    act.sa_handler = handler;
    sigaction(2, &act, &oact);

    while(true)
    {
        cout << "I am a process: " << getpid() << endl;
        sleep(1);
    }

    return 0;
}

问题:pending位图是什么时候从1被修改成0的?

cpp 复制代码
#include <iostream>
#include <cstring>
#include <signal.h>
#include <unistd.h>

using namespace std;

void PrintPending()
{
    sigset_t set;
    sigpending(&set);

    for(int signo = 1; signo <= 31; ++signo)
    {
        if(sigismember(&set, signo))
        {
            cout << "1";
        }
        else
        {
            cout << "0";
        }
    }
    cout << "\n";
}

void handler(int signo)
{
    PrintPending(); // 在这里打印pending表,来测试pending位图是处理handler信号之前修改还是处理之后修改的。
    cout << "catch a signal, signal number : " << signo << endl;
}

int main()
{
    struct sigaction act, oact;
    memset(&act, 0, sizeof(act));
    memset(&oact, 0, sizeof(oact));

    act.sa_handler = handler;
    sigaction(2, &act, &oact);

    while(true)
    {
        cout << "I am a process: " << getpid() << endl;
        sleep(1);
    }

    return 0;
}

通过实验时发现,操作系统是先将pending位图由1变成0,再去调用handler方法,也就是在执行捕捉方法之前,先清0,再调用。

问题:当处理2号信号,可以再次触发2号信号吗?

cpp 复制代码
#include <iostream>
#include <cstring>
#include <signal.h>
#include <unistd.h>

using namespace std;

void PrintPending()
{
    sigset_t set;
    sigpending(&set);

    for (int signo = 1; signo <= 31; ++signo)
    {
        if (sigismember(&set, signo))
        {
            cout << "1";
        }
        else
        {
            cout << "0";
        }
    }
    cout << "\n";
}

void handler(int signo)
{
    cout << "catch a signal, signal number : " << signo << endl;
    while (true) // 使用死循环,测试当执行2号信号时,再次发送2号信号的情况
    {
        PrintPending(); // 在这里打印pending表,来测试pending位图是处理handler信号之前修改还是处理之后修改的。
        sleep(1);
    }
}

int main()
{
    struct sigaction act, oact;
    memset(&act, 0, sizeof(act));
    memset(&oact, 0, sizeof(oact));

    act.sa_handler = handler;
    sigaction(2, &act, &oact);

    while (true)
    {
        cout << "I am a process: " << getpid() << endl;
        sleep(1);
    }

    return 0;
}

不可以!当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字(也就是block表),当信号处理函数返回时自动恢复原来的信号屏蔽字。这样也就保证了在处理某个信号的时候,如果这种信号再次产生,那么它会被阻塞当前处理结束为止。

关于sigaction结构体中的sigset_t sa_mask变量:

  • sa_mask的类型是sigset_t。
  • 当处理2号信号时,2号信号会自动屏蔽2号信号,但是如果还想屏蔽更多信号,
cpp 复制代码
#include <iostream>
#include <cstring>
#include <signal.h>
#include <unistd.h>

using namespace std;

void PrintPending()
{
    sigset_t set;
    sigpending(&set);

    for (int signo = 1; signo <= 31; ++signo)
    {
        if (sigismember(&set, signo))
        {
            cout << "1";
        }
        else
        {
            cout << "0";
        }
    }
    cout << "\n";
}

void handler(int signo)
{
    cout << "catch a signal, signal number : " << signo << endl;
    while (true) // 使用死循环,测试当执行2号信号时,再次发送2号信号的情况
    {
        PrintPending(); // 在这里打印pending表,来测试pending位图是处理handler信号之前修改还是处理之后修改的。
        sleep(1);
    }
}

int main()
{
    struct sigaction act, oact;
    memset(&act, 0, sizeof(act));
    memset(&oact, 0, sizeof(oact));

    act.sa_handler = handler;
    // 屏蔽其他(1,3,4)信号
    sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask, 1);
    sigaddset(&act.sa_mask, 3);
    sigaddset(&act.sa_mask, 4);

    sigaction(2, &act, &oact);

    while (true)
    {
        cout << "I am a process: " << getpid() << endl;
        sleep(1);
    }

    return 0;
}

sa_mask会处理当处理一个信号时屏蔽其他信号。

可重入函数

main函数调用inset函数像一个链表head中插入节点node1,插入操作分为两步,当刚结束完第一步的时候,因为硬件中断使进程切换到内核,再次回到用户态之前检查到有信号待处理,于是切换到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的两步都完成后从sighandler返回到内核态,再次回到用户态继续执行main函数的insert函数的后续代码,现在执行第二步,此时main函数和sighandler先后向链表中插入两个节点,最后只有一个节点被真正插入,而另外一个节点丢失,造成了内存泄漏。

【现象】insert函数被main和handler执行流重复进入

【问题】节点丢失,内存泄漏

  • 如果一个函数被重复进入的情况下,出错或者可能出错称之为不可重入函数,否则叫做可重入函数。

目前的大部分函数都是不可重入函数

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

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

volatile

cpp 复制代码
#include <iostream>
#include <cstring>
#include <signal.h>
#include <unistd.h>

using namespace std;

int flag = 0;

void handler(int signo)
{
    cout << "catch a signal: " << signo << endl;
    flag = 1;
}

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

    while(!flag); // 当flag=0,!flag为真,当flag=1, !flag为假。

    cout << "Process quit normal" << endl;

    return 0;
}

执行该程序时,当发送2号信号时,循环结束,程序退出。

但是优化条件下,flag变量可能被直接优化到CPU内的寄存器中,在编译期间可以携带-O0选项(没有优化),-O1选项(优化级别1),-O3选项(优化级别3).

不同的优化级别会产生不同的效果,根据优化导致CPU会直接读取寄存器中的内容,而不会选择读取内存中的内容。

  • 例如register是一个建议型关键字,意思是能把变量放在CPU中就把变量放在寄存器中。

volatine关键字可以防止变量过度优化,也就是一直存放在内存中,而不会选择放在CPU中的寄存器中。与register相当于是相反的两个方向。

volatile int flag = 0;防止编译器过度优化,保持内存的可见性。

SIGHLD信号

子进程退出时,不是悄悄退出,而是在退出的时候是会主动的向父进程发送SIGCHLD(17)号信号。

cpp 复制代码
#include <iostream>
#include <cstring>
#include <signal.h>
#include <unistd.h>

using namespace std;

void handler(int signo)
{
    cout << "I am process: " << getpid() << " catch a signo: " << signo << endl;
}

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

    pid_t id = fork();
    if(id == 0)
    {
        while(true)
        {
            cout << "I am child process: " << getpid() << ", ppid: " << getppid() << endl;
            sleep(1);
        }
        exit(0);
    }

    // father
    while(true)
    {
        cout << "I am father process: " << getpid() << endl;
        sleep(1);
    }
    return 0;
}

子进程在进行等待的时候,我们可以采用基于信号的方法等待。

进程等待的好处:

  1. 获取子进程的退出状态
  2. 释放子进程的僵尸状态
  3. 虽然不知道父子谁先运行,但是一定是父进程最后退出。

子进程退出时,还是需要调用wait/waitpid这样的接口,也就是说父进程必须保证自己是一定在运行的。

将子进程等待写入到信号捕捉函数中:

cpp 复制代码
#include <iostream>
#include <cstring>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>

using namespace std;

void handler(int signo)
{
    pid_t rid = waitpid(-1, nullptr, 0);
    cout << "I am process: " << getpid() << " catch a signo: " << signo << " child process quit: " << rid << endl;
}

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

    pid_t id = fork();
    if(id == 0)
    {
        while(true)
        {
            cout << "I am child process: " << getpid() << ", ppid: " << getppid() << endl;
            sleep(5);
            break;
        }
        exit(0);
    }

    // father
    while(true)
    {
        cout << "I am father process: " << getpid() << endl;
        sleep(1);
    }
    return 0;
}

问题:如果存在10个进程,需要同时退出或者退出一半进程?

同时退出10个进程的代码:

cpp 复制代码
#include <iostream>
#include <cstring>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <ctime>

using namespace std;

void handler(int signo)
{
    pid_t rid;
    while ((rid = waitpid(-1, nullptr, WNOHANG)) > 0)
    {
        cout << "I am process: " << getpid() << " catch a signo: " << signo << " child process quit: " << rid << endl;
    }
}

int main()
{
    signal(17, handler);
    for (int i = 0; i < 10; ++i)
    {
        pid_t id = fork();
        if (id == 0)
        {
            while (true)
            {
                cout << "I am child process: " << getpid() << ", ppid: " << getppid() << endl;
                sleep(5);
                break;
            }
            exit(0);
        }
    }

    // father
    while (true)
    {
        cout << "I am father process: " << getpid() << endl;
        sleep(1);
    }
    return 0;
}

退出部分进程的代码(使用不同时间):

cpp 复制代码
#include <iostream>
#include <cstring>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <ctime>

using namespace std;

void handler(int signo)
{
    pid_t rid;
    while ((rid = waitpid(-1, nullptr, WNOHANG)) > 0)
    {
        cout << "I am process: " << getpid() << " catch a signo: " << signo << " child process quit: " << rid << endl;
    }
}

int main()
{
    srand(time(nullptr));
    signal(17, handler);
    for (int i = 0; i < 10; ++i)
    {
        pid_t id = fork();
        if (id == 0)
        {
            while (true)
            {
                cout << "I am child process: " << getpid() << ", ppid: " << getppid() << endl;
                sleep(5);
                break;
            }
            exit(0);
        }
        sleep(rand() % 5 + 3);
    }

    // father
    while (true)
    {
        cout << "I am father process: " << getpid() << endl;
        sleep(1);
    }
    return 0;
}

事实上,由于UNIX的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调用sigaction将SIGHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。此方法对于Linux可以使用,但是不保证在其它类UNIX系统上都可用。

cpp 复制代码
#include <iostream>
#include <cstring>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <ctime>

using namespace std;

int main()
{
    signal(17, SIG_IGN);
    for (int i = 0; i < 10; ++i)
    {
        pid_t id = fork();
        if (id == 0)
        {
            while (true)
            {
                cout << "I am child process: " << getpid() << ", ppid: " << getppid() << endl;
                sleep(5);
                break;
            }
            exit(0);
        }
        sleep(1);
    }

    // father
    while (true)
    {
        cout << "I am father process: " << getpid() << endl;
        sleep(1);
    }
    return 0;
}
相关推荐
小安啃代码2 小时前
在ubuntu中使用wps无法使用宋体
linux·ubuntu·wps
Jia ming2 小时前
大小端模式:字节顺序的奥秘
linux·运维·服务器
Zach_yuan2 小时前
Linux 线程入门到理解:从 pthread 使用到线程库底层原理
linux·运维·服务器
不会kao代码的小王2 小时前
深信服超融合 HCI 核心技术解析:aSV、aSAN 与 aNET 的协同架构
运维·服务器·网络·数据库·github
YuTaoShao2 小时前
【LeetCode 每日一题】1895. 最大的幻方——(解法二)前缀和优化
linux·算法·leetcode
a程序小傲2 小时前
中国邮政Java面试被问:边缘计算的数据同步和计算卸载
java·服务器·开发语言·算法·面试·职场和发展·边缘计算
翼龙云_cloud2 小时前
亚马逊云渠道商:如何在AWS控制台中创建每月成本预算?
服务器·云计算·aws
小尧嵌入式2 小时前
【Linux开发二】数字反转|除数累加|差分数组|vector插入和访问|小数四舍五入及向上取整|矩阵逆置|基础文件IO|深入文件IO
linux·服务器·开发语言·c++·线性代数·算法·矩阵
试试勇气2 小时前
Linux学习笔记(十二)--用户缓冲区
linux·笔记·学习