最近正好有一些空余时间,在这里总结一下曾经使用过的Linux进程间通信的几种方法,贴出来帮助有需要的人,也有助于自己总结经验加深理解,这里先从信号开始。
(一)概念
信号是Linux系统响应某些条件而产生的一个事件,信号可以指定发送给某一进程,接收到信号的进程会立即停止当前手头上的工作对信号进行相应,采取相应的行动(不考虑Linux内核的线程调度)。信号通常是由一些错误的产生而引发的中断,如内存地址冲突,非法的指令等等。我们也可以使用它当作进程间通信的一种方式,由一个进程发送给另外一个进程。
我们通常会在进程中事先预定需要捕获哪些信号,当该信号被发送给当前进程时,他就会被捕获,并且当前线程会执行当捕获到该信号时的逻辑。
具体的信号类型被定义在头文件
信号 | 值 | 说明 |
---|---|---|
#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是接收的信号,
--func是信号处理函数,func本身必须带一个int型的参数,就是信号本身。我们也可以用一些宏来而不是函数指针来传入func参数,如SIG_DFL,它表示将sig信号的行为回归成默认行为。
现在我们先来完成一个简单的信号处理程序"receiver.c":
#include
#include
#include
#include
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
#include
#include
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
#include
#include
#include
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进程间通信应用的不是特别广泛,但是他也有其特点。接收到信号的进程会直接停下当前的工作转入信号处理程序,拥有很强的即时性。但是发送信号无法携带额外的数据信息,只能单纯起到一个简单的通知作用,同时发送信号需要或许相应的权限,某些时候由于没有相关权限导致发送信号失败。所以信号很少用于频繁的需要数据交互的进程间通信中,对进程的管理和调度可能会起到一定作用。