Linux进程信号

文章目录

  • 一、信号入门背景知识
    • 1.1 什么是信号
    • 1.2 学习信号的时间轴
    • 1.3 技术应用角度的信号
    • 1.4 signal—信号处理函数
    • 1.5 常见的信号
    • 1.6 信号处理常见方式
  • 二、产生信号
    • 2.1 通过键盘产生
    • 2.2 硬件异常产生信号
    • 2.3 系统调用产生信号
    • 2.4 软件条件产生信号
    • 2.5 信号发送的本质
  • 三、信号产生过程中
    • 3.1 信号的概念专业化
    • 3.2 三张重要的表
    • 3.3 sigset_t
    • 3.4 信号集操作函数
      • sigprocmask
      • sigpending
  • 四、信号发送后
    • 4.1 信号发送后如何被处理
    • 4.2 信号处理函数sigaction
      • sa_mask
  • 五、可重入函数
  • 六、其他
    • 6.1 volatile关键字
    • 6.2 SIGCHLD信号

首先明确:信号VS信号量
他们两者毫无关系!

一、信号入门背景知识

生活中的信号——给人看的!
计算机中的信号——给进程看的!

1.1 什么是信号

①进程具有识别并处理信号的能力(这个能力远远早于信号的产生),且进程知道该如何识别信号,且进程提前约定好当接收到信号时要执行了哪些操作,即约定信号的处理方式和安装信号处理函数。
②进程收到某种信号的时候,并不是立即处理,而是在合适的时候。(信号随时都可能产生——异步性,但是当前进程可能会做更加重要的事情)
③进程收到信号后,需要先将信号保存起来,以供在合适的时候处理

信号的本质:数据

保存在哪里呢?
信号发送—>是往task_struct内写入信号数据的!

但是task_struct是一个内核数据结构,内核并不相信任何人,只相信自己,所以是操作系统向task_struct中写入信号数据的。
所以:无论我们的任何信号在任何时间发送,本质都是在底层通过操作系统发送的!

1.2 学习信号的时间轴

Linux进程信号_第1张图片

1.3 技术应用角度的信号

用户输入命名,在shell中启动一个前台程序,ctrl+c就能在键盘输入产生一个硬件中断,被操作系统获取,解释称为信号,发送给目标前台进程,前台进程因为收到信号,进而退出

#include
#include
int main()
{
    while(1){
        printf("hello world! pid:%d\n",getpid());
        sleep(1);
    }
    return 0;
}

Linux进程信号_第2张图片

1.4 signal—信号处理函数

函数名称 signal
函数功能 信号处理函数
头文件 #include
函数原型 sighandler_t signal(int signum, sighandler_t handler);
参数 signum:要处理的信号
handler:信号处理函数
返回值 ≠-1:成功(该信号以前的处理函数)
SIG_ERR:出错
typedef void (*sighandler_t)(int);// 函数指针

sighandler_t signal(int signum, sighandler_t handler); // 修改进程对于信号的默认处理动作

signal函数声明中,出现了一个新的类型,sighandler_t,该类型是被重命名的,是一种函数指针,函数指针指向的这个函数返回值是void,有一个int类型的参数。在signal函数安装时需要此类型的函数指针作为参数,并且当进程调用signal函数捕捉信号成功后,将会返回signo信号之前的处理函数;如果进程在运行过程中收到信号signo时,将会转去指向handler函数(回调函数)。

#include 
#include 
#include 
void handler(int signo)
{
    printf("you can not stop this process by ctrl+c! signo:%d pid:%d\n",signo,getpid());
}
int main()
{
    //通过signal注册对2号信号的处理动作,改成我们的自定义动作
    signal(2,handler);
    while(1){
        printf("hello world! pid:%d\n",getpid());
        sleep(1);
    }
    return 0;
}

注册函数的时候,不是调用这个函数,只有当信号到来的时候,这个函数才会去调用我们的自定义动作(回调函数)
结果展示:
Linux进程信号_第3张图片

1.5 常见的信号

查看指令:kill -l
Linux进程信号_第4张图片

  • 每个信号都有一个编号和宏定义名称,这些宏定义可以在signal.h中找到
  • 34号以上为实时信号,我们只关心1到31这些普通信号。这些信号各自在什么条件下产生,默认动作是什么,可以在7号手册中查看 man 7 signal

总结:
一般而言,进程收到信号的处理方案有3种情况
1.默认动作——部分是终止自己,暂停等
2.忽略动作——是一种信号处理的方式,只不过动作就是什么也不干
3.自定义动作——我们刚刚用signal方法,就是在修改信号的处理动作由:默认->自定义动作

1.6 信号处理常见方式

  • 默认动作——一部分是终止自己,暂停等
  • 忽略此信号——是一种信号处理的方式,只不过动作就是什么也不干
  • 自定义信号处理函数(信号的捕捉)——我们刚刚用signal方法,就是在修改信号的处理动作由:默认—>自定义动作

二、产生信号

2.1 通过键盘产生

我们可以捕捉各种各样的信号:

void handler(int signo)
{
    printf("signo:%d pid:%d\n",signo,getpid());
    exit(123);
}
int main()
{   
    int sig=1;
    for(;sig<31;sig++)
    {
        signal(sig,handler);
    }
    while(1){
        printf("hello world! pid:%d\n",getpid());
        sleep(1);
    }
    return 0;
}

Linux进程信号_第5张图片
我们可以知道,信号的一种产生方式,通过键盘终端产生。
键盘产生的信号,只能终止前台进程,加个&可以让进程后台运行
我们还可以发现9号信号是不能够被捕捉的(不可被自定义)!

2.2 硬件异常产生信号

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

int main()
{
    
    while (1)
    {
        int * p=NULL;
        p=(int*)100;
        *p=100;  //error 空指针解引用
        printf("hello world! pid:%d\n", getpid());
        sleep(1);
    }
    return 0;
}

结果展示:
在这里插入图片描述
段错误!!!VS下叫做崩溃
就是程序中有越界或者野指针的情况!

为什么会崩溃?
我们再来看一下这个代码:

void handler(int signo)
{
    switch (signo)
    {
    case 2:
        printf("get a signo:%d\n", signo);
        break;
    case 3:
        printf("get a signo:%d\n", signo);
        break;
    case 9:
        printf("get a signo:%d\n", signo);
        break;
    default:
        printf("get a signo:%d pid:%d\n", signo,getpid());
        break;
    }
    // exit(123);
}
int main()
{
    int sig = 1;
    for (; sig < 31; sig++)
    {
        signal(sig, handler);
    }
    while (1)
    {
        int *p = NULL;
        p = (int *)100;
        *p = 100; // error 空指针解引用
        printf("hello world! pid:%d\n", getpid());
        sleep(1);
    }
    return 0;
}

结果展示:
Linux进程信号_第6张图片
原因就是我们的进程收到了11号信号!

同样我们有着其他错误,比如除0时会收到8号信号。
Linux进程信号_第7张图片
同样我们不捕捉该信号,又会发生什么呢?
如下发生浮点数错误:
在这里插入图片描述
崩溃的原理:
Linux进程信号_第8张图片
总结

  1. 在win or Linux下进程崩溃的本质,是进程收到了对应的信号,然后进程执行信号的默认处理动作(杀死进程)。
  2. 信号产生的方式,程序中存在异常问题,导致我们收到信号退出。

当进程崩溃的时候,你最想知道什么? 崩溃的原因是什么,可以通过waitpid();+ status参数获取到
崩溃的原因–>崩溃时,收到的是哪一个信号?
你还想知道什么? ?在哪一行崩溃了! !

下面我们再来看看status这个整型:
Linux进程信号_第9张图片

  1. 在Linux中,当一个进程退出的时候,它的退出码和退出信号都会被设置(正常情况)
  2. 当一个进程异常的时候,进程的退出信号会被设置,表明当前进程退出的原因
  3. 如果必要,OS会设置退出信息中的core dump标志位,并将进程在内存中的数据转储到磁盘当中,方便我们后期调试
  4. 进程如果异常的时候,被core dump该位置会被设置为1,不是所有的信号都被core dump

默认情况下,在Linux云服务器上,core dump这项技术是被关闭的,我们要手动打开。
查看系统资源指令:ulimit -a
Linux进程信号_第10张图片

手动设置该文件的大小:ulimit -c 10240

Linux进程信号_第11张图片
现在就允许我们core dump了。

现在再次运行我们之前的程序:
在这里插入图片描述
现在观察,发现多了一个core文件,这个文件可以让我们用gbd调试的
Linux进程信号_第12张图片
Linux进程信号_第13张图片
使用gdb调试:
Linux进程信号_第14张图片

验证core dump标志位是否被设置的:

//验证core dump标志位是否被设置的
#include 
int main()
{
    if (fork() == 0)
    {
        while (1)
        {
            printf("I am child...!\n");
            int a = 10;
            a/=0;
        }
    }
    int status = 0;
    waitpid(-1, &status, 0);
    printf("exit code:%d\n", (status >> 8) & 0xFF);
    printf("exit signal:%d\n", status & 0x7F);
    printf("exit dump flag:%d\n", (status >> 7) & 1);

    return 0;
}

Linux进程信号_第15张图片

2.3 系统调用产生信号

产生信号的另一种方法是调用kill函数,是由某个进程调用kill发送信号给指定进程或者进程组

函数名称 kill
函数功能 向进程发送信号
头文件 #include
函数原型 int kill(pid_t pid, int sig);
参数 pid:目标进程PID
sig:要发送的信号
返回值 0:成功
-1:失败

采用系统调用向目标进程发送信号

#include 
static void Usage(const char* proc)
{
    printf("Usage:\n\t %s signo who\n",proc);
}
// ./mytest signo who
int main(int argc,char* argv[])
{
    if(argc!=3)
    {
        Usage(argv[0]);
        return 1;
    }
    int signo=atoi(argv[1]); // 强转成为整数
    int who=atoi(argv[2]);
    printf("signo:%d,who:%d\n",signo,who);

    kill(who,signo);

    return 0;
}

结果展示:
Linux进程信号_第16张图片
raise函数也可以向进程发送信号,与kill函数不同的是,raise函数发送信号的目标进程是确定的,是向调用raise函数的进程自己发送一个信号,因此使用raise函数时,通常说进程自举了一个信号。

函数名称 raise
函数功能 自举一个信号
头文件 #include
函数原型 int raise(int signo)
参数 signo:要发送的信号
返回值 0:成功
-1:失败
// raise函数
int main()
{
    while (1)
    {
        printf("I am process!\n");
        sleep(3);
        raise(8);
    }

    return 0;
}
函数名称 abort
函数功能 使当前进程接收到信号而异常终止,意思就是给自己发送6号信号
头文件 #include
函数原型 void abort(void)
参数
返回值
//abort函数
void handler(int signo)
{
    printf("get a signo:%d pid:%d\n",signo,getpid());
    exit(123);
}
int main()
{
    int sig=1;
    for(;sig<31;sig++)
    {
        signal(sig,handler);
    }
    while(1){
        printf("hello world! pid:%d\n",getpid());
        sleep(1);
        abort();//给自己发送6号信号
    }
    return 0;
}

2.4 软件条件产生信号

通过某种软件(OS),来触发信号的发送,系统层面设置定时器,或者某种操作而导致条件不就绪等这样的场景下,而触发的信号发送

进程间通信:当读端不光不读, 而且还关闭了读fd,写端一直在写,最终写进程会受到sigpipe (13) ,就是一种典型的软件条件触发的信号发送

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

函数名称 alarm
函数功能 设置计时器
头文件 #include
函数原型 int alarm(int seconds);
参数 seconds:定时器设置的秒数
返回值 0:之前未调用过alarm
>0:上一次调用alarm时设置的秒数余留的时间
void handler(int signo)
{
    switch (signo)
    {
    case 2:
        printf("get a signo:%d\n", signo);
        break;
    case 3:
        printf("get a signo:%d\n", signo);
        break;
    case 9:
        printf("get a signo:%d\n", signo);
        break;
    default:
        printf("get a signo:%d pid:%d\n", signo,getpid());
        break;
    }
     exit(123);
}
int main()
{
    int sig=1;
    for(;sig<31;sig++)
    {
        signal(sig,handler);
    }

    alarm(3); // 3秒后给当前进程发送14号信号

    while(1){
        printf("hello world! pid:%d\n",getpid());
        sleep(1);
    }
    return 0;
}

结果展示:
Linux进程信号_第17张图片
返回值
这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。打个比方,某人要小睡一觉,设定闹钟为30分钟之后响,20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响,“以前设定的闹钟时间还余下的时间”就 是10分钟。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数

//alarm 函数
void handler(int signo)
{
    switch (signo)
    {
    case 2:
        printf("get a signo:%d\n", signo);
        break;
    case 3:
        printf("get a signo:%d\n", signo);
        break;
    case 9:
        printf("get a signo:%d\n", signo);
        break;
    default:
        printf("get a signo:%d pid:%d\n", signo,getpid());
        break;
    }
     exit(123);
}
int main()
{
    int sig=1;
    for(;sig<31;sig++)
    {
        signal(sig,handler);
    }

    int ret=alarm(30); // 30秒后给当前进程发送14号信号

    while(1){
        printf("hello world! ret=%d\n",ret);
        sleep(3);

        int res=alarm(0);// 取消闹钟
        printf("res=%d\n",res);
    }
    return 0;
}

统计一秒钟我们的服务器对一个整型值能递增多少

//统计一下一秒钟对于整型值能递增多少
int count=0;
int main()
{
    alarm(1);// 没有设置alarm信号捕捉(自定义),而是执行默认动作——终止进程
    while(1)
    {
        printf("count=%d\n",count);
        count++;
    }
    return 0;
}

Linux进程信号_第18张图片
改进一下代码:

//统计一下一秒钟对于整型值能递增多少
int count=0;
void handlerAlarm(int signo)
{
    printf("count=%d\n",count);
    exit(123);
}
int main()
{
    signal(SIGALRM,handlerAlarm);
    alarm(1);// 没有设置alarm信号捕捉(自定义),而是执行默认动作——终止进程
    while(1)
    {
        //现在不进行打印操作
        count++;
    }
    return 0;
}

在这里插入图片描述
为何两次数据差异那么大?
原因是:IO太慢了

总结
信号产生的方式种类虽然非常多,但是无论产生信号的方式千差万别,但是最终,一定都是通过OS向目标进程发送的信号! ! !

2.5 信号发送的本质

进程中采用位图结构来标识进程是否收到了某信号!
Linux进程信号_第19张图片
众多的信号一定是发给进程的,而且是发给进程的task_struct的,那么就会用到一个数据变量(unisgned int)来标识是否收到了某信号。——位图结构
Linux进程信号_第20张图片
所以:
比特位的位置,代表的是哪一个信号
比特位的内容,代表的就是是否收到了信号

如何理解OS给进程发送信号?
OS发送信号数据给task_ struct–>本质是OS向指定进程的task_struct 中的信号位图写入比特位1,即完成能信号的发送,信号的发送–>信号的写入。

三、信号产生过程中

3.1 信号的概念专业化

递达:实际执行信号的处理动作

  • 递达又分为三种 1. 默认 2. 忽略 3. 自定义捕捉

未决:信号从产生到递达之间

  • 本质是这个信号被暂存在进程的task_struct信号位图中

阻塞:进程可以选择阻塞某个信号

  • 本质是OS允许进程暂时屏蔽指定的信号
    1.信号依旧是未决的
    2.该信号不会被递达,直到解除阻塞,方可递达

忽略和阻塞的区别
忽略是信号递达之后的处理动作
阻塞是未递达

3.2 三张重要的表

Linux进程信号_第21张图片
blockpending表都是位图结构
阻塞位图也叫做信号屏蔽字

比特位的位置,代表的是哪一个信号
比特位的内容,代表的就是是否收到了信号

每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表handler。
block:状态位图,表示哪些信号不应该被递达,直到解除阻塞
pending:保存的是已经收到,但是还没有被递达的信号
handler:处理信号的动作

信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志
OS发送信号的本质:修改目标进程中的pending位图。所以进程内置了“识别”信号的方式!
例如:SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。

3.3 sigset_t

不要认为有接口才是系统调用,OS也会给用户提供数据类型,配合系统调用完成

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

//set是一个变量,该变量在什么地方保存?
//和我们之前用到的int, double, 没有任何差别,都是在用户栈上
sigset_t set;
set|=1;//error 不合法

虽然sigset_t是一个位图结构,但是不同的操作系统实现是不一样的,不能让用户直接修改该变量,需要使用特定的函数。

3.4 信号集操作函数

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

 #include 
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类型的变量之前,一定要调用sigemptysetsigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddsetsigdelset在该信号集中添加或删除某种有效信号

这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。

sigprocmask

如果进程需要阻塞某些信号,可以使用函数sigprocmask来设置屏蔽信号集。

函数名称 sigprocmask
函数功能 查看或者修改当前的屏蔽信号集
头文件 #include
函数原型 int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数 how:修改屏蔽信号集的方式
set:本次要按照how的方式操作的信号集合指针
oset:修改前的屏蔽信号集指针
返回值 0:成功
-1:失败

说明:如果oset为非空,那么进程当前的屏蔽信号集就会通过oset返回。指针set为空时,不修改进程当前的屏蔽信号集;set非空时,how的取值才有意义,表示如何修改当前的屏蔽信号集,可以选择如下三种方式。

how 说明
SIG_BLOCK set包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask按位或上set
SIG_UNBLOCK set包含了我们希望从当前信号屏蔽字中解除阻塞信号,相当于mask=mask&~set
SIG_SETMASK 设置当前信号屏蔽字为set所指向的值,相当于mask=set
#include 
#include 
#include 
int main()
{
    // sigset_t set;
    // //set|=1;  error
     sigset_t iset,oset;// in out
     sigemptyset(&iset);
     sigemptyset(&oset);

    //对2号进行信号屏蔽
    sigaddset(&iset,2);

    //1. 设置当前进程的屏蔽字
    //2. 获取当前进程老的屏蔽字
    sigprocmask(SIG_SETMASK,&iset,&oset);

    while(1)
    {
        printf("hello world!\n");
        sleep(1);
    }
    return 0;
}

结果展示:
Linux进程信号_第22张图片

#include 
#include 
#include 
int main()
{
    // sigset_t set;
    // //set|=1;  error
     sigset_t iset,oset;// in out
     sigemptyset(&iset);
     sigemptyset(&oset);

    //对2 9 号进行信号屏蔽
    sigaddset(&iset,2);
    sigaddset(&iset,9);

    //1. 设置当前进程的屏蔽字
    //2. 获取当前进程老的屏蔽字
    sigprocmask(SIG_SETMASK,&iset,&oset);

    while(1)
    {
        printf("hello world!\n");
        sleep(1);
    }
    return 0;
}

Linux进程信号_第23张图片

sigpending

不修改pending位图,只是单纯的获取进程的pending位图

#include 
int sigpending(sigset_t *set);
void show_pending(sigset_t *set)
{
	printf("current process pending:");
    int i = 1;
    for (i = 1; i < 31; i++)
    {
        if (sigismember(set, i))
        {
            printf("1");
        }
        else
        {
            printf("0");
        }
    }
    printf("\n");
}
// 获取pending位图
int main()
{
    sigset_t iset, oset;
    sigemptyset(&iset);
    sigemptyset(&oset);

    //对2号进行信号屏蔽
    sigaddset(&iset, 2);

    // 1. 设置当前进程的屏蔽字
    // 2. 获取当前进程老的屏蔽字
    sigprocmask(SIG_SETMASK, &iset, &oset);

    sigset_t pending;
    while (1)
    {
        sigemptyset(&pending);
        sigpending(&pending);
        show_pending(&pending);
        sleep(1);
    }
    return 0;
}

Linux进程信号_第24张图片

void show_pending(sigset_t *set)
{
    printf("current process pending:");
    int i = 1;
    for (i = 1; i <= 31; i++)
    {
        if (sigismember(set, i))
        {
            printf("1");
        }
        else
        {
            printf("0");
        }
    }
    printf("\n");
}
void handler(int signo)
{
    printf("%d号信号被递达了,已经处理完成!\n",signo);
}
// 获取pending位图
int main()
{
    signal(2,handler);
    sigset_t iset, oset;
    sigemptyset(&iset);
    sigemptyset(&oset);

    //对2号进行信号屏蔽
    sigaddset(&iset, 2);

    // 1. 设置当前进程的屏蔽字
    // 2. 获取当前进程老的屏蔽字
    sigprocmask(SIG_SETMASK, &iset, &oset);

    int count=0;
    sigset_t pending;
    while (1)
    {
        sigemptyset(&pending);
        sigpending(&pending);

        show_pending(&pending);

        sleep(1);
        count++;
        if(count==10)
        {
            sigprocmask(SIG_SETMASK,&oset,NULL);
            // 2号信号的动作是默认终止进程
            printf("恢复2号信号,可以被递达了");
        }
    }
    return 0;
}

Linux进程信号_第25张图片

四、信号发送后

4.1 信号发送后如何被处理

信号什么时候被处理?

因为信号是被保存在进程的PCB中的pending位图里面的,处理包括检测和递达(三个动作:默认、忽略、自定义)。

处理是发生在进程从内核态返回用户态时进行处理工作!

用户态:就是用户代码和数据被访问或者执行的时候所处的状态。我们自己写的代码全部都是在用户态执行的,它是一种受监管的状态!
内核态:执行OS的代码和数据时,计算机所处的状态就叫做内核态。OS的代码的执行全部都是在内核态,它的权限很高!
主要区别:在于权限!
用户调用系统函数后,除了进入函数,身份也会发生变化,会从用户身份变成内核身份

从底层学习用户态和内核态的切换

用户的代码和数据一定要被加载到内存中,OS的代码和数据也会被加载进入内存中,我们此处假设计算机中只有一个CPU,那么内核页表是被所有进程所共享的!!!这样设计的话,我们的进程A能够看到自己的代码和数据,同时也能够看到操作系统的代码和数据!进程具有了地址空间是能够看到用户和内核的所有内容的,但是不一定能够访问。

CPU内有寄存器保存了当前进程的状态
用户态使用的是用户级页表,只能访问用户数据和代码
内核态使用的是内核级页表,只能访问内核级的数据和代码

Linux进程信号_第26张图片
总结
1、进程之间无论如何切换,我们能够保证我们一定能够找到同一个OS,因为我们每个进程都有3~4G的地址空间,使用同一张内核页表
2、所谓的系统调用:就是进程的身份转化成为内核,然后根据内核页表找到系统函数,执行就行了
3、在大部分情况下,实际上我们的操作系统都是可以在进程的上下文中直接运行的

信号的捕捉处理
Linux进程信号_第27张图片

为了方便理解和记忆信号的处理过程,我们可以把模型简化成为无穷大的样子:
Linux进程信号_第28张图片

为什么一定要切换回用户态才能执行信号的捕捉?

理论上操作系统是能够直接执行用户的代码的!但是OS因为身份特殊,不能直接执行用户的代码。

4.2 信号处理函数sigaction

与signal函数差不多,它的主要功能是修改handler函数指针数组

函数名称 sigaction
函数功能 信号处理
头文件 #include
函数原型 int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
参数 signum:要处理的信号
act:信号处理函数
oldact:之前的信号处理函数
返回值 0:成功
-1:出错

看一看结构体内容:

struct sigaction {
    void     (*sa_handler)(int);
    void     (*sa_sigaction)(int, siginfo_t*, void*);//实时信号,我们不关心
    sigset_t   sa_mask;
    int        sa_flags;// 不用关心此选项,设置为0
    void     (*sa_restorer)(void);// 实时信号相关,也不用关心
};

在以上的类型定义中,sa_handler或者sa_sigaction用于接收信号处理函数,两者使用其一即可,当信号处理函数需要接收附加信息时,必须给sa_sigaction赋予信号处理函数指针,同时还要将sa_flags置为SA_SIGINFO,如果程序只需要接收信号,而不需要接收额外信息时,那将函数指针赋值给sa_handler即可。
sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数(handler)捕捉信号,或者说向内核注册了一个信号处理函 数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。

void handler(int signo)
{
    printf("get a signal:%d\n", signo);
}
// sigaction函数的使用
int main()
{
    struct sigaction act;
    memset(&act, 0, sizeof(act));

    act.sa_handler = handler;  // 自定义捕捉(去执行handler方法)
    // act.sa_handler=SIG_DFL; // 默认动作(终止进程)
    // act.sa_handler=SIG_IGN; // 忽略动作(什么都不做)

    // 本质是修改当前进程的handler函数指针数组的特定的内容
    sigaction(2, &act, NULL);

    while (1)
    {
        printf("hello world!\n");
        sleep(1);
    }
    return 0;
}

结果展示:
Linux进程信号_第29张图片

sa_mask

核心功能sa_mask表示在响应信号,进入信号处理函后,进程需要阻塞对于哪些信号的响应,可以将该信号添加到sa_mask信号集中去。
成员sa_mask用来定义在执行信号处理函数时要阻塞的信号集合,sa_mask成员类型为sigset_t,即信号集类型,需要使用信号操作集函数来操作它,它的每一位代表一种信号。

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

void handler(int signo)
{
    while(1){
    printf("get a signal:%d\n", signo);   
    sleep(1);     
    }
}
int main()
{
    struct sigaction act;
    memset(&act, 0, sizeof(act));
    act.sa_handler = handler;
    sigemptyset(&act.sa_mask);
    
    // 捕捉2号信号时,将3号信号屏蔽掉
    // 向sa_mask信号集中添加3号信号
    sigaddset(&act.sa_mask,3);
    
    // 本质是修改当前进程的handler函数指针数组的特定的内容
    sigaction(2, &act, NULL);

    while (1)
    {
        printf("hello world!\n");
        sleep(1);
    }
    return 0;
}

Linux进程信号_第30张图片

五、可重入函数

Linux进程信号_第31张图片
1、main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换到sighandler函数。
Linux进程信号_第32张图片

2、sighandler也调用insert函数向同一个链表head中插入节点node2

Linux进程信号_第33张图片
3、插入操作的两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后向链表中插入两个节点,而最后只有一个节点真正插入链表中了。
Linux进程信号_第34张图片
insert函数被不同的控制流调用,像这样的有可能在第一次调用还没返回时就再次进入该函数,我们将这种现象称之为重入函数。
Linux进程信号_第35张图片

不可被重入函数: insert函数一旦重入,有可能出现问题
可重入函数:insert函数一旦重入,不会出现问题

我们所学到的大部分函数,STL, boost库中的函数,大部分都是不可重入的!(这是一个中性词)

六、其他

6.1 volatile关键字

volatile的作用:保持内存的可见性!告诉编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中操作,读取必须贯穿式的读取内存,不要读取中间缓冲区中寄存器区中的数据!

#include 
#include 

int flag=0;

void handler(int signo)
{
    flag=1;
    printf("change flag 0 to 1\n");
}
int main()
{
    signal(2,handler);
    
    while(!flag);
    printf("this process quit normal!\n");
    return 0;
}

在这里插入图片描述

标准情况下,键入ctrl+c,2号信号被捕捉,执行自定义动作,修改flag=1,while循环条件不满足,退出循环,进程结束。

但是我们的编译器是可以有各种优化的:
Linux进程信号_第36张图片
我们这里使用-O3优化一下

test_volatile:test_volatile.c
	gcc -o $@ $^ -O3 //优化
.PHONY:clean
clean:
	rm -f test_volatile

Linux进程信号_第37张图片
优化情况下,键入ctrl+c,2号信号被捕捉,执行自定义动作,修改flag=1,但是while循环条件依旧满足,进程继续运行!但是很显然flag已经被修改了,这是什么原因呢?
编译器优化是不能够甄别代码中的多执行流的情况的,handler函数的执行流编译器是识别不到的,所以它只能识别在main执行流中,while循环只对flag做检测,它永远发现flag=0,编译器编译的时候就会将flag优化到CPU里面去,flag是一个变量,进程运行起来就要把flag加载进入内存,因为需要 !flag作逻辑运算,就需要CPU中的运算器,它检测到flag=0,循环永远继续。
我们优化完之后,就不会进行内存级别的访问了,而是直接访问CPU。
Linux进程信号_第38张图片

很显然,while循环检测的flag并不是内存中最新的flag,这就存在数据二义性的问题。(while检测的flag其实已经因为优化,被放在了CPU寄存器中),解决这个问题就需要我们的volatile关键字

volatile int flag=0;

Linux进程信号_第39张图片

6.2 SIGCHLD信号

进程那一章学过用waitwaitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻 塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不 能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一下,程序实现复杂。
其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程

#include 
#include 
#include 
#include
void GetChild(int signo)
{
    printf("get a signal:%d,pid:%d\n",signo,getpid());
}
int main()
{
    signal(SIGCHLD,GetChild);
    pid_t id=fork();
    if(id==0)
    {
        //child
        int cnt=5;
        while(cnt--)
        {
            printf("I am child process:%d\n",getpid());
            sleep(1);
        }
        exit(0);
    }

    //father
    while(1);
    return 0;
}

Linux进程信号_第40张图片
Linux进程信号_第41张图片

但是有时我们不想获取子进程的退出信息,父进程照常做自己的事情,我们可以用signalSIGCHLD信号的处理动作设置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用signal函数自定义的忽略通常没有区别。但这是一个特例,此方法只在Linux下可用,但不保证在其他平台下可行。

void GetChild(int signo)
{
    //waitpid();//可以在这里处理
    printf("get a signal:%d,pid:%d\n",signo,getpid());
}
int main()
{
    //signal(SIGCHLD,GetChild);
    signal(SIGCHLD,SIG_IGN);// 显示的忽略17号信号,当进程退出后,自动释放僵尸进程
    pid_t id=fork();
    if(id==0)
    {
        //child
        int cnt=5;
        while(cnt--)
        {
            printf("I am child process:%d\n",getpid());
            sleep(1);
        }
        exit(0);
    }

    //father
    while(1);
    return 0;
}

Linux进程信号_第42张图片

你可能感兴趣的:(Linux,linux,操作系统,信号,进程)