Linux系统编程——进程间通信:信号中断处理

什么是信号?

信号是 Linux 进程间通信的最古老的方式。信号是软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式 。信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。


“中断”在我们生活中经常遇到,譬如,我正在房间里打游戏,突然送快递的来了,把正在玩游戏的我给“中断”了,我去签收快递( 处理中断 ),处理完成后,再继续玩我的游戏。这里我们学习的“信号”就是属于这么一种“中断”。我们在终端上敲“Ctrl+c”,就产生一个“中断”,相当于产生一个信号,接着就会处理这么一个“中断任务”(默认的处理方式为中断当前进程)。


信号可以直接进行用户空间进程和内核空间进程的交互,内核进程可以利用它来通知用户空间进程发生了哪些系统事件。


一个完整的信号周期包括三个部分:信号的产生,信号在进程中的注册,信号在进程中的注销,执行信号处理函数。如下图所示:


注意:这里信号的产生,注册,注销时信号的内部机制,而不是信号的函数实现。


Linux 可使用命令:kill -l("l" 为字母),查看相应的信号。



列表中,编号为 1 ~ 31 的信号为传统 UNIX 支持的信号,是不可靠信号(非实时的),编号为 32 ~ 63 的信号是后来扩充的,称做可靠信号(实时信号)。不可靠信号和可靠信号的区别在于前者不支持排队,可能会造成信号丢失,而后者不会。非可靠信号一般都有确定的用途及含义,  可靠信号则可以让用户自定义使用更多详情,请看《Linux信号列表》


信号的产生方式

1)当用户按某些终端键时,将产生信号。

终端上按“Ctrl+c”组合键通常产生中断信号 SIGINT,终端上按“Ctrl+\”键通常产生中断信号 SIGQUIT,终端上按“Ctrl+z”键通常产生中断信号 SIGSTOP 等。


2)硬件异常将产生信号。

除数为 0,无效的内存访问等。这些情况通常由硬件检测到,并通知内核,然后内核产生适当的信号发送给相应的进程。


3)软件异常将产生信号。

当检测到某种软件条件已发生,并将其通知有关进程时,产生信号。


4)调用 kill() 函数将发送信号。

注意:接收信号进程和发送信号进程的所有者必须相同,或发送信号进程的所有者必须是超级用户。


5)运行 kill 命令将发送信号。

此程序实际上是使用 kill 函数来发送信号。也常用此命令终止一个失控的后台进程。


信号的常用操作

发送信号

所需头文件:

#include <sys/types.h>

#include <signal.h>


int kill(pid_t pid, int signum);

功能:

给指定进程发送信号。

注意:使用 kill() 函数发送信号,接收信号进程和发送信号进程的所有者必须相同,或者发送信号进程的所有者是超级用户。

参数:

pid: 取值有 4 种情况:

pid > 0: 将信号传送给进程 ID 为pid的进程。

pid = 0: 将信号传送给当前进程所在进程组中的所有进程。

pid = -1: 将信号传送给系统内所有的进程。

pid < -1: 将信号传给指定进程组的所有进程。这个进程组号等于 pid 的绝对值。

signum: 信号的编号,这里可以填数字编号,也可以填信号的宏定义,可以通过命令 kill -l ("l" 为字母)进行相应查看。

返回值:

成功:0

失败:-1


下面为测试代码,本来父子进程各自每隔一秒打印一句话,3 秒后,父进程通过 kill() 函数给子进程发送一个中断信号 SIGINT( 2 号信号),最终,子进程结束,剩下父进程在打印信息:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>

int main(int argc, char *argv[])
{
	pid_t pid;
	int i = 0;

	pid = fork(); // 创建进程
	if( pid < 0 ){ // 出错
		perror("fork");
	}
	
	if(pid == 0){ // 子进程
		while(1){
			printf("I am son\n");
			sleep(1);
		}
	}else if(pid > 0){ // 父进程
		while(1){
			printf("I am father\n");
			sleep(1);
			
			i++;
			if(3 == i){// 3秒后
				kill(pid, SIGINT); // 给子进程 pid ,发送中断信号 SIGINT
				// kill(pid, 2); // 等级于kill(pid, SIGINT);
			}
		}
	}

	return 0;
}


运行结果如下:



等待信号

所需头文件:

#include <unistd.h>


int pause(void);

功能:

等待信号的到来(此函数会阻塞)。将调用进程挂起直至捕捉到信号为止,此函数通常用于判断信号是否已到。

参数

无。

返回值:

直到捕获到信号才返回 -1,且 errno 被设置成 EINTR。


测试代码如下:

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

int main(int argc, char *argv[])
{
	printf("in pause function\n");
	pause();
	
	return 0;
}


没有产生信号前,进程一直阻塞在 pause() 不会往下执行,假如,我们按“Ctrl+c”,pause() 会捕获到此信号,中断当前进程。


处理信号

一个进程收到一个信号的时候,可以用如下方法进行处理:

1)执行系统默认动作

对大多数信号来说,系统默认动作是用来终止该进程。


2)忽略此信号

接收到此信号后没有任何动作。


3)执行自定义信号处理函数

用用户定义的信号处理函数处理该信号。


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


产生一个信号,我们可以让其执行自定义信号处理函数。假如有函数 A, B, C,我们如何确定信号产生后只调用函数 A,而不是函数 B 或 C。这时候,我们需要一种规则规定,信号产生后就调用函数 A,就像交通规则一样,红灯走绿灯行,信号注册函数 signal() 就是做这样的事情


所需头文件:

#include <signal.h>


typedef void (*sighandler_t)(int);// 回调函数的声明
sighandler_t signal(int signum,sighandler_t handler);

功能:

注册信号处理函数(不可用于 SIGKILL、SIGSTOP 信号),即确定收到信号后处理函数的入口地址。此函数不会阻塞

参数:

signum:信号的编号,这里可以填数字编号,也可以填信号的宏定义,可以通过命令 kill -l ("l" 为字母)进行相应查看。

handler: 取值有 3 种情况:

SIG_IGN:忽略该信号
SIG_DFL:执行系统默认动作
信号处理函数名:自定义信号处理函数,如:fun

回调函数的定义如下:

void fun(int signo)

{

// signo 为触发的信号,为 signal() 第一个参数的值

}

注意信号处理函数应该为可重入函数,关于可重入函数的更多详情,请《浅谈可重入函数与不可重入函数》

返回值

成功:第一次返回 NULL,下一次返回此信号上一次注册的信号处理函数的地址。如果需要使用此返回值,必须在前面先声明此函数指针的类型

失败:返回 SIG_ERR


示例一:

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

// 信号处理函数
void signal_handler(int signo)
{
	if(signo == SIGINT){
		printf("recv SIGINT\n");
	}else if(signo == SIGQUIT){
		printf("recv SIGQUIT\n");
	}
}

int main(int argc, char *argv[])
{
	printf("wait for SIGINT OR SIGQUIT\n");
	
	/* SIGINT: Ctrl+c ; SIGQUIT: Ctrl+\ */
	// 信号注册函数
	signal(SIGINT, signal_handler);
	signal(SIGQUIT, signal_handler);
	
	// 等待信号
	pause();
	pause();
	
	return 0;
}

在终端里敲“Ctrl+c”或“Ctrl+\”,自动调用其指定好的回调函数 signal_handler():



示例二:

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

// 回调函数的声明
typedef void (*sighandler_t)(int);

void fun1(int signo)
{
	printf("in fun1\n");
}

void fun2(int signo)
{
	printf("in fun2\n");
}

int main(int argc, char *argv[])
{
	sighandler_t previous = NULL;
	
	// 第一次返回 NULL
	previous = signal(SIGINT,fun1); 
	if(previous == NULL)
	{
		printf("return fun addr is NULL\n");
	}
	
	// 下一次返回此信号上一次注册的信号处理函数的地址。
	previous = signal(SIGINT, fun2);
	if(previous == fun1)
	{
		printf("return fun addr is fun1\n");
	}
	
	// 还是返回 NULL,因为处理的信号变了
	previous = signal(SIGQUIT,fun1);
	if(previous == NULL)
	{
		printf("return fun addr is NULL\n");
	}
	
	return 0;
}

运行结果如下:



信号集与信号阻塞集

信号集

为了方便对多个信号进行处理,一个用户进程常常需要对多个信号做出处理,在 Linux 系统中引入了信号集(信号的集合)。这个信号集有点类似于我们的 QQ 群,一个个的信号相当于 QQ 群里的一个个好友


信号集是用来表示多个信号的数据类型(sigset_t),其定义路径为:/usr/include/i386-linux-gnu/bits/sigset.h。


信号集相关的操作主要有如下几个函数:

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

以上几个函数的用法都是比较简单,这里就不一一介绍,我们通过一个例子来学习其用法:

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

int main(int argc, char *argv[])
{
	sigset_t set;	// 定义一个信号集变量
	int ret = 0;

	sigemptyset(&set); // 清空信号集的内容
	
	// 判断 SIGINT 是否在信号集 set 里
	// 在返回 1, 不在返回 0
	ret = sigismember(&set, SIGINT);
	if(ret == 0){
		printf("SIGINT is not a member of set \nret = %d\n", ret);
	}
		
	sigaddset(&set, SIGINT); // 把 SIGINT 添加到信号集 set
	sigaddset(&set, SIGQUIT);// 把 SIGQUIT 添加到信号集 set
	
	// 判断 SIGINT 是否在信号集 set 里
	// 在返回 1, 不在返回 0
	ret = sigismember(&set, SIGINT);
	if(ret == 1){
		printf("SIGINT is a member of set \nret = %d\n", ret);
	}
	
	sigdelset(&set, SIGQUIT); // 把 SIGQUIT 从信号集 set 移除
	
	// 判断 SIGQUIT 是否在信号集 set 里
	// 在返回 1, 不在返回 0
	ret = sigismember(&set, SIGQUIT);
	if(ret == 0){
		printf("SIGQUIT is not a member of set \nret = %d\n", ret);
	}
	
	return 0;
}

运行结果如下:



信号阻塞集(屏蔽集、掩码)

信号阻塞集也称信号屏蔽集、信号掩码。每个进程都有一个阻塞集,创建子进程时子进程将继承父进程的阻塞集。信号阻塞集用来描述哪些信号递送到该进程的时候被阻塞(在信号发生时记住它,直到进程准备好时再将信号通知进程)。


所谓阻塞并不是禁止传送信号, 而是暂缓信号的传送。若将被阻塞的信号从信号阻塞集中删除,且对应的信号在被阻塞时发生了,进程将会收到相应的信号。


我们可以通过 sigprocmask() 修改当前的信号掩码来改变信号的阻塞情况。


所需头文件:

#include <signal.h>


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

功能:

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

参数:

how: 信号阻塞集合的修改方法,有 3 种情况:

SIG_BLOCK:向信号阻塞集合中添加 set 信号集,新的信号掩码是set和旧信号掩码的并集。
SIG_UNBLOCK:从信号阻塞集合中删除 set 信号集,从当前信号掩码中去除 set 中的信号。
SIG_SETMASK:将信号阻塞集合设为 set 信号集,相当于原来信号阻塞集的内容清空,然后按照 set 中的信号重新设置信号阻塞集。

set: 要操作的信号集地址。

若 set 为 NULL,则不改变信号阻塞集合,函数只把当前信号阻塞集合保存到 oldset 中。

oldset: 保存原先信号阻塞集地址

返回值:

成功:0,

失败:-1,失败时错误代码只可能是 EINVAL,表示参数 how 不合法。


注意:不能阻塞 SIGKILL 和 SIGSTOP 等信号,但是当 set 参数包含这些信号时 sigprocmask() 不返回错误,只是忽略它们。另外,阻塞 SIGFPE 这样的信号可能导致不可挽回的结果,因为这些信号是由程序错误产生的,忽略它们只能导致程序无法执行而被终止。


示例代码如下:

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

int main(int argc, char *argv[])
{
	sigset_t set; // 信号集合
	int i = 0;
	
	sigemptyset(&set); // 清空信号集合
	sigaddset(&set, SIGINT); // SIGINT 加入 set 集合
	
	while(1)
	{
		// set 集合加入阻塞集,在没有移除前,SIGINT 会被阻塞
		sigprocmask(SIG_BLOCK, &set, NULL);
		for(i=0; i<5; i++)
		{
			printf("SIGINT signal is blocked\n");
			sleep(1);
		}
		
		// set 集合从阻塞集中移除
		// 假如 SIGINT 信号在被阻塞时发生了
		// 此刻,SIGINT 信号立马生效,中断当前进程
		sigprocmask(SIG_UNBLOCK, &set, NULL);
		for(i=0; i<5; i++)
		{
			printf("SIGINT signal unblocked\n");
			sleep(1);
		}
	}
	
	return 0;
}


可靠信号的操作

从 UNIX 系统继承过来的信号(SIGHUP~SIGSYS,前 32 个)都是不可靠信号,不支持排队(多次发送相同的信号, 进程可能只能收到一次,可能会丢失)。


SIGRTMIN 至 SIGRTMAX 的信号支持排队(发多少次, 就可以收到多少次, 不会丢失),故称为可靠信号。


可靠信号就是实时信号,非可靠信号就是非实时信号。


signal() 函数只能提供简单的信号安装操作,使用 signal() 函数处理信号比较简单,只要把要处理的信号和处理函数列出即可。


signal() 函数主要用于前面 32 种不可靠、非实时信号的处理,并且不支持信号传递信息。


Linux 提供了功能更强大的 sigaction() 函数,此函数可以用来检查和更改信号处理操作,可以支持可靠、实时信号的处理,并且支持信号传递信息。


下面我们一起学习其相关函数的使用。


所需头文件:

#include <signal.h>


int sigqueue(pid_t pid, int sig, const union sigval value);

功能:

给指定进程发送信号。

参数:

pid: 进程号。

sig: 信号的编号,这里可以填数字编号,也可以填信号的宏定义,可以通过命令 kill -l ("l" 为字母)进行相应查看。

value: 通过信号传递的参数。

union sigval 类型如下:

union sigval
{
	int   sival_int;
	void *sival_ptr;
};

返回值:

成功:0

失败:-1


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

功能:

检查或修改指定信号的设置(或同时执行这两种操作)。

参数:

signum:要操作的信号。

act:   要设置的对信号的新处理方式(设置)。

oldact:原来对信号的处理方式(设置)。


如果 act 指针非空,则要改变指定信号的处理方式(设置),如果 oldact 指针非空,则系统将此前指定信号的处理方式(设置)存入 oldact。


返回值:

成功:0

失败:-1


信号设置结构体:

struct sigaction
{
	/*旧的信号处理函数指针*/
	void (*sa_handler)(int signum) ;
	
	/*新的信号处理函数指针*/
	void (*sa_sigaction)(int signum, siginfo_t *info, void *context);
	
	sigset_t sa_mask;/*信号阻塞集*/
	
	int sa_flags;/*信号处理的方式*/
};


sa_handler、sa_sigaction:信号处理函数指针,和 signal() 里的函数指针用法一样,应根据情况给 sa_sigaction、sa_handler 两者之一赋值,其取值如下:

SIG_IGN:忽略该信号

SIG_DFL:执行系统默认动作

处理函数名:自定义信号处理函数

sa_mask:信号阻塞集

sa_flags:用于指定信号处理的行为,它可以是一下值的“按位或”组合:

SA_RESTART:使被信号打断的系统调用自动重新发起(已经废弃)

SA_NOCLDSTOP:使父进程在它的子进程暂停或继续运行时不会收到 SIGCHLD 信号。

SA_NOCLDWAIT:使父进程在它的子进程退出时不会收到 SIGCHLD 信号,这时子进程如果退出也不会成为僵尸进程。

SA_NODEFER:使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号。

SA_RESETHAND:信号处理之后重新设置为默认的处理方式。

SA_SIGINFO使用 sa_sigaction 成员而不是 sa_handler 作为信号处理函数


信号处理函数:
void (*sa_sigaction)( int signum, 
siginfo_t *info,  void *context );
参数说明:

signum:信号的编号。
info:记录信号发送进程信息的结构体,进程信息结构体路径:/usr/include/i386-linux-gnu/bits/siginfo.h,其结构体详情请点此链接
context:可以赋给指向 ucontext_t 类型的一个对象的指针,以引用在传递信号时被中断的接收进程或线程的上下文,其结构体详情点此链接


下面我们做这么一个例子,一个进程在发送信号,一个进程在接收信号的发送。

发送信号示例代码如下:

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

/*******************************************************
*功能:     发 SIGINT 信号及信号携带的值给指定的进程
*参数:		argv[1]:进程号
			argv[2]:待发送的值(默认为100)
*返回值:	0
********************************************************/
int main(int argc, char *argv[])
{
	if(argc >= 2)
	{
		pid_t pid,pid_self;
		union sigval tmp;

		pid = atoi(argv[1]); // 进程号
		if( argc >= 3 )
		{
			tmp.sival_int = atoi(argv[2]);
		}
		else
		{
			tmp.sival_int = 100;
		}
		
		// 给进程 pid,发送 SIGINT 信号,并把 tmp 传递过去
		sigqueue(pid, SIGINT, tmp);
		
		pid_self = getpid(); // 进程号
		printf("pid = %d, pid_self = %d\n", pid, pid_self);
		
	}
	
	return 0;
}

接收信号示例代码如下:
#include <signal.h>
#include <stdio.h>

// 信号处理回电函数
void signal_handler(int signum, siginfo_t *info, void *ptr)
{
	printf("signum = %d\n", signum); // 信号编号
	printf("info->si_pid = %d\n", info->si_pid); // 对方的进程号
	printf("info->si_sigval = %d\n", info->si_value.sival_int); // 对方传递过来的信息
}

int main(int argc, char *argv[])
{
	struct sigaction act, oact;
	
	act.sa_sigaction = signal_handler; //指定信号处理回调函数
	sigemptyset(&act.sa_mask); // 阻塞集为空
	act.sa_flags = SA_SIGINFO; // 指定调用 signal_handler
	
	// 注册信号 SIGINT
	sigaction(SIGINT, &act, &oact);
	
	while(1)
	{
		printf("pid is %d\n", getpid()); // 进程号
		
		pause(); // 捕获信号,此函数会阻塞
	}
	
	return 0;
}

两个终端分别编译代码,一个进程接收,一个进程发送,运行结果如下:



本教程示例代码下载请点此处。

  • 23
    点赞
  • 47
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
进程间通信是指在不同进程之间进行数据交换和同步的一种技术。Linux提供了多种进程间通信的方式,包括管道、消息队列、共享内存和信号量等。在实验六中,我们将学习如何使用这些方式进行进程间通信。 1. 管道 管道是一种半双工的通信方式,它可以在两个进程之间传递数据。在Linux中,管道分为匿名管道和命名管道。匿名管道只能用于父子进程之间的通信,而命名管道可以用于任意两个进程之间的通信。 使用匿名管道进行进程间通信的步骤如下: - 父进程创建管道,并调用fork函数创建子进程。 - 子进程通过管道接收数据。 - 父进程通过管道发送数据。 - 子进程接收到数据后进行处理。 使用命名管道进行进程间通信的步骤如下: - 创建命名管道。 - 打开命名管道并进行读写操作。 2. 消息队列 消息队列是一种进程间通信机制,它允许不同进程之间通过一个消息传递序列来进行通信。在Linux中,每个消息都有一个类型,接收进程可以选择接收某个特定类型的消息。 使用消息队列进行进程间通信的步骤如下: - 创建消息队列。 - 发送消息到消息队列。 - 接收消息并进行处理。 3. 共享内存 共享内存是一种进程间通信的方式,它允许不同进程之间共享同一个物理内存区域。这种方式比较高效,但需要考虑进程间的同步和互斥问题,否则会出现数据不一致的情况。 使用共享内存进行进程间通信的步骤如下: - 创建共享内存区域。 - 进程通过共享内存区域进行数据交换。 - 进程需要进行同步和互斥操作。 4. 信号信号量是一种进程间同步的机制,它可以用来保证不同进程之间的共享资源在同一时刻只能被一个进程访问。在Linux中,每个信号量都有一个计数器,当计数器为0时,进程需要等待;当计数器大于0时,进程可以继续执行。 使用信号量进行进程间通信的步骤如下: - 创建信号量。 - 进程对信号量进行P操作(等待)。 - 进程对信号量进行V操作(释放)。 总体来说,不同的进程间通信方式各有优缺点,应根据实际需求选择适合的方式。在实验六中,我们将通过编写代码来学习如何使用这些方式进行进程间通信

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值