Linux学习_信号

        首先,要明确信号信号量两者没有任何关系。信号:Linux系统提供的让用户(进程)给其它进程发送异步信息的一种方式,属于软中断。信号量属于进程间通信。在进程运行期间,信号可以分为如下阶段:信号的产生、信号的保存、信号的处理。

1. 信号的产生

        我们可以使用 kill -l 的指令查看系统中的信号列表,这些信号本质上就是宏,宏的定义可以在signal.h 中找到,通过 man 7 signal 可以查看每个信号的具体含义,其中编号34以上的是实时信号,暂不讨论:

1.1 按键产生信号

        Ctrl+C 产生的SIGINT的默认处理动作是终止进程, Ctrl+\ 产生的SIGQUIT的默认处理动作是终止进程并且Core Dump(云服务器系统中默认关闭),Ctrl+Z 产生的SIGTSTP的默认处理动作是暂停进程。对于按键输入的信息是由键盘驱动操作系统联合解释的,而操作系统是通过硬件中断技术知道键盘在输入数据。

1.2 系统调用产生信号

1. kill 函数
        kill 命令是调用kill函数实现的,kill函数可以给一个指定的进程发送指定的信号。

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
成功返回0,错误返回-1

2. raise 函数
        raise 函数可以给当前进程发送指定的信号。(类似与 kill(getpid(),信号) )

#include <signal.h>
int raise(int sig);
成功返回0,错误返回-1

3. abort 函数
        abort 函数使当前进程接收到指定的 SIGABRT 信号而异常终止。(类似与 kill(getpid(),6) )

#include <stdlib.h>
void abort(void);
类似exit函数,abort函数总是会成功的,所以没有返回值。

1.3 软件条件产生信号

1. SIGPIPE 
        
是一种由软件条件产生的信号,当管道文件读写条件不满足时会产生。
2. SIGALRM

#include <unistd.h>
unsigned int alarm(unsigned int seconds);
        调用alarm 函数可以设定一个闹钟 , 也就是告诉内核在 seconds 秒之后给当前进程发 SIGALRM 信号 , 该信号的默认处理动作是终止当前进程。这个函数的返回值 0或者是以前设定的闹钟时间还余下的秒数。如果 seconds 值为 0, 表示取消以前设定的闹钟 , 函数的返回值仍然是以前设定的闹钟时间还余下的秒数。

1.4 硬件异常产生信号

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

1.5 Core Dump

        status在学习进程等待的时候,我们了解到wait和waitpid都有一个共同的参数status,它是子进程的退出状态信息。这里的core dump标志进程是否接收到信号,默认为0,所以信号没有0信号.

        在信号中,我们可以看到一些信号的默认操作是核心转储Core,但在云服务器上,Core和Term都是终止进程。是因为为了防止未知的core dump 一直进行,导致服务器磁盘被打满,所以云服务器默认将core 退出,进行特定处理,默认core是关闭的。

        我们可以通过如下指令查看到core是被关闭的:

        可以通过如下指令打开core功能:

        打开之后,当进程接受到某些默认处理是Core的信号终止后,会把该进程在内存中的与调试有关的核心数据转储到磁盘中形成core、core.pid的文件,方便我们事后进行调试。

2. 信号的保存

2.1 相关概念

        首先要明确以下概念:
        实际执行信号的处理动作称为信号递达(Delivery)。
        信号从产生到递达之间的状态, 称为信号未决 (Pending)
        进程可以选择阻塞 (Block ) 某个信号。
        被阻塞的信号产生时将保持在未决状态, 直到进程解除对此信号的阻塞 , 才执行递达的动作。
         阻塞忽略是不同的 , 只要信号被阻塞就不会递达 , 忽略是在递达之后可选的一种处理动作

2.2 在内核中的表示

        信号在内核中的表示示意图:
        每个信号都有两个标志位分别表示阻塞(block) 和未决 (pending), 还有一个函数指针表示处理动作。信号产生时, 内核在进程控制块中设置该信号的未决标志 , 直到信号递达才清除该标志。在上图的例子中,SIGHUP 信号未阻塞也未产生过 , 当它递达时执行默认处理动作。
        SIGINT 信号产生过 , 但正在被阻塞 , 所以暂时不能递达。虽然它的处理动作是忽略 , 但在没有解除阻塞之前不能忽略这个信号, 因为进程仍有机会改变处理动作之后再解除阻塞。
        SIGQUIT 信号未产生过 , 一旦产生 SIGQUIT 信号将被阻塞 ,它的处理动作是sighandler( 用户自定义函数)。
        如果在进程解除对某信号的阻塞之前这种信号产生过多次,在 Linux中 : 常规信号在递达之前产生多次只计一次 , 实时信号在递达之前产生多次可以依次放在一个队列里

2.3 sigset_t

        未决阻塞标志可以用相同的数据类型sigset_t 来存储 ,sigset_t 称为信号集 , 这个类型可以表示每个信号的“ 有效 无效 状态:
        在阻塞信号集 有效 无效 的含义是该信号是否被阻塞
        在未决信号集 有效” 无效 的含义是该信号是否处于未决状态
        所以操作系统向进程发送信号,本质上是写入信号,把对应标志位置为1/0。

2.4 信号集操作函数

2.4.1 操作sigset_t类型的函数

        sigset_t  类型对于每种信号用一个 bit 表示 有效 无效 状态 , 至于这个类型内部如何存储这些 bit 则依赖于系统实现, 从使用者的角度是不必关心的 , 使用者只能调用以下函数来操作 sigset_t 变量 ,而不应该对它的内部数据做任何解释。即用户不能直接对直接操作信号这种内核数据结构,只能通过操作 sigset_t 类型数据来间接影响内部的信号。
#include <signal.h>
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);

成功返回0,出错返回-1
        函数sigemptyset  初始化 set 所指向的信号集 , 使其中所有信号的对应 bit 清零 , 表示该信号集不包含任何有效信号。
        函数sigfillset 初始化 set 所指向的信号集 , 使其中所有信号的对应 bit 置1 , 表示该信号集的有效信号包括系统支持的所有信号。
        在使用sigset_t 类型的变量之前 , 一定要调用 sigemptyset  sigfillset  做初始化 , 使信号集处于确定的状态。
        初始化sigset_t变量之后就可以在调用 sigaddset  sigdelset  在该信号集中添加删除某种有效信 号。
        sigismember 是一个布尔函数,用来判断某个信号是否在一个有效信号集中,若存在就返回1,不存在就返回0,出错返回-1。

2.4.2 sigprocmask

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset); 
返回值:若成功则为0,若出错则为-1

        调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集):
        如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。
        如果
set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。
        如果oset
set都是非空指针,则先将原来的信号屏蔽字备份到oset,然后根据sethow参数更改信号屏蔽字。
        假设当前的信号屏蔽字为mask,
下表说明了how参数的可选值。

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

2.4.3 sigpending

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

3. 信号的处理

3.1 内核对信号的捕捉

        首先,我们要明确信号就是通过软件的方式,来模拟硬件中断。通过高频率、不间断的向cpu发送中断,cpu就会不断地处理中断。cpu根据中断向量表,就会去执行对应的功能(比如操作系统、响应键盘),所以操作系统就会一直执行。
        每个进程都有自己的PCB和进程空间如下。可以看到除了常用的用户空间,还有内核空间。这两个空间都有一张对应的页面来映射物理地址空间,分别是用户级页表内核级页表。要知道我们访问OS,本质就是通过进程的内核空间来访问的,这也是为什么无论进程怎么切换,总能找到OS。
        当CPU处理进程的时候,因为内核数据不暴露给外面,所以需要区分是用户级别还是内核级别,从而访问不同级别的页表,在CPU中就有了CS寄存器中存放了权限标识(0:内核态 3:用户态),CR3寄存器存放对应级别页表的物理地址(关于这两个寄存器不做详述)。所以这时就必须要区分当前用户的运行模式用户态内核态

        信号的处理过程中,一共有4次的状态切换(用户态与内核态)。如图是信号的捕捉流程:

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

3.2 sigaction  

#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
调用成功则返回0,出错则返回- 1
        signo  是指定信号的编号, sigaction  函数可以读取和修改与指定信号相关联的处理动作
act oact 指向 sigaction 结构体
        若 act 指针非空 , 则根据 act 修改该信号的处理动作
        若
oact 指针非 空 , 则通过 oact 传出该信号原来的处理动作
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参数的赋值:
        赋值为常数 SIG_IGN 传给 sigaction 表示忽略信号
        赋值为常数 SIG_DFL 表示执行系统默认动作
        赋值为一个函数指针表示用自定义函数捕捉信号 , 或者说向内核注册了一个信号处理函 数 , 该函数返回值为void, 可以带一个 int 参数 , 通过参数可以得知当前信号的编号
        当某个信号的处理函数被调用时, 内核自动将当前信号加入进程的信号屏蔽字 , 当信号处理函数返回时自动恢复原来的信号屏蔽字, 这样就保证了在处理某个信号时 , 如果这种信号再次产生 , 那么 它会被阻塞到当前处理结束为止。
        如果在调用信号处理函数时,除了当前信号被自动屏蔽之外 , 还希望自动屏蔽另外一些信号 , 则用 sa_mask 字段说明这些需要额外屏蔽的信号, 当信号处理函数返回时自动恢复原来的信号屏蔽字。
        sa_flags字段包含一些选项 ,本文把sa_flags 设为 0,sa_sigaction 实时信号的处理函数。

4. 可重入函数

        main函数调用 insert 函数向一个链表 head 中插入节点 node1, 插入操作分为两步, 刚做完第一步的时候 因为硬件中断使进程切换到内核, 再次回用户态之前检查到有信号待处理 于是切换到 sighandler 函数,sighandler 也调用 insert 函数向同一个链表 head 中插入节点 node2, 插入操作的两步都做完之后从sighandler返回内核态 再次回到用户态就从 main 函数调用的 insert 函数中继续往下执行 , 先前做第一步之后被打断, 现在继续做完第二步。结果是 ,main 函数和 sighandler 先后向链表中插入两个节点 而最后只有一个节点真正插入链表中了。
        像上例这样,insert 函数被不同的控制流程调用 有可能在第一次调用还没返回时就再次进入该函数, 这称为重入,insert 函数访问一个全局链表 有可能因为重入而造成错乱 像这样的函数称为不可重入函数 反之,如果一个函数只访问自己的局部变量参数, 则称为可重入函数
        如果一个函数符合以下条件之一则是不可重入的:
                调用了malloc free 因为 malloc 也是用全局链表来管理堆的
                调用了标准I/O库函数,标准 I/O 库的很多实现都以不可重入的方式使用全局数据结构

5. volatile

        作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量 的任何操作,都必须在真实的内存中进行操作。
        下面我们通过代码,来观察
volatile在信号中的作用:

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

int flag = 0;
//volatile int flag = 0;

void handler(int sig)
{
    (void)sig;
    printf("chage flag 0 to 1\n");
    flag = 1;
}

int main()
{
    signal(2, handler);
    while (!flag);
    printf("process quit normal\n");
    return 0;
}

        标准情况下,当按下Ctrl+C时2号信号被捕捉,执行handler 函数,从而导致while循环退出。但现在我们可以在gcc编译时加上参数强制编译器优化,使用man gcc 可以看到,默认优化级别是0。优化逻辑是:while循环中没有对flag进行操作,为了提高效率,每次对flag进行逻辑运算时,不再从内存中读取,而是直接在cpu的相关寄存器中读取。这时,我们在handler 函数中对flag的修改是对内存中的flag修改,寄存器中的flag还是0,存在数据二义性,所以按下Ctrl+C也不能终止循环。

        而当我们使用volatile后,强制编译器读取flag时,从内存中读取,就可以解决上面的问题。

6. SIGCHLD信号

        用 wait waitpid 函数清理僵尸进程时, 父进程可以阻塞等待子进程结束 也可以非阻塞地查询是否有子进程结束等待清理( 也就是轮询的方式 ) 。采用第一种方式 父进程阻塞了就不能处理自己的工作了; 采用第二种方式 ,父进程在处理自己的工作的同时还要记得时不时地轮询一 下,程序实现复杂。
        
其实, 子进程在终止时会给父进程发 SIGCHLD 信号, 该信号的默认处理动作是忽略, 父进程可以自定义 SIGCHLD 信号 的处理函数 这样父进程只需专心处理自己的工作 不必关心子进程了 , 子进程终止时会通知父进程 父进程在信号处理 函数中调用 wait 清理子进程即可。
        要想不产生僵尸进程还有另外一种办法; 父进程调用 sigaction SIGCHLD 的处理动作置为SIG_IGN 这样 fork 出来的子进程在终止时会自动清理掉 不会产生僵尸进程 也不会通知父进程。系统默认的忽略动作和用户用sigaction 函数自定义的忽略通常是没有区别的 但这是一个特例。此方法对于 Linux 可用,对于其它Unix系统不保证可行。
        下面代码实现: 父进程 fork 出子进程 子进程调用 exit(2) 终止 父进程自定义 SIGCHLD 信号的处理函数, 在其中调用 wait 获得子进程的退出状态并打印。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void handler(int sig)
{
    pid_t id;
    while ((id = waitpid(-1, NULL, WNOHANG)) > 0)
    {
        printf("wait child success: %d\n", id);
    }
    printf("child is quit! %d\n", getpid());
}
int main()
{
    signal(SIGCHLD, handler);
    pid_t cid;
    if ((cid = fork()) == 0)
    { // child
        printf("child : %d\n", getpid());
        sleep(3);
        exit(1);
    }
    while (1)
    {
        printf("father proc is doing some thing!\n");
        sleep(1);
    }
    return 0;
}

  • 8
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值