程序员成长之旅——进程信号

信号的概念

信号就是软中断。
信号提供异步处理事件的一种方式。例如:用户在终端按下结束进程键,使一个进程提前终止。
每一个信号都有一个名字,它们的名字都以SIG打头。例如,每当进程调用了abort函数时,都会产生一个SIGABRT信号。
每一个信号对应一个正整数,定义在头文件

信号产生的场景

  • 当用户在终端按下特定的键时,会产生信号。例如,当用户按下DELETE按键(或Control-C)时,会产生一个中断信号(interrupt signal,SIGINIT),该信号使得一个运行中的程序终止。
  • 硬件异常可以产生信号。会引发硬件异常的情况如除以0,非法内存引用(invalid memory reference)等。这种情况会被硬件检测到,并通知内核,然后内核产生相应的信号通知对应的运行进程。例如,当一个进程执行了一个非法的内存引用,会触发SIGSEGV信号。
  • kill函数允许当前进程向其他的进程或者进程组发送任意的信号。当然,这种方法存在限制:我们必须是信号接收进程的所有者,或者我们必须是超级用户(superuser)。
  • kill命令的作用和kill函数类似。这个命令多用户杀死后台进程。
  • 软件异常可以根据不同的条件产生不同的信号。例如:网络连接中接受的数据超出边界时,会触发SIGURG信号。

对于进程来说,信号是随机产生的,所以进程不能简单地根据检测某个变量是否改变来判断信号是否发生,而应该告诉内核“当这个信号发生时,做下面的这些事情”。

信号的处理

对于进程来说,不能判别是否出现一个信号,而是必须要告诉内核信号出现的时候,执行下列操作。
信号的处理方式有三种:

  1. 忽略此信号
  2. 执行信号的默认处理动作。
  3. 提供自定义行为,要求处理该信号的时候切换到用户态执行这个处理函数,也叫做捕捉一信号。

注:捕捉信号的时候需要注意不能捕捉SIGKILL信号和SIGSTOP信号。当捕捉到SIGCHLD信号,这个时候标识一个子进程已经终止,所以这个时候我们可以调用waitpid函数来取得该子进程的进程ID以及它的终止状态。

对于一些信号发生时,会造成进程终止,同时生成一个core文件,该core文件记录了该进程终止时的内存情况,可以帮助调试和调查进程的终止状态。

有几种情况不会生成core文件

  • 如果进程设置了suid位(chmod u+s file),并且当前用户不是程序文件的所有者;
  • 如果进程设置了guid位(set-group-ID),并且当前用户不是程序文件的组所有者;
  • 如果过户没有当前工作目录的写权限;
  • 如果core文件已经存在,并且用户没有该文件的写权限;
    该core文件太大(由参数RLIMIT_CORE限制)

产生信号

(1)终端产生信号
首先提出一个概念叫做 core dump,我想在linux下写c,肯定不少发现错误的时候报这个错误接下来我们先来看看这个东西到底是个什么。
core dump叫做核心转储,也叫做核心文件(core file),是操作系统在进程收到某些信号而终止运行时,将此时进程的地址空间的内容以及有关进程状态的其他信息写出的一个磁盘文件,这个信息我们常常用于调试程序。
默认的linux系统当中是不生成这个文件的,我们可以使用 ulimit -a 查看系统中这个文件的大小。
在这里插入图片描述
我们可以使用命令 ulimit -c xxxx 设置生成的core dump的大小。
在这里插入图片描述
默认情况下,生成的core dump文件的格式是core.xxx,后面一般都是pid。并且生成在当前目录下。
现在我们模拟生成一下这样的core dump文件,我们首先写出一个死循环。

int main()
{
    printf("hello world\n");
    while(1);

    return 0;
}

我们运行这个程序,然后操作,Ctrl+\,这样就会出现:
在这里插入图片描述
从上图我们可以看到我们操作过程中从键盘Ctrl+,这样就会产生一个信号SIGQUIT,这个信号传递给运行的进程,然后进程得到这个信号引发终止进程并引发核心转储。

接下来我们来看看如何利用这个coredump文件进行调试

我们直接gdb tese文件和core文件就好,在终端输入 gdb tese core.4622 得到:
在这里插入图片描述
可以很快定位到错误之处。
(2)通过系统调用产生信号
我们可以通过系统调用来产生信号。这里我们先来看一下kill函数。

int kill(pid_t pid, int sig);

kill函数可以给指定的进程发送信号。
这个函数当中一个参数是进程的pid,第二个参数是我们需要发送给pid进程的一个信号的序号,比如sig = 9 ,那我们就发送SIGKILL。

再来介绍一个raise函数

int raise(int sig);

这个函数是用来给当前进程发送信号的。
abort函数使得当前进程接收到信号而异常终止。

void abort(void);

这个函数会产生SIGABRT信号,这个信号是夭折信号。
(3)软件产生信号
软件产生信号这里我们首先来说一个函数alarm函数:

unsigned int alarm(unsigned int seconds);

里面的变量seconds所给的是一个时间,单位是秒。这个函数的意思就是类似闹钟的形式,alarm(1)的意思让操作系统在1秒钟以后结束这个进程alarm的默认行为动作就是终止这个进程。alarm函数的信号SIGALRM信号,这个信号的默认动作就是终止这个进程,当使用alarm(0)的意思就是取消以前设定的闹钟,返回值就是所剩余的时间。调用alarm函数会产生SIGALRM信号。
(4)硬件异常产生信号
硬件异常产生信号,这些条件由硬件检测并通知内核,然后内核向当前进程发送适当的信号,例如当前进程执行了除以0的指令,CPU运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给该进程,再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给该进程。

阻塞信号

(1)阻塞的概念
信号递达:正在执行信号处理的动作
信号产生与信号递达之间叫做信号未决,也叫pending。
当信号阻塞的时候不会递达,接触阻塞,信号才能递达。
关于信号,我们首先需要从内核的角度来看看信号。
在内核当中,当一个进程接收到信号,会对应的在进程的pcb当中有三个相关的结构
在这里插入图片描述
因为我们现在有31个普通信号,所以这个时候我们可以想下我们前期所说的位图,我们也就可以利用一个整形就够了,每一个信号对应一个比特位。

另外因为是bit位,所以这里注意,即使你产生了多个信号,这里的信号位也只是从0变为1,不记录信号产生了多少次。

pending表标识信号未决表,表示信号是否产生,block阻塞表,表示当前进程与信号屏蔽相关内容。我们也把阻塞信号集叫做当前进程的信号屏蔽字。

注意阻塞和忽略是两回事,阻塞只是屏蔽了信号,而忽略是对信号的一种处理方式。
(2)信号集相关的函数
在linux下信号我们定义成为sigset_t类型的,sigset_t我们叫做信号集,这种类型经过我的测试大小是128个字节。
信号集下面有一些函数。

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

这里的函数都放在signal.h当中,sigemptyset函数用来初始化set所指向的信号集,使得信号集所有信号的对应的bit位清空。
sigfillset函数标识对set所指向的信号集的所有位进行置位操作。
注意,使用信号集之前一定得先试用sigemptyset或者是sigfillset进行初始化信号集。
sigaddset是对set所指向的信号集进行进行添加一个信号signo。
sigdelset函数是对信号集进行删除有效的信号。
sigismember函数是用来判断是否在set所指向的信号集当中包含signo信号。

说完看这些函数我们再说一个和信号屏蔽字相关的函数,sigprocmask函数,

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

这个函数是用来进行读取或者修改进程的信号屏蔽字这里的how说的是如何进行更改,set指向你要修改的当前信号屏蔽字,oldset指向修改前你的信号屏蔽字。
在这里插入图片描述
注意:如果调用了sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少会将其中的一个信号递达。

接下来说另外的一个函数叫做sigpending,它用来输出pending表中的内容。

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

void printfspending(sigset_t *set)
{
    int i=0;
    for(i=0;i<32;i++)
    {
        if(sigismember(set,i))
        {
            printf("1");
        }
        else
        {
            printf("0");
        }
    }
    printf("\n");
}
int main()
{
    sigset_t set,oset;
    sigemptyset(&set);
    printfspending(&set);
    sigaddset(&set,SIGINT);
    sigprocmask(SIG_BLOCK,&set,NULL);
    while(1)
    {
        sigpending(&oset);
        printfspending(&oset);
        sleep(1);
    }

    return 0;
}

在这里插入图片描述

捕捉信号

先来提出一个函数就叫做sigaction函数,这个函数可以修改和信号相关联的动作,实现信号的捕捉。

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

struct sigaction的定义:

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

在这里插入图片描述
我们也可以使用signal函数可以实现这个功能。

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

它的第一个参数是信号的编号,第二个参数是指向自定义函数的指针,就是当你捕捉到这个信号,不让它去做它的默认操作,而是去做你想要让它做函数,这个参数是一个返回值为void,参数为int的一个函数指针。

signal是C标准库提供的信号处理函数,

接下来说一说信号捕捉的时候的状态转换:
在这里插入图片描述
从上面这张图就可以看出整个状态的转换,

1.首先当你遇到中断、异常或者系统调用的时候进入内核态。
2.然后产生信号,这样由内核态切换用户态,这个过程当中需要去PCB检查那三张表,然后发现有递达的信号,然后这个时候就去处理信号对应的操作。也就是信号处理函数。
3.处理信号处理函数的时候,这个时候为了安全的问题,这个时候为用户态。
4.信号处理函数结束后,然后从用户态切换到内核态。
5.然后由内核态切换到中断异常执行处的用户态。

所以总共有4次状态的切换。

可重入信号

可重入函数主要用于多任务环境中,一个可重入的函数简单来说就是可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,转入OS调度下去执行另外一段代码,而返回控制时不会出现什么错误;而不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等,所以它如果被中断的话,可能会出现问题,这类函数是不能运行在多任务环境下的。
注意事项
编写可重入函数时,若使用全局变量,则应通过关中断、信号量(即P、V操作)等手段对其加以保护。

若对所使用的全局变量不加以保护,则此函数就不具有可重入性,即当多个进程调用此函数时,很有可能使有关全局变量变为不可知状态。

如何将一个不可重入的函数改写成可重入的函数?

答:把一个不可重入函数变成可重入的唯一方法是用可重入规则来重写它。其实很简单,只要遵守了几条很容易理解的规则,那么写出来的函数就是可重入的。

  1. 不要使用全局变量。因为别的代码很可能覆盖这些变量值。

  2. 在和硬件发生交互的时候,切记执行类似disinterrupt()之类的操作,就是关闭硬件中断。完成交互记得打开中断,在有些系列上,这叫做"进入/退出核心"。

  3. 不能调用其它任何不可重入的函数。

  4. 谨慎使用堆栈。最好先在使用前先OS_ENTER_KERNAL。

堆栈操作涉及内存分配,稍不留神就会造成益出导致覆盖其他任务的数据,所以,请谨慎使用堆栈!最好别用!很多黑客程序就利用了这一点以便系统执行非法代码从而轻松获得系统控制权。还有一些规则,总之,时刻记住一句话:保证中断是安全的!
参考网址

SIGCHLD

最后我们来说一个信号,是SIGCHLD信号,这个信号是我们子进程终止的时候会给父进程传送这个信号。
SIGCHLD信号产生的条件:
1.子进程终止时
2.子进程收到SIGSTOP信号停止的时候。
3.子进程处在停止状态,接受到SIGCONT后唤醒。

父进程接收到了SIGCHLD信号,这个时候的默认动作是忽略,当然你可以去进行信号捕捉。我们能通过信号捕捉可以去处理其他。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

从零出发——

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

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

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

打赏作者

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

抵扣说明:

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

余额充值