Linux 进程间通信基础(一)--信号

最近正好有一些空余时间,在这里总结一下曾经使用过的Linux进程间通信的几种方法,贴出来帮助有需要的人,也有助于自己总结经验加深理解,这里先从信号开始。

(一)概念

信号是Linux系统响应某些条件而产生的一个事件,信号可以指定发送给某一进程,接收到信号的进程会立即停止当前手头上的工作对信号进行相应,采取相应的行动(不考虑Linux内核的线程调度)。信号通常是由一些错误的产生而引发的中断,如内存地址冲突,非法的指令等等。我们也可以使用它当作进程间通信的一种方式,由一个进程发送给另外一个进程。

我们通常会在进程中事先预定需要捕获哪些信号,当该信号被发送给当前进程时,他就会被捕获,并且当前线程会执行当捕获到该信号时的逻辑。

具体的信号类型被定义在头文件<signal.h>中,这边列出几个简单的信号:

信号  值  说明
#define SIGHUP 1 连接挂断        
#define SIGINT 2 终端中断             
#define SIGQUIT 3 终端退出
#define SIGILL 4 非法指令

比如当我们在终端使用"ctrl + c"的组合健结束一个进程时,系统实际上是给该进程发送了一个SIGINT信号。

(二)信号的接收处理

我们先来试着接收一下信号,编写一个接收信号的程序。我们在程序中可以使用函数signal()来接收信号:

void (*signal(int sig, void (*func)(int) ))(int);

其中,

--sig是接收的信号,

扫描二维码关注公众号,回复: 2436821 查看本文章

--func是信号处理函数,func本身必须带一个int型的参数,就是信号本身。我们也可以用一些宏来而不是函数指针来传入func参数,如SIG_DFL,它表示将sig信号的行为回归成默认行为。

现在我们先来完成一个简单的信号处理程序"receiver.c":

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

bool runable;		/* 运行标志 */

/* 信号处理函数:根据用户的输入设置运行标志runable */
void exit_check( int sig )
{
	char user_choice;

	printf( "Are you sure to exit this application?(Y yes, N no)\n" );
	scanf( "%c", &user_choice );
	switch( user_choice )
	{
		case 'Y': runable = false; break;
		case 'y': runable = false; break;
		case 'N': runable = true; break;
		case 'n': runable = true; break;
		default:break;
	}

	return;
}

/* 入口函数 */
int main()
{
	runable = true;

	/* 注册信号处理函数,当收到SIGINT信号,调用exit_check() */
	signal( SIGINT, exit_check );

	while( 1 )
	{
		printf( "Application is running." );
		sleep(2);
		/* 当运行标志是false时,退出程序 */
		if( runable == false )
		{
			break;
		}
	}

	return 0;
}

可以看到这个程序是一个无限循环,每次循环都回打印一个表示程序正在运行的语句,然后检查一下运行标志runable变量,如果该变量变为false时,则终止循环退出程序。 在循环之前则是注册了一个信号响应函数exit_check()负责相应SIGINT信号,让客户在确认一下,是否退出程序,并根据客户的输入,修改runable变量。

现在我们试着来编译运行一下程序:

root@Server:/home/root/workspace/signal# gcc receiver.c -o receiver
root@Server:/home/root/workspace/signal# ./receiver 
Application is running.
Application is running.
Application is running.
Application is running.
Application is running.

可以看到程序一直在运行,现在我们尝试使用"ctrl + c"来终止它(就是向它发送SIGINT信号):

^CAre you sure to exit this application?(Y yes, N no)

可以看到程序没有马上终止而是进入了exit_check()函数,需要用户再次确认,我们尝试输入"Y"让程序退出:

Y
root@Server:/home/root/workspace/signal#

可以看到程序退出了,是因为我们修改了runale变量,而函数结束以后继续进入循环,检测时发现runable是false,所以终止了循环,退出了程序。

(三)信号的发送处理

我们现在来尝试一下从一个进程向另一进程发送信号,我们可以使用kill()函数完成这个工作:

int kill( pid_t pid, int sig );

其中,

--pid是目标进程的进程号。

--sig是你要发送的信号。

我们先来写一个简单的信号发送程序sender.c,这里我们简单采用手动输入进程pid号的方式:

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

int main()
{
	int thread_num;

	/* 要求用户输入进程id */
	printf( "Please input the thread number:" );
	scanf( "%d", &thread_num );

	/* 发送信号 */
	kill( thread_num, SIGINT );
	printf( "Send the SIGINT signal." );

	return 0;
}

我们先来运行一下之前的receiver.c:

root@Server:/home/root/workspace/signal# ./receiver 
Application is running.
Application is running.

然后利用ps命令查看他的进程号:

root@Server:/home/root/workspace/signal# ps aux|grep receiver
root      2937  0.0  0.0   4440   776 pts/1    S+   11:09   0:00 ./receiver
root      2939  0.0  0.0  13068  1132 pts/2    S+   11:09   0:00 grep --color=auto receiver

可以看到receiver的进程号是2937,现在我们重新启动一个终端并尝试运行sender.c,输入2937:

root@Server:/home/root/workspace/signal# gcc sender.c -o sender
root@Server:/home/root/workspace/signal# ./sender 
Please input the thread number:2937
Send the SIGINT signal.
root@Server:/home/root/workspace/signal# 

我们在查看一下刚才的receiver的状态:

Application is running.
Application is running.
Are you sure to exit this application?(Y yes, N no)

可以看到信号SIGINT成功的从sender发送到了receiver。

(四)信号的接收处理--更健壮的方式

我们已经清楚了使用signal()函数来定义信号的接收方式,其实linux定义了一个更健壮的接口,我们推荐使用它而不是signal(),介绍signal()只是为了更清楚的理解信号,现在开始要使用新的接口了。我们可以使用sigaction()函数来定义接收信号的行为:

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

其中

--sig是要接收的信号。

--act是定义接收到信号的行动。

--oact则是在本次定义以前,接收到该信号会采取的方式,如果需要保存之前的行为,可以将oact获取出来,以便以后恢复,如果没有需要可以设置成null。

sigaction这个结构体主要定义接受到信号后采取的行动,现在来看一下它:

struct sigaction {
        __sighandler_t sa_handler;	/* function, SIG_DFL or SIG_IGN */
        unsigned long sa_flags;			/* signal action modifiers  */
        __sigrestore_t sa_restorer;
        sigset_t sa_mask;           /* mask last for extensibility */
};

其中,

--sa_handler是函数指针,它指向接收信号时被调用的函数。它也可以被设置为SIG_DFL(恢复默认的动作)或者SIG_IGN(忽略该信号)。

--sa_mask指定了一个信号集,表示在接收到信号后调用sa_handler之前,该集之中的信号将被屏蔽,以防止还未处理这个信号,就又收到了其他的信号。

--sa_flags用于设置相关的标志位,这些标志位也定义在signal.h中。

--sa_restorer不需要使用。

现在我们来将上面的receiver.c改成使用新接口的处理方式,创建新的文件receiver2.c:

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

bool runable;		/* 运行标志 */

/* 信号处理函数:根据用户的输入设置运行标志runable */
void exit_check( int sig )
{
	char user_choice;

	printf( "Are you sure to exit this application?(Y yes, N no)\n" );
	scanf( "%c", &user_choice );
	switch( user_choice )
	{
		case 'Y': runable = false; break;
		case 'y': runable = false; break;
		case 'N': runable = true; break;
		case 'n': runable = true; break;
		default:break;
	}

	return;
}

/* 入口函数 */
int main()
{
	struct sigaction act;
	struct sigaction oact;

	runable = true;

	/* 注册信号处理函数,当收到SIGINT信号,调用exit_check() */
	act.sa_handler = exit_check;
	act.sa_flags = 0;
	sigemptyset( &act.sa_mask );
	sigaction( SIGINT, &act, &oact );

	while( 1 )
	{
		printf( "Application is running.\n" );
		sleep(2);
		/* 当运行标志是false时,退出程序 */
		if( runable == false )
		{
			break;
		}
	}

	return 0;
}

还是按照以前的方式,尝试用"ctrl + c"发送以下中断信号:

root@Server:/home/root/workspace/signal# gcc receiver2.c -o receiver2
root@Server:/home/root/workspace/signal# ./receiver2
Application is running.
Application is running.
Application is running.
^CAre you sure to exit this application?(Y yes, N no)
Y
root@Server:/home/root/workspace/signal# 

可以看到心得接口依然实现了我们期望的功能。

(五)信号集

你可能会有疑问就是我在receiver2.c中使用了一个函数来设置了信号集sa_mask。现在我们就来说一点sa_mask信号集的东西。sa_mask这个信号集表示在接收到信号后调用sa_handler之前,该集之中的信号将被屏蔽,这些信号将被阻塞并且不会传递给当前进程。其目地是防止出现还未处理这个信号,就又收到了其他的信号的情况。信号集提供了一组接口来操作它:

int sigaddset( sigset_t *set, int signo );
int sigemptyset( sigset_t *set );
int sigfillset( sigset_t *set );
int sigdelset( sigset_t *set, int signo );
int sigismember( sigset_t *set, int signo );
int sigprocmast( in how, const sigset_t *set, sigset_t *oset );
int sigpending( sigset_t *set );
int sigsuspend( const sigset_t *sigmask );

其中,

--sigaddset()是向信号集set中添加信号setno。

--sigemptyset()是将信号集set初始化成空。

--sigfillset()是将信号集set初始化成包含所有信号。

--sigdelset()是从信号集set中删除信号setno。

--sigismember()是判断信号signo是否包含在信号集set中。

--sigprocmast()是指定新的屏蔽信号set,获取已阻塞的旧的屏蔽信号oset。

--sigpending()是将被阻塞信号中停留在待处理状态的一组信号写到set中去。

--sigsuspend()是进程屏蔽信号替换为sigmask。

(六)一点小结

发送信号在linux进程间通信应用的不是特别广泛,但是他也有其特点。接收到信号的进程会直接停下当前的工作转入信号处理程序,拥有很强的即时性。但是发送信号无法携带额外的数据信息,只能单纯起到一个简单的通知作用,同时发送信号需要或许相应的权限,某些时候由于没有相关权限导致发送信号失败。所以信号很少用于频繁的需要数据交互的进程间通信中,对进程的管理和调度可能会起到一定作用。

猜你喜欢

转载自blog.csdn.net/hyklose/article/details/80333002