首页 > 嵌入式 > 系统编程 > 进程间通信-信号
2018
03-07

进程间通信-信号

进程间通信概述

进程间通信(IPC:Inter Processes Communication):进程是一个独立的资源分配单元,不同进程之间的资源是独立的,没有关联,不能在一个进程直接访问另一个进程的资源,比如一片内存,或是打开的文件描述符。但进程不是孤立的,不同进程需要进行信息的交互和状态的传递,因此需要进程间通信。

进程间通信的功能:

  • 数据传输:一个进程需要将它的数据发送给另一个进程。
  • 资源共享:多个进程之间共享同样的资源。
  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知他们发生了某种事件。
  • 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有操作,并能够及时知道它的状态改变。
    linux进程间通信(IPC)由以下这几个部分发展而来:

    1. 最初的UNIX进程间通信
    2. System V进程间通信
    3. POSIX进程间通信
    4. Socket进程间通信
    5. Linux把优势都继承了下来并形成了自己的IPC

linux操作系统支持的主要进程间通信的通信机制:

进程间通信-信号 - 第1张  | rs232的博客

信号

概述

信号是UNIX和Linux系统响应某些条件而产生的一个事件。它是在软件层次上对中断机制的一种模拟。信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。

信号通常是由于某些错误条件而生成的,如内存冲突、浮点错误、非法指令等。一般由shell和终端处理器生成中断。信号还可以作为在进程间传递消息或修改行为的一种方式,明确地由一个进程发送给另外一个进程,进程也可以给自己本身发送信号。

信号是一种异步通信方式。进程不必等待信号的到达,进程也不知道信号什么时候到达。信号可以直接进行用户空间进程和内核空间进程的交互,内核进程可以利用它来通知用户空间发生了哪些系统事件。

Linux系统定义了一系列的信号,每个信号的名字都以字符SIG开头,每个信号和一个数字编码相对应,在头文件signum.h中定义,这些信号都被定义为正整数。信号名定义路径:/usr/include/i386-linux-gnu/bits/signum.h。在linux下,要想查看信号和编码的对应关系,可以使用命令:kill -l

进程间通信-信号 - 第2张  | rs232的博客

部分信号说明:

进程间通信-信号 - 第3张  | rs232的博客

信号的基本操作

以下条件可以产生一个信号:

  • 当用户按下某些终端键时,将对前台程序(即正在运行的程序)产生信号,例如:
    • Ctrl + c:中断信号SIGINT
    • Ctrl + : 终端退出信号SIGQUIT
    • Ctrl + z: 终端挂起信号SIGSTOP
  • 硬件异常
    除数为0,无效的内存访问等。这些情况通常由硬件检测到,并通知内核,然后内核产生适当的信号发送给相应的进程。
  • 软件异常
    当检测到某种软件条件已发生,并将其通知进程时,产生信号,比如子进程退出信号。
  • 调用kill函数
    调用kill函数可以发送信号,此时,要求发送信号的进程和接收信号的用户必须相同,或发送信号进程的所有者必须是超级用户。
  • 运行kill命令
    通常用此命令来终止一个失控的后台进程,进程的进程号需要由ps命令获得。此程序实际上是使用kill函数来发送信号。kill有一个变体叫killall,用于给运行某一命令的所有进程发送信号。常用于在不知道某个进程的PID,但是知道进程的名称时对进程发送信号。
kill -9 PID #对进程号为PID的进程发送编号为9的信号(SIGKILL)
kill -KILL PID #同上
killall -9 process #对进程名称为process的进程发送SIGKILL信号

一个进程收到一个信号的时候,有如下几种处理办法:

  • 执行默认动作:对于大多数信号来说,系统默认动作是终止该进程。
  • 忽略此信号:接收到此信号后没有任何动作。
  • 执行用户自定义的信号处理函数。

注意:SIGKILL和SIGSTOP不能更改信号的处理方式,因为它们向用户提供了一种使进程终止的可靠方法。

信号编程

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

kill函数

int kill(pid_t pid, int signum);

功能:给指定进程发送信号
返回值:成功返回0,失败返回-1
参数:
pid的取值有4种情况:

  • pid > 0:将信号传送给进程ID为pid的进程。
  • pid = 0:将信号传送给当前进程所在进程组中所有的进程。
  • pid = -1:将信号传送给系统内所有进程。
  • pid < -1:将信号传给指定进程组的所有进程。这个进程组号等于pid的绝对值。
    signum是要发送信号的编号。

示例:kill1.c

#include "common.h"
#include <signal.h>
int main(void)
{
pid_t pid;
pid = fork();
if(pid < 0)
err_sys("fork error");
else if(pid == 0)
{
sleep(1);
printf("child processn");
}
else
{
printf("parent processn");
kill(pid, SIGINT);
}
return 0;
}

raise函数

int raise(int signum);
功能:
给调用进程本身发送一个信号;
参数:
signum:信号编号
返回值:
成功返回0,失败返回-1

示例:raise.c

#include "common.h"
#include <signal.h>
int main(void)
{
printf("startn");
sleep(2);
raise(SIGALRM);
sleep(10);
return 0;
}

alarm函数

#include <unistd.h>
unsigned int alarm(unsigned int seconds);

功能:
在seconds秒后向调用程序发送一个SIGALRM信号,SIGALRM信号的默认动作是终止调用alarm函数的进程。如果在接收到SIGALRM信号之前再次调用alarm函数,则闹钟重新计时。
参数:
需要延时的秒数,设为0时将取消所有已设置的闹钟请求。
返回值:
若以前没有设置过定义器,或设置的定时器已超时,返回0;否则返回定时器剩余的秒数,并重新设置定时器。

示例:alarm1.c

#include "common.h"
int main(void)
{
int seconds = 0;
seconds = alarm(5);
printf("seconds = %dn", seconds);
sleep(2);
seconds = alarm(5);
printf("seconds = %dn", seconds);
while(1);
return 0;
}

abort函数

void abort(void);

功能:
向进程发送一个SIGABRT信号,默认情况下进程会退出。
注意:
即使SIGABRT信号被加入阻塞集,一旦进程调用了abort函数,进程也还是会被终止,且在终止前会刷新缓冲区,关文件描述符。

pause函数

int pause(void);

功能:将调用进程挂起直至捕捉到信号为止。这个函数通常用于判断信号是否已到。
返回值:直到捕获到信号,pause函数才返回-1,且errno被设置成EINTER。

signal函数

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

功能:注册指定信号的处理函数,可以多次调用signal对不同的信号分别注册处理函数,也可以多次调用signal对同一信号注册不同的处理函数。
参数:
signum:信号编号
handler:信号处理函数的函数指针,设置为SIG_IGN表示忽略信号,SIG_DFL表示恢复该信号的默认行为。
返回值:返回函数地址,该地址为这个信号上一次注册的信号处理函数的地址。
注意:
信号处理函数要注意可重入性,即使信号处理函数都是可重入函数,也要注意在进入处理函数时,首先保存errno的值,结束时,再恢复原值。因为信号处理过程中,errno的值随时可能被改变。

示例:signal1.c

#include "common.h"
#include <signal.h>
void signal_handler1(int signo)
{
printf("recv SIGINTn");
}
void signal_handler2(int signo)
{
printf("recv SIGQUITn");
}
int main(void)
{
signal(SIGINT, signal_handler1);
signal(SIGQUIT, signal_handler2);
while(1)
{
printf("hello, worldn");
sleep(1);
}
}

sigaction函数

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

功能:设置与信号signum关联的处理动作。
参数:
signum:需要处理的信号
act:定义在接收到signum信号后该采取的行动。
oldact:如果oldact不为空,则sigaction会将原来对该信号的处理动作写到它指向的位置,相当于获取旧有的信号处理配置。
返回值:
成功返回0,失败返回-1。

这个是UNIX规范推荐的一个更新和更健壮的信号编程接口(当然更复杂!)。其中,表示信号的处理动作不再是简单的一个函数指针,而是一个struct sigaction结构体,该结构体至少包含以下三项成员:

  • void (*)(int) sa_handler——信号处理函数或SIG_DFL或SIG_IGN
  • sigset_t sa_mask——信号阻塞集合,表示进程在执行信号处理时需要阻塞的信号,从信号处理函数返回时,这些被阻塞的信号将恢复为原先值。
  • int sa_flags——信号处理的行为

sa_mask成员指定了一个信号集合,在调用sa_handler所指向的信号处理函数之前,该信号集合所包含的信号会加入到进程的信号阻塞集(也称为信号屏蔽字)中,这些信号会在处理函数执行期间被进程所阻塞,从而防止处理函数还未运行结束时就被接收到的情况。
每个进程都有一个阻塞集,它用来描述哪些信号递送到该进程的时候被阻塞(在信号发生时记住它,直到进程准备好时再将信号通知进程)。所谓阻塞并不是禁止传送信号,而是暂缓信号的传送。若将被阻塞的信号从信号阻塞集中删除,且对应的信号在阻塞时发生了,进程将会收到对应的信号。
sa_flags指定了信号的处理行为,它是一系列的标志位,可以取以下值:

进程间通信-信号 - 第4张  | rs232的博客

信号集

信号集的思路是,一个进程往往需要对多个信号做出处理,为了方便处理多个信号,引入了信号集的概念。信号集用是用来表示多个信号的数据类型,在linux系统中被定义为sigset_t类型。不需要关心sigset_t的类型,只需要注意和它相关的一系列接口就可以了。

#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
int sigismember(const sigset_t *set, int signum);

函数的功能和函数的名称一样,sigemptyset将信号集初始化为空,sigfillset将信号集初始化为包含所有的已定义信号。sigaddset和sigdelset从信号集中增加或删除给定的信号。sigismember判断一个信号是否是一个信号集的成员。它们在成功时返回0,失败时返回-1并设置errno。

sigprocmask函数

每个进程都有一组进程阻塞集,表示当前被阻塞的一组信号,它们不能被当前进程接收到。可以通过sigprocmask函数来修改进程的信号阻塞集。

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

功能:
检查或修改信号阻塞集,根据how指定的方法对进程的阻塞集合进行修改,新的信号阻塞集由set参数指定,而原来的信号阻塞集由参数oldset保存。
参数:

  • how:信号阻塞集的修改方法,可以取以下值
    SIG_BLOCK:向信号阻塞集中添加set信号集
    SIG_UNBLOCK:从信号阻塞集体中删除set集合
    SIG_SETMASK:将信号阻塞集设为set集合
  • set:要操作的信号集地址
  • oldset:保存原先信号集的地址
    返回值:
    执行成功返回0,如果参数how取值无效,它将返回-1并设置errno为EINVAL。

示例:sigmask.c

#include "common.h"
#include <signal.h>
int main(void)
{
int i = 0;
sigset_t set;
sigemptyset(&set);
sigprocmask(SIG_BLOCK, NULL, &set);/*获取进程的默认信号屏蔽字*/
sigaddset(&set, SIGINT);
sigprocmask(SIG_BLOCK, &set, NULL); /*设置屏蔽SIGINT信号*/
while(i++ < 5)
{
printf("hello worldn");
sleep(1);
}
sigemptyset(&set);
sigaddset(&set, SIGINT);
sigprocmask(SIG_UNBLOCK, &set, NULL);/*恢复进程的默认信号屏蔽字*/
while(1)
{
printf("hehehehehen");
sleep(1);
}
return 0;
}

段错误调试

段错误作为编程中一种非常常见又非常难处理的错误,需要多加小心,并且学会调试它的办法。下面介绍几种常见和不常见的手段,以便不时之需。

分析核心转储文件

核心转储(Core Dump)是指系统在程序崩溃时将程序的内存映像信息存储到磁盘上,以便于从这些信息中分析出程序崩溃的原因。生成的核心转储文件一般生成在当前路径下取名为core。但是系统默认是不会生成这个文件的,需要通过下列命令来开启这个选项。
生成核心转储文件命令:ulimit -c unlimited
在需要调试段错误时,一般在编译时需要加上-g选项,让可执行程序附加调试信息。

#include <stdio.h>
int func1(int *p){
int a = 1;
int b = a + 1;
int c = b + a;
*p = 1;
return 1;
}
int func2(int *p){
func1(p);
}
int func3(int *p){
func2(p);
}
int main(void)
{
int *p = NULL;
func3(p);
}
# gcc target.c -g -o target
# gdb target core

gdb调试程序

直接通过gdb调试程序,让程序在gdb中运行,然后打印段错误信息所在的下一行,或者通过backtrace命令查看栈回溯,找到出错的函数所在的位置。

注册段错误处理函数

通过sigaction函数注册一个段错误信号的处理函数,在该函数中通过sigaction获得段错误时返回的进程上下文信息进行栈回溯,把函数的调用过程打印出来,再通过反汇编源文件的方式,确定出错点的位置。参考sigsegv.c。

最后编辑:
作者:rs232
这个作者貌似有点懒,什么都没有留下。

留下一个回复

你的email不会被公开。