【Linux】信号--信号的捕捉/可重入函数/volatile/SIGCHLD信号

文章目录

一、信号的捕捉

1.用户态和内核态

用户态的的时候,进行以下操作:1.操作系统自身的资源(getpid,waitpid...)2.硬件资源(printf, write,read)

用户为了访问内核或者硬件资源,必须通过系统调用完成访问。实际执行系统调用"人是"进程",但是身份其实是内核。往往系统调用比较费时间一些,所以尽量避免频繁调用系统调用

CPU中有两类寄存器:1.可见寄存器2.不可见寄存器。凡是和当前进程强相关的,上下文数据都保存在寄存器中。CR3寄存器表征当前进程的运行级别;0:内核态,3表示用户态

我一直不太理解:我是一个进程,怎么跑到OS中执行方法呢?

每个进程都有自己独立的用户级页表,内核级页表只有一份就够了

每一个进程都有自己的地址空间(用户空间独占)内核空间(被映射到了每一个进程的3~4G)。进程要访问OS的接口,其实]只需要在自己的地址空间上进行跳转就可以了!!每一个进程都有3~4GB,都会共享一个内核级页表,无论进程如何切换,会不会更改任何的[3.4]。用户,凭什么能够执行访问内核的接口或者数据呢?系统调用接口,起始的位置会帮你做的!Int 80 --陷入内核

2.内核如何实现信号的捕捉

信号产生的时候,不会被立即处理,而是在合适的时候。从内核态返回用户态的时候,进行处理,说明曾经我一定是先进入了内核态!----系统调用,进程切换

线程通过系统调用陷入内核,完成了从用户态到内核态的转变你,然后遍历block和pending表,以及映射的hander,对信号进行默认/忽略/自定义的捕捉,对于自定义捕捉,操作系统通过特定的调用,将自己的身份重新改为用户态,执行自定义函数,执行完毕之后,又通过特殊的系统调用sigreturn再次回到内核,继续进行信号检测,然后返回用户模式,从上次被中断的地方继续向下执行。

3.sigaction

c 复制代码
#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函数调用,而是被系统所调用。

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

void Count(int cnt)
{
    while(cnt)
    {
        printf("cnt: %2d\r", cnt);
        fflush(stdout);
        cnt--;
        sleep(1);
    }
    printf("\n");
}

void handler(int signo)
{
    std::cout << "get a signo: " << signo << "正在处理中..." << std::endl;
    Count(20);
}

int main()
{
    struct sigaction act, oact;
    act.sa_handler = handler;
    act.sa_flags = 0;
    // 当我们正在处理某一种信号的时候,我们也想顺便屏蔽其他信号,就可以添加到这个sa_mask中
    sigemptyset(&act.sa_mask);
    // sigaddset(&act.sa_mask, 3);
    sigaction(SIGINT, &act, &oact);

    while (true)
        sleep(1);
    return 0;
}

当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。 sa_flags字段包含一些选项,本章的代码都把sa_flags设为0,sa_sigaction是实时信号的处理函数

二、可重入函数

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

volatile 作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作

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

volatile int quit = 0;

void handler(int signo)
{
    printf("%d 号信号,正在被捕捉!\n",signo);
    printf("quit: %d", quit);
    quit = 1;
    printf("->%d\n", quit);
}

int main()
{
    signal(2, handler);
    while (!quit);
    printf("注意,我是正常退出的\n");
    return 0;
}

不加volatile就会一直休眠

加了之后,收到2号信号后直接退出

四、SIGCHLD信号

wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻 塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不 能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一 下,程序实现复杂。

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

请编写一个程序完成以下功能:父进程fork出子进程,子进程调用exit(2)终止,父进程自定 义SIGCHLD信号的处理函数,在其中调用wait获得子进程的退出状态并打印。

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

void hanlder(int signo)
{
    pid_t id;
    int status;
    if (id = waitpid(-1, &status, 0) > 0)
    {
        std::cout << "wait child process success,lastcode: " << ((status >> 8) & 0xff) << std::endl;
    }
    std::cout << "child is quit" << std::endl;
}
int main()
{
    signal(SIGCHLD, hanlder);
    pid_t id = fork();
    if (id == 0)
    {
        std::cout << "child create seccuess" << std::endl;
        sleep(3);
        exit(2);
    }
    while (true)
    {
        std::cout << "father process is doing other things" << std::endl;
        sleep(1);
    }
    return 0;
}

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

请编写程序验证这样做不会产生僵尸进程

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

int main()
{
    signal(SIGCHLD, SIG_IGN);
    pid_t id = fork();
    if (id == 0)
    {
        std::cout << "child create success" << std::endl;
        sleep(3);
        exit(2);
    }

    int status = 0;
    id = waitpid(id, &status, 0);
    if (id > 0)
    {
        std::cout << "wait child process success,lastcode: " << ((status >> 8) & 0xff) << std::endl;
    }

    std::cout << "child is quit" << std::endl;
    return 0;
}
相关推荐
梅见十柒10 分钟前
wsl2中kali linux下的docker使用教程(教程总结)
linux·经验分享·docker·云原生
Koi慢热13 分钟前
路由基础(全)
linux·网络·网络协议·安全
传而习乎23 分钟前
Linux:CentOS 7 解压 7zip 压缩的文件
linux·运维·centos
soulteary25 分钟前
突破内存限制:Mac Mini M2 服务器化实践指南
运维·服务器·redis·macos·arm·pika
我们的五年33 分钟前
【Linux课程学习】:进程程序替换,execl,execv,execlp,execvp,execve,execle,execvpe函数
linux·c++·学习
爱吃青椒不爱吃西红柿‍️1 小时前
华为ASP与CSP是什么?
服务器·前端·数据库
IT果果日记1 小时前
ubuntu 安装 conda
linux·ubuntu·conda
Python私教1 小时前
ubuntu搭建k8s环境详细教程
linux·ubuntu·kubernetes
羑悻的小杀马特1 小时前
环境变量简介
linux
小陈phd2 小时前
Vscode LinuxC++环境配置
linux·c++·vscode