Linux进程间通信 - 信号(signal)机制

Linux进程间通信 - 信号(signal)机制


1 概述

Linux和类Linux系统下进程间通信(Inter-Process Communication, IPC)有很多种方式,包括套接字(socket),共享内存(shared memory),管道(pipe),消息队列(message queue)等[1],各自有各自的一些应用场景和用途,这次就来总结一下通过信号(signal)的机制。

信号,是Linux中向进程发送的消息,接收到该信号的进程会相应地采取一些行动,即通过软中断的方式来响应这个信号,触发一些事先指定或特定的事件。进程之间可以互相通过系统调用kill来发送信号,内核也可以因为内部事件而给进程发送信号,通知进程发生了某件事件[2]。

2 signal的系统api使用

2.1 响应信号

程序可以用signal库函数来处理信号,它的定义如下:

#include <signal.h>
void (*signal(int sig, void (*func)(int)))(int);

这个定义表示,signal是一个带有sig和func两个参数的函数,其中func是一个函数指针,指向的函数带有1个int类型参数且无返回值,signal函数返回值也是一个带有1个int类型参数且无返回值的函数指针,关于函数指针和函数形式的分析可以参考我的另一篇文章《C - 函数指针》。signal函数作用是绑定信号值为sig的信号的响应时间为func指向的函数,即当捕获到sig信号时,调用func指向的函数(可称为信号处理函数),另外func也可以用下面两个特殊值之一来代替信号处理函数:

    SIG_IGN    忽略信号
    SIG_DFL    恢复默认行为

编写一个示例程序C代码:

#include <stdio.h>

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

void laugh(int sig){
    printf("Haha... this time catch you, signal %d\n",sig);
    printf("We will restore to default.\n");
    signal(sig,SIG_DFL);
}

int main(){
    int sleep_sec = 1;
    signal(SIGINT,laugh);
    while(1){
        printf("waiting signal(%d seconds)... \n",sleep_sec);
        sleep(sleep_sec);
    }
    return 0;
}

编译运行:

waiting signal(1 seconds)... 
waiting signal(1 seconds)... 
waiting signal(1 seconds)... 
waiting signal(1 seconds)... 
^CHaha... this time catch you, signal 2
We will restore to default.
waiting signal(1 seconds)... 
waiting signal(1 seconds)... 
^C

可以发现,当我们按Ctrl+C时(Linux or Mac),操作系统会给该程序发送信号值为2的信号,其实是SIGINT信号,在main函数中,设定捕获SIGINT信号时,去执行laugh函数。正常情况下,程序进行不断的sleep和printf循环,当收到SIGINT信号时,会发生软件中断,保存现场,然后转而去执行信号处理函数,这里也就是laugh,执行完后,回到现场,继续执行中断时的后续代码。

在我们的laugh信号处理函数里,我们使用了SIG_DFL的值来进行重绑定,于是下一次我们通过键盘按Ctrl+C时,它就中断去执行默认的信号处理函数而不是laugh了,于是就退出程序了。

signal函数返回的是先前对指定信号进行处理的信号处理函数的函数指针,如果未定义信号处理函数,则返回SIG_ERR并设置errno为一个正数值,如果给出的是一个无效的信号,或者尝试处理的信号是不可捕获或不可忽略的信号(如SIGKILL),errno将被设置为EINVAL。

这里需要注意两点:

  1. 不同版本的UNIX/LINUX系统,对信号处理方式上有些不同,比如有的是执行完信号处理函数后,自动的将对应的信号的处理函数恢复到默认,而有的不是,所以这里我们希望恢复到默认,最好的方法就是自己写出恢复默认的代码;
  2. 信号的值,会因为系统的不同而不同,比如我们这个系统的SIGINT的值是2,可能其他的就不一定

信号的名称是在头文件signal.h中定义的,它们以SIG开头,比如下表:

信号名称 说明
SIGABORT *进程异常终止
SIGALRM 超时警告
SIGFPE *浮点运算异常
SIGHUP 连接挂断
SIGILL *非法指令
SIGINT 终端中断
SIGKILL 终止进程(此信号不能被捕获或忽略)
SIGPIPE 向无读进程的管道写数据
SIGQUIT 终端退出
SIGSEGV *无效内存段访问
SIGTERM 终止
SIGUSER1 用户定义信号1
SIGUSER2 用户定义信号2

其中带*的信号,系统对该信号的响应视具体实现而定。如果进程接收到上表信号中的任何一个,但事先没有安排捕获它,进程将会立刻终止。通常,系统将会生成core文件,放在当前目录下,该文件是进程在内存中的映像,对程序调试很有用处。当然,系统还有很多其他的信号,比如后文所述,可以通过kill -l来查看可发送的信号。

2.2 发送信号

进程可以通过调用kill函数向包括它本身在内的其他进程发送一个信号。如果程序没有权限,kill函数会调用失败,失败的常见原因是目标进程由另一个用户所拥有,这个函数跟shell同名命令kill的功能完全一样,定义如下:

#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);

kill函数中,pid表示要发送信号到达的目标进程的进程id,sig为发送的信号值。

成功时,返回0。失败时,返回-1,并设置 errno变量,errno值包括以下情况:

    errno = EINVAL     给定的信号无效
    errno = EPERM      发送进程权限不够
    errno = ESRCH      目标进程不存在

Linux信号机制向我们提供了一个有用的闹钟功能,进程可以通过调用alarm函数在经过预定时间后发送一个SIGALRM信号:

#include <unistd.h>

unsigned int alarm(unsigned int seconds);

alarm函数用来在seconds秒之后安排发送一个SIGALRM信号,但由于处理的延时和时间调度的不确定性,实际闹钟时间比预先安排的要稍微拖后一点儿。如果把参数设置为0,则取消所有已设置的闹钟请求。

还有另外一个有用的信号函数:

#include <unistd.h>

int pause(void);

它的作用很简单,就是把程序的执行挂起直到有一个信号出现为止,才会继续运行它下面的代码。
这个函数很有用,因为有时我们需要等待某个信号的发生,使用它便意味着程序不需要总是在执行,浪费CPU资源,对系统性能造成极大的影响。

对于信号机制的使用值得一提的是:如果信号出现在系统调用的执行过程中,可能有些系统调用会失败,大多数情况是一些比较慢得调用,比如从终端读数据,如果在这个系统调用等待数据时出现一个信号,它就会返回一个错误。工程实践中使用时,要十分注意和周全考虑。

其实上面介绍的传统UNIX编程中的signal和其相关函数,X/Open和UNIX规范推荐了一个更新更健壮的信号编程接口: sigaction,定义如下:

#include <signal.h>

int sigaction(int sig, const struct sigaction* act, struct sigaction *oact);

3 Linux系统命令使用

想要给一个进程发送信号,而该进程并不是当前的前台进程,就需要使用kill命令[4],它需要一个可选的信号代码,和一个进程ID,例如给PID为520的进程发送挂断信号(SIGHUP),使用如下命令:

    命令格式:kill [参数] [进程号]
    例如:
    kill -HUP 520

如果没有指定信号代码或值,则默认情况下,采用编号为15的TERM信号。TERM信号将终止所有不能捕获该信号的进程。对于那些可以捕获该信号的进程就要用编号为9的kill信号,强行“杀掉”该进程。

    命令参数:
    -l  信号,若果不加信号的编号参数,则使用“-l”参数会列出全部的信号名称
    -a  当处理当前进程时,不限制命令名和进程号的对应关系
    -p  指定kill 命令只打印相关进程的进程号,而不发送任何信号
    -s  指定发送信号
    -u  指定用户

比如可以使用-l参数列出所有支持的信号和他们的值:

$ kill -l
 1) SIGHUP   2) SIGINT   3) SIGQUIT  4) SIGILL
 5) SIGTRAP  6) SIGABRT  7) SIGEMT   8) SIGFPE
 9) SIGKILL 10) SIGBUS  11) SIGSEGV 12) SIGSYS
13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGURG
17) SIGSTOP 18) SIGTSTP 19) SIGCONT 20) SIGCHLD
21) SIGTTIN 22) SIGTTOU 23) SIGIO   24) SIGXCPU
25) SIGXFSZ 26) SIGVTALRM   27) SIGPROF 28) SIGWINCH
29) SIGINFO 30) SIGUSR1 31) SIGUSR2

$ kill -l SIGKILL
9
$ kill -l SIGTERM
15

kill命令有一个变体,即killall,它可以给运行着某一命令的所有进程发送信号,一般的Linux系统都会有这个命令,如果不知道某进程的pid,或者想给执行相同命名的许多不同的进程发送信号,这条命令就很有用了,一重常用的做法是,通知inetd程序重新读取它的配置选项[1]:

    killall -HUP inetd

参考文献

[1] Linux程序设计(第4版)

[2] Linux信号(signal)机制分析, http://www.cnblogs.com/hoys/archive/2012/08/19/2646377.html

[3] C - 函数指针,http://blog.csdn.net/junyucsdn/article/details/50519347

[4] 每天一个linux命令(42):kill命令,http://www.cnblogs.com/peida/archive/2012/12/20/2825837.html

展开阅读全文

Git 实用技巧

11-24
这几年越来越多的开发团队使用了Git,掌握Git的使用已经越来越重要,已经是一个开发者必备的一项技能;但很多人在刚开始学习Git的时候会遇到很多疑问,比如之前使用过SVN的开发者想不通Git提交代码为什么需要先commit然后再去push,而不是一条命令一次性搞定; 更多的开发者对Git已经入门,不过在遇到一些代码冲突、需要恢复Git代码时候就不知所措,这个时候哪些对 Git掌握得比较好的少数人,就像团队中的神一样,在队友遇到 Git 相关的问题的时候用各种流利的操作来帮助队友于水火。 我去年刚加入新团队,发现一些同事对Git的常规操作没太大问题,但对Git的理解还是比较生疏,比如说分支和分支之间的关联关系、合并代码时候的冲突解决、提交代码前未拉取新代码导致冲突问题的处理等,我在协助处理这些问题的时候也记录各种问题的解决办法,希望整理后通过教程帮助到更多对Git操作进阶的开发者。 本期教程学习方法分为“掌握基础——稳步进阶——熟悉协作”三个层次。从掌握基础的 Git的推送和拉取开始,以案例进行演示,分析每一个步骤的操作方式和原理,从理解Git 工具的操作到学会代码存储结构、演示不同场景下Git遇到问题的不同处理方案。循序渐进让同学们掌握Git工具在团队协作中的整体协作流程。 在教程中会通过大量案例进行分析,案例会模拟在工作中遇到的问题,从最基础的代码提交和拉取、代码冲突解决、代码仓库的数据维护、Git服务端搭建等。为了让同学们容易理解,对Git简单易懂,文章中详细记录了详细的操作步骤,提供大量演示截图和解析。在教程的最后部分,会从提升团队整体效率的角度对Git工具进行讲解,包括规范操作、Gitlab的搭建、钩子事件的应用等。 为了让同学们可以利用碎片化时间来灵活学习,在教程文章中大程度降低了上下文的依赖,让大家可以在工作之余进行学习与实战,并同时掌握里面涉及的Git不常见操作的相关知识,理解Git工具在工作遇到的问题解决思路和方法,相信一定会对大家的前端技能进阶大有帮助。
©️2020 CSDN 皮肤主题: 大白 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值