Linux信号

目录

  • 认识信号
  • 信号的产生
    • 通过终端按键产生信号
    • 调用系统函数向进程发信号
      • kill
      • raise
      • abort
    • 由软件条件产生信号
      • alarm
    • 硬件异常产生信号
  • 核心转储
  • 信号的保存
    • 在内核中的表示
    • sigset_t
    • 信号集操作函数
    • sigprocmask
    • sigpending
  • 捕捉信号
    • 用户态和内核态
    • 信号捕捉的原理
    • sigaction
  • 可重入函数
  • volatile
  • SIGCHLD信号

认识信号

在生活中什么叫做信号,比如红绿灯:红灯亮的时候,会有匹配的动作----你为什么会有这个动作?曾经有人有事“培养”过你。信号没有产生,我们也知道该怎么处理它,正常运行。进程就是我,信号就是就是一个数字,进程在没有收到信号的时候,进程会正常运行。如果收到信号进程会对收到的信号,做与信号匹配的行为。程序员在设计进程的时候,早就已经设计了对信号的识别能力,所以信号才会做与信号匹配的行为。因为信号可能随时产生,所以在信号产生前,我可能正在做优先级更高的事情,我可能不能立马处理这个信号。我们要在后续合适的时进行处理。信号产生<------ 时间窗口-------->信号处理,所以进程在收到信号的时候,如果在时间窗口没有立马处理这个信号,需要进程将信号保存起来,所以进程具有记录信号的能力。
信号的产生,对于进程来说是异步的。进程是如何记录对应产生的信号?记录在哪里?因为1 ~ 31是普通信号,可以用一个int类型的位图来表示,比特位的位置代表的是信号的编号,比特位的0,1代表是否收到信号。信号记录在PCB(task_struct)的结构体中的uint32_t signals;所谓的发送信号,本质其实是写入信号,直接修改特定进程的信号位图中的特定的比特位,0->1。task struct 数据内核结构,只能由OS进行修改,无论后面我们有多少种信号产生的方式,最终都必须让0S来完成最后的发送过程。信号产生之后,不是立即处理的。是在合适的时候进行处理的。

kill -l   //查看系统定义的信号列表

Linux信号_第1张图片

  • 每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定 义 #define SIGINT 2
  • 编号34以上的是实时信号,本章只讨论编号34以下的信号,不讨论实时信号。这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明: man 7 signal

信号处理常见方式

  1. 忽略此信号。
  2. 执行该信号的默认处理动作。
  3. 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(Catch)一个信号。

信号的产生

键盘,系统调用,指令,软件条件,硬件条件

通过终端按键产生信号

用户输入命令,在Shell下启动一个前台进程。

  • 用户按下Ctrl-C(对应的是2号信号) ,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程
  • 前台进程因为收到信号,进而引起进程退出
#include 
#include 

int main()
{
    while(true)
    {
        std::cout<<"我是一个进程,我正在运行....."<<std::endl;
        sleep(1);
    }
    return 0;
}

Linux信号_第2张图片

Linux信号_第3张图片

#include 
#include 
#include 
#include 
void handler(int signo)
{
    std::cout <<"捕捉的信号是:"<< signo << std::endl;
}

int main()
{
    signal(2,handler);//捕捉2信号
    while(true)
    {
        std::cout<<"我是一个进程,我的进程PID是:"<< getpid() <<std::endl;
        sleep(1);
    }
    return 0;
}

Linux信号_第4张图片

  1. 2号信号,进程的默认处理动作是终止进程
  2. signal 可以进行对指定的信号设定自定义处理动作
  3. signal(2, handler)调用完这个函数的时候,handler方法被调用了吗?没有! 做了什么?只是更改了2号信号的处理动作,并没有handlder方法
  4. 那么handler方法,什么时候被调用?当2号信号产生的时候!
  5. 默认我们对2号信号的处理动作:终止进程。我们用signal(2, handler), 我们在执行用户动作的自定义捕捉!
#include 
#include 
#include 
#include 
void handler(int signo)
{
    std::cout <<"捕捉的信号是:"<< signo << std::endl;
}

int main()
{
    for(int i = 1; i <= 31; ++i)
    {
        signal(i,handler);//捕捉所有信号,让进程变的杀不死,能实现吗?
    }
    while(true)
    {
        std::cout<<"我是一个进程,我的进程PID是:"<< getpid() <<std::endl;
        sleep(5);
    }
    
    return 0;
}

Linux信号_第5张图片

  1. Ctrl-C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。
  2. Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl-C 这种控制键产生的信号。
  3. 前台进程在运行过程中用户随时可能按下 Ctrl-C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的。

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

kill

Linux信号_第6张图片
mykill.cc

#include 
#include 
#include 
#include 
#include 
#include 

void Usagc(char* proc)
{
    std::cout<< "Usagc:: \n\t";
    std::cout<< proc << " 信号编号 进程PID" << std::endl;
}
// ./mykill 9 PID
int main(int argc, char* argv[])
{
    if(3 != argc)
    {
        Usagc(argv[0]);
        exit(1);
    }
    int signo = atoi(argv[1]);
    int target_id = atoi(argv[2]);
    int n = kill(target_id,signo);
    if(-1 == n)
    {
        std::cerr << errno << ":" << strerror(errno) << std::endl;
        exit(2);
    }
    return 0;
}

loop.cc

#include 
#include 
#include 
#include 

int main()
{
    while(true)
    {
        std::cout<<"我是一个进程,我的进程PID是:"<< getpid() <<std::endl;
        sleep(5);
    }
    
    return 0;
}

Linux信号_第7张图片

raise

Linux信号_第8张图片

#include 
#include 
#include 
#include 
void handler(int signo)
{
    std::cout <<"捕捉的信号是:"<< signo << std::endl;
}

int main()
{
    signal(SIGINT,handler);
    while(true)
    {
        sleep(2);
        raise(2);
    }
    return 0;
}

Linux信号_第9张图片

abort

Linux信号_第10张图片

#include 
#include 
#include 
#include 
void handler(int signo)
{
    std::cout <<"捕捉的信号是:"<< signo << std::endl;
}

int main()
{
    signal(6,handler);
    while(true)
    {
        std::cout << "begin" << std::endl;
        abort();
        std::cout << "end" << std::endl;
    }
    
    return 0;
}

Linux信号_第11张图片
注意:abort函数执行了自定义捕捉后,会退出进程。

由软件条件产生信号

SIGPIPE(14)是一种由软件条件产生的信号,当具有“血缘关系”的两个进程进行在使用管道通信的时候,读端进程将读端关闭,那写端进程就会收到SIGPIPE信号,写端进程会被操作系统关闭。

alarm

Linux信号_第12张图片

查看云服务器1秒钟的算力是多少。(这个算力不准确)

#include 
#include 
#include 
#include 
int count = 0;
int n = 0;
void handler(int signo)
{
    std::cout <<"捕捉的信号是:"<< signo << std::endl;
    std::cout << "count: " << count << std::endl;
}

int main()
{
    signal(SIGALRM,handler);
    alarm(1);//一次性的
    while(true)
        ++count;
    return 0;
}

Linux信号_第13张图片

#include 
#include 
#include 
#include 
//int count = 0;

void handler(int signo)
{
    std::cout <<"捕捉的信号是:"<< signo << std::endl;
    int n = alarm(10);
    std::cout << "return: " << n << std::endl;
    //alarm(0)  表示将闹钟取消
}

int main()
{
    std::cout<< "PID:"<< getpid()<<std::endl;
    signal(SIGALRM,handler);
    alarm(10);//一次性的
    while(true)
        sleep(1);
    return 0;
}

Linux信号_第14张图片
为什么会有怎末多信号呢?
因为你收到了不同的信号,未来会表明你是不同的出错的原因。我们就可以根据信号的调整来判别出我们当前进程是因为什么原因退出。

硬件异常产生信号

硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。

#include 
#include 
#include 
#include 

void handler(int signo)
{
    std::cout<<"捕捉到的信号是:" << signo << "导致进程崩溃的" << std::endl;
    sleep(1);
    //一般走到这里要退出当前进程
}
int main()
{
    signal(8, handler);
    int a = 10;
    a /= 0;//除0的本质,就是触发硬件异常(CPU)
    std::cout << "-------" << std::endl;
    return 0;
}

Linux信号_第15张图片
为什么一直都在打信息?
CPU当中一旦除0,CPU的寄存器中的溢出标记位会被置1,被置1后,因为你的进程没有退出,而这个状态标记位也属于你当前进程的上下文,所以你但前进程出了异常了,操作系统就识别到这个异常给当前进程发送信号,可是你进程并没有去修复状态标记位,我们现在也没办法修饰。这个进程也没退,那么其中对应的溢出标记位就会一直都在,所以操作系统就一直向着个进程发送信号。
为什么当前进程一直不退?
因为以前我们收到的8号信号的默认动作是退出,我今天还是默认动作吗?今天我们叫做自定义捕捉动作,所以我的进程它的处理动作不再是不再是你所想的那样直接退出进程,而是我打完这句话我就继续向后运行,操作系统识别到你有硬件异常,不让你往后跑,然后就继续给你8号信号。
操作系统是软硬件的管理者。

核心转储

Linux系统级别提供了一种能力,可以将一-个进程在异常的时候,OS可以将该进程在异常的时候,核心代码部分进行核心转储,将内存中进程的相关数据,全部dump到磁盘中,一般会在当前进程的运行目录下,形成core. pid这样的二进制文件。

ulimit -a  //查看当前系统中特定资源的上限

Linux信号_第16张图片

#include 
#include 
#include 
#include 

int main()
{
    while(true)
    {
        std::cout<< "当前进程PID:"<< getpid()<<std::endl;
        sleep(5);
    }
    return 0;
}

Linux信号_第17张图片

man 7 signal 指令可以看到下面内容

Linux信号_第18张图片
核心转储有什么作用?
方便异常后,进行调试

#include 
#include 
#include 
#include 

int main()
{
    std::cout<< "野指针问题" << std::endl;
    int *p = nullptr;
    *p = 100;
    std::cout<< "除0错误" << std::endl;
    int a = 10;
    a /= 0;
    return 0;
}

Linux信号_第19张图片

信号的保存

信号其他相关常见概念

  • 实际执行信号的处理动作称为信号递达(Delivery)。
  • 信号从产生到递达之间的状态,称为信号未决(Pending)。
  • 进程可以选择阻塞 (Block )某个信号,被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。

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

在内核中的表示

信号在内核中的表示示意图:要横着看
Linux信号_第20张图片

pending表: 位图结构。比特位的位置,表示哪-一个信号。比特位的内容,代表是否收到该信号0表示没收到,1表示收到了。 通过着一串代码来uint32_ t pending = 0; pending |= (1<<(signo(几号信号)-1))添加几号信号。
block表: 位图结构。比特位的位置, 表示哪一个信号, 比特位的内容,代表是否对应的信号该被阻塞,0表示没阻塞,1表示阻塞了。
handler表: 函数指针数组指针的类型void (*sighandler_t)(int);该数组的下标,表示信号编号,数组的特定下标的内容,表示该信号的递达动作。

#include 
#include 
#include 
#include 

void handler(int signo)
{
    std::cout<<"捕捉到的信号是:" << signo << std::endl;
    sleep(1);
}
int main()
{
    signal(2,SIG_DFL);//默认处理
    signal(2,SIG_IGN);//忽略处理
    signal(2,handler);//自定义捕捉
    return 0;
}
  • 每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
  • SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
  • SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。 如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次。

sigset_t

typedef __sigset_t sigset_t;
# define _SIGSET_NWORDS	(1024 / (8 * sizeof (unsigned long int)))
typedef struct
  {
    unsigned long int __val[_SIGSET_NWORDS];
  } __sigset_t;

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

信号集操作函数

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

#include 
int sigemptyset(sigset. _t *set); 
int sigillset(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清零,表示该信号集不包含任何有效信号。成功返回0,失败返回-1。
  • 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系统支持的所有信号。成功返回0,失败返回-1。
  • 函数sigaddset在set所指向的信号集,添加signo有效信号,成功返回0,失败返回-1。
  • 函数sigdelset在set所指向的信号集,删除signo有效信号,成功返回0,失败返回-1。
  • 函数sigismember判断在set所指向的信号集,是否存在signo有效信号,存在返回1,不存在返回0,调用失败返回-1.

sigprocmask

调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。
Linux信号_第21张图片

  • 如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。
  • 如果set是非空指针,则 更改进程的信号屏蔽字,参数how指示如何更改。
  • 如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。

假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。

选项(how) 含义
SIG_BLOCK set包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask I set
SIG_UNBLOCK set包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask=mask & ~set
SIG_SETMASK 设置当前信号屏蔽字为set所指向的值,相当于mask=set

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

sigpending

#include 
int sigpending(sigset_t *set);

读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。

#include 
#include 
#include 

void handler(int signo)
{
    std::cout<<"捕捉到的信号是:" << signo << std::endl;
}

void printfPending(sigset_t *oset)
{
    std::cout << "当前进程的pending位图: ";
    int signo = 1;
    for(; signo <=31; signo++)
    {
        if(sigismember(oset, signo)) std::cout << "1";
        else std::cout << "0";
    }
    std::cout << std::endl;
}

int main()
{
    signal(2, handler);//捕捉2号信号
    // 只是在用户层面上进行设置
    sigset_t set, oset; 
    sigemptyset(&set);
    sigemptyset(&oset);
    sigaddset(&set, 2); //SIGINT

    // 设置进入进程,谁调用,设置谁
    sigprocmask(SIG_BLOCK, &set, &oset); // 1. 2号信号没有反应 2. 我们看到老的信号屏蔽字block位图是全零、
    int cnt = 0;
    while(true)
    {
        //获取pending信号集
        sigset_t pending;
        int n = sigpending(&pending);
        if(-1 == n)
        {
            std::cerr<< "调用失败" <<std::endl;
            exit(1);
        }
        //打印pending位图
        printfPending(&pending);
        sleep(1);

        if(cnt++ == 10)
        {
            std::cout << "解除对2号信号的屏蔽" << std::endl;
            sigprocmask(SIG_SETMASK,&oset, nullptr);
        }
    }
    return 0;
}

Linux信号_第22张图片

捕捉信号

用户态和内核态

信号的产生是异步的。信号的处理,可以不是立即的处理的,而是在“合适“的时候进行处理的。合适的时候是当进程从内核态切换回用户态的时候。进程会在OS的指导下,进行信号的检测与处理。硬件当中一旦出问题的话,它类似于中断的一种方式,然后告诉操作系统,然后操作系统去处理的。不要简单的认为,就是只有出问题的时候,操作系统才处理它,出问题一定是因为进程导致的,或者执行流导致的,操作系统也在不断的调度它。所以操作系统在调度的时候,操作在执行我们对应的代码的时候,那么在执行每条语句的时候,它在硬件上或者软件上的会自动去检查我们对应的尤其是状态标志位,还有我们mmu异常这样的状况,比如说比如说操作系统把你当前的进程切下去了,下次再唤醒的时候他也会直接做检查的,所以硬件上软件上它都会做。
用户态:执行你写的代码的时候,进程所处的状态。
内核态:执行OS的代码的时候,进程所处的状态。进程时间片到了,需要切换,就要执行进程切换逻辑或执行系统调用
Linux信号_第23张图片
1.所有的进程[0, 3]GB是不同的,每一个进程都要有自己的用户级页表。
2.所有的进程[3, 4]GB是一样的,每一个进程都可以看到同一张内核级页表,所有进程都可以通过统一的窗口, 看到同一个OS。
3.没错OS运行的本质:其实都是在进程的地址空间内运行的。无论进程如何切换,[3, 4]GB操作系统的代码与数据不变。
4.所谓的系统调用的本质:其实就如同调用so中的方法,在自己的地址空间中进行函数跳转并返回即可。

只要你访问的是0~3G的范围,你对应的状态全部就是用户态,一旦你要访问我们对应的,那么3~4级的区域的体系结构,操作系统会对你的身份,对你的执行级别做检测,检测之后发现你并不是内核态,你是用户态,那么此时我们就直接拦截你CPU,就拒绝执行你的代码,然后操作系统当中就能识别到有非法访问,进而操作系统识别到硬件异常,然后对你的目标进程发信号,终止你就可以了,换一句话说我们可以设置用户态和内核态这两种执行级别。
CPU里有个寄存器CR3,CR3寄存器里有若干个比特位,对整个比特位整体识别下来,如果是3表示正在运行的进程执行级别是用户态,0表示正在运行的进程执行级别是内核态,但你要执行系统调用时,你要跳转,所以你要更改CPU内的PC指针,给说CPU我要访问这个地址,CPU在访问的时候之前它会检测,检测一下让CPU访问的这个地址,如果访问的是3~4级的,CPU会查看CR3寄存器,如果CR3寄存器里边的值是3,然后直接拦截你,设计你的状态叫做非法访问,CR3寄存器当中有个非法访问的状态标志位,操作系统识别到,进而向目标进程发送信号终止,如果发现你的执行级别是0,那么就允许你访问,让你直接访问到这个代码或者是数据。操作系统提供的所以系统调用接口,内部在正式执行调用逻辑的时候,会去修改执行级别。所以我们的系统调用,它内部自动的把包了对应更改执行级别,所以你又不能更改执行级别,所以你只能调用系统调用,这样的话你就只能通过系统调用来访问操作系统的代码和数据了。

操作系统是软件,本质上是个死循环,每台计算机都有时钟,时钟每隔很短的一段时间会向OS发送时钟中断,操作系统要执行对应的中断处理方法,检查当前进程的时间片,如果时间片到了,就将当前进程对应的上下文等进行保存并切换,然后选择合适的进程,进程调度函数schedule()
操作系统的运行是在进程的上下文中运行的

信号捕捉的原理

Linux信号_第24张图片
每次从内核到用户都要做信号检测,检查pending位图(此时处于内核态),首先判断pending位图,这个32位的比特位整体是否为0,如果为0直接返回,如果不为0,从低比特位找最近的1,进行检查,如果找到了在查看block位图,查看该信号是否阻塞,如果没有在执行handler表中的默认处理,忽略处理,还是自定义捕捉。处理动作之前在内核时将比特位制0。一次只能处理一个信号,下一个信号只能下一次检查时处理。

sigaction

 #include 

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

功能: 可以读取和修改与指定信号相关联的处理动作。
参数:

  • signum表示几号信号。
  • act指针非空,则根据act修改该信号的处理动作。
  • 若oldact指针非空,则通过oldact传出该信号原来的处理动作。

返回值: 调用成功则返回0,出错则返回-1。

struct sigaction
{
    void (*sa_handler)(int);
    void (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t sa_mask;
    int sa_flags;
    void (*sa_restorer)(void);
};

结构体成员
sa_handler

  • 将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号
  • 将sa_handler赋值为常数SIG_DFL表示执行系统默认动作
  • 将sa_handler赋值为一个函数指针表示用自定义函数捕捉信号。

sa_flags: 设置为0即可。
sa_mask: 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。

剩余的结构体成员不用管。

#include 
#include 
#include 
#include 

void handler(int signo)
{
    std::cout<<"捕捉到的信号是:" << signo << std::endl;
}

int main()
{
    struct sigaction act;
    struct sigaction oldact;
    memset(&act,0,sizeof(act));
    memset(&oldact,0,sizeof(oldact));
    act.sa_handler = handler;
    act.sa_flags = 0;
    sigaction(2,&act,&oldact);
    while(true)
    {
        sleep(1);
    }
    return 0;
}

Linux信号_第25张图片

#include 
#include 
#include 
#include 

void printfPending(sigset_t *oset)
{
    std::cout << "当前进程的pending位图: ";
    for(int signo = 1; signo <=31; signo++)
    {
        if(sigismember(oset, signo)) std::cout << "1";
        else std::cout << "0";
    }
    std::cout << std::endl;
}

void handler(int signo)
{
    std::cout<<"捕捉到的信号是:" << signo << std::endl;
    int cnt = 20;
    std::cout << "当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字"<<std::endl;
    while(cnt)
    {
        cnt--;

        sigset_t pending;
        sigemptyset(&pending); // 不是必须的
        sigpending(&pending);
        printfPending(&pending);
        sleep(1);
    }
    std::cout << "当信号处理函数返回时自动恢复原来的信号屏蔽字" << std::endl;
}

int main()
{
    
    struct sigaction act;
    struct sigaction oldact;
    memset(&act,0,sizeof(act));
    memset(&oldact,0,sizeof(oldact));
    act.sa_handler = handler;
    act.sa_flags = 0;

    sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask,3);
    sigaddset(&act.sa_mask,4);
    sigaddset(&act.sa_mask,5);

    sigaction(2,&act,&oldact);
    while(true)
    {
        std::cout << getpid() << std::endl;
        sleep(1);
    }

    return 0;
}

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

可重入函数

main函数调用链表的插入insert函数,进程在收到某种信号时,它的处理动作是用户自定义的函数sighandler, sighandler函数内部又调用了链表的插入insert函数。
Linux信号_第27张图片
通过对下面链表的插入,理解可重入函数。
Linux信号_第28张图片

1.main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步(p->next = head;)的时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换到sighandler函数。
Linux信号_第29张图片
2.sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的 两步都做完之后从sighandler返回内核态.
Linux信号_第30张图片
3.再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二步
Linux信号_第31张图片
结果是,main函数和sighandler先后 向链表中插入两个节点,而最后只有一个节点真正插入链表中了,而node2节点找不到了,造成内存泄漏。

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

volatile

其中我们在编译的时候,实际上有相当多的选项,在它的选项当中有若干个编译级别其中一直用的gcc一般有-O -O0 -O1 -O2 -O3 -Os -Ofast编译优化级别,不同的编译优化级别,优化的程度是不同的。当然不同的版本gcc用可能不一样。

  • -O0:不进行任何优化,编译速度最快,生成的代码最简单,调试时最容易使用。

  • -O1:进行基本的优化,包括去除无用代码、简化表达式、优化循环等,编译速度较快,生成的代码质量较高。

  • -O2:在-O1的基础上进行更多的优化,包括函数内联、循环展开、减少函数调用等,编译速度较慢,生成的代码质量更高。

  • -O3:在-O2的基础上进行更多的优化,包括向量化、调整指令顺序等,编译速度更慢,生成的代码质量更高。

  • -Os:优化代码大小,尽量减少生成的代码大小,适用于嵌入式系统等空间受限的环境。

  • -Ofast:在-O3的基础上进行更多的优化,包括允许不符合标准的优化、禁用某些安全检查等,编译速度更快但可能会影响代码的正确性。

#include 
#include 
int flag = 0;
void handler(int sig)
{
    printf("chage flag 0 to 1\n");
    flag = 1;
    printf("flag=%d\n",flag);
}
int main()
{
    signal(2, handler);
    while (!flag);//注意这里我们故意没有携带while的代码块,故意让编译器认为在main中,quit只会被检测
    printf("进程退出正常\n");
    return 0;
}

Linux信号_第32张图片

同样的代码
Linux信号_第33张图片

编译器对这份代码哪里进行了优化?CTRL+C对应的信号捕捉的方法是执行着的,然而printf代码都打出来了,flag = 1,那么说明这个赋值动作一定执行了,可是为什么一定执行了,那么where循环却不退出呢?

  • 在没有优化的情况下,每一都把内存中的fiag数据加载到CPU。
  • 在有优化的情况下,在我的整个代码里,可以分为两个执行流,一是main执行流,二是handler执行流,它是属于信号的流程。在main执行流中,发现当前flag变量没有被修改的,flag变量只是在循环时被检测,所以 CPU或者编译器它在编译时发现,flag变量没有被修改,我为什么要重复,每次都要将flag变量做据录到的内存里,在从内存里漏到CPU里面,没必要,所以我只需要在编译代码形成汇编代码时,只要第一次把数据漏到CPU寄存器中,往后循环检测时,只需要检测CPU的寄存器即可。

volatile 告诉编译器,保证每次检测,都要尝试着从内存中进行数据读取,不要用寄存器中的数据,让内存数据可见!

#include 
#include 
volatile int flag = 0;//volatile 保证内存可见性
void handler(int sig)
{
    printf("chage flag 0 to 1\n");
    flag = 1;
    printf("flag=%d\n",flag);
}
int main()
{
    signal(2, handler);
    while (!flag);//注意这里我们故意没有携带while的代码块,故意让编译器认为在main中,quit只会被检测
    printf("进程退出正常\n");
    return 0;
}

Linux信号_第34张图片
如何理解编还器的优化?
1.编译器的本质是在代码上动手脚(更改代码,在语言层到汇编层之间优化的)
2.CPU其实很笨,其实用户喂给他什么代码,它才执行什么代码

SIGCHLD信号

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

#include 
#include 
#include 
#include 
#include 

pid_t id;
void waitProcess(int signo)
{
    printf("捕捉到一个信号: %d, who: %d\n", signo, getpid());
    sleep(5);
    //证明会全部回收所有进程
    //只要收到一个信号,就证明该waitProcess函数至少会被调用一次,
    //waitpid 中的-1,它是等待任何一个退出,然后你退了我就把你收回来。
    //所以waitProcess函数中循环的waitpid进行回收,直到没有可回收,
    //因为我把所有的事情都不是全退了,没有可收到的返回值它就直接-1,或者直接就变成小于0了,我们直接就break。
    //所以它是循环式的,将我们的所有的指令声全部回收到。

    //pid_t res = waitpid(-1, NULL, 0);在这种情况下如果有10个进程,5个进程退出,5个进程没退出,当前进程会阻塞。
    while (1)
    {
        pid_t res = waitpid(-1, NULL, WNOHANG);
        if (res > 0)
        {
            printf("wait success, res: %d, id: %d\n", res, id);
        }
        else break; // 如果没有子进程了?
    }

    printf("handler done...\n");
}

int main()
{
    signal(SIGCHLD, waitProcess);
    int i = 1;
    for (; i <= 10; i++)
    {
        id = fork();
        if (id == 0)
        {
            // child
            int cnt = 5;
            while (cnt)
            {
                printf("我是子进程, 我的pid: %d, ppid: %d\n", getpid(), getppid());
                sleep(1);
                cnt--;
            }

            exit(1);
        }
    }
    // 如果你的父进程没有事干,你还是用在直接等待的方法
    // 如果你的父进程很忙,而且不退出,可以选择信号的方法
    while (1)
    {
        sleep(1);
    }

    return 0;
}

由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略 通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可用

#include 
#include 
#include 
#include 
#include 
#include 
int main()
{
    signal(SIGCHLD, SIG_IGN);
    int i = 1;
    for (; i <= 10; i++)
    {
        id = fork();
        if (id == 0)
        {
            // child
            int cnt = 5;
            while (cnt)
            {
                printf("我是子进程, 我的pid: %d, ppid: %d\n", getpid(), getppid());
                sleep(1);
                cnt--;
            }

            exit(1);
        }
    }
    while (1)
    {
        sleep(1);
    }

    return 0;
}

你可能感兴趣的:(linux)