linux下进程间通信的几种主要手段简介:
- 管道(Pipe)及有名管道(named pipe):无名管道可用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信;
- 信号(Signal):信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数);
- 报文(Message)队列(消息队列):消息队列是消息的链接表,包括Posix消息队列system V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。
- 共享内存:使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。
- 信号量(semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。
- 套接口(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到其它类Unix系统上:Linux和System V的变种都支持套接字。
- RPC——Remote Procedure Call
其中,1、2为Unix方式,3、4、5为SystemV方式,前五个都是用于同一主机进程间通信
6、7用于不同机器之间的进程间通信
获取时钟
UNIX 的时间系统存在一个基点, 就是格林威治时间 1970 年 1 月 1 日凌晨 0 点 0 分 0 秒, 也是传说中
UNIX 的生日.
UNIX 中存在三种格式的时间:
(1) 系统时间. UNIX 从出生到现在的秒数, 表现为一个 time_t 类型的变量
(2) 高分辨率时间. 精确到微秒的时间, 表现为一个 timeval 结构的变量
(3) 日历时间. 以'年, 月, 日, 时, 分, 秒'结构表示的时间, 表现为 tm 结构.
系统时间.
它是 UNIX 中最基本的时间形式. 用于系统时间的函数如下:
#include <time.h>
time_t time(time_t *tloc);
double difftime(time_t time2, time_t time1);
函数 difftime 获取两次 time 调用返回的系统时间差.
秒数往往很难读懂, UNIX 中更改系统时间为日历时间的函数如下:
#include <time.h>
struct tm *localtime(const time_t *clock);
time_t mktime(struct tm*timeptr);
函数 localtime 转换系统时间, clock 为当地时间, 并以 tm 结构返回.
函数 mktime 实现函数 localtime 的反功能.
下面给出一个打印本地时间的例子
[bill@billstone Unix_study]$ cat time1.c
#include <time.h>
#include <stdio.h>
int main()
{
struct tm when;
time_t now;
time(&now);
when = *localtime(&now);
printf("now=[%d] [%04d %02d %02d %02d:%02d:%02d]\n", now, \
when.tm_year+1900, when.tm_mon+1, when.tm_mday, \
when.tm_hour, when.tm_min, when.tm_sec);
return 0;
}
[bill@billstone Unix_study]$ make time1
cc time1.c -o time1
[bill@billstone Unix_study]$ ./time1
now=[1239927129] [2009 04 17 08:12:09]
信号的概念
信号是传送给进程的事件通知, 它可以完成进程间异步事件的通信.
导致信号产生的原因很多, 但总体说来有三种可能:
(1) 程序错误. 当硬件出现异常, 除数为 0 或者软件非法访问等情况时发生.
(2) 外部事件. 当定时器到达, 用户按健中断或者进程调用 abort 等信号发送函数时方生.
(3) 显式请求. 当进程调用 kill, raise 等信号发送函数或者用户执行 shell 命令 kill 传递信号时发生.
同样的, 当进程收到信号时有三种处理方式:
(1) 系统默认. 系统针对不同的信号有不同的默认处理方式.
(2) 忽略信号. 信号收到后, 立即丢弃. 注意信号 SIGSTOP 和 SIGKILL 不能忽略.
(3) 捕获信号. 进程接收信号, 并调用自定义的代码响应之.
查看系统支持的信号:
# kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL
5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE
9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2
13) SIGPIPE 14) SIGALRM 15) SIGTERM 17) SIGCHLD
18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN
22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO
30) SIGPWR 31) SIGSYS 33) SIGRTMIN 34) SIGRTMIN+1
35) SIGRTMIN+2 36) SIGRTMIN+3 37) SIGRTMIN+4 38) SIGRTMIN+5
39) SIGRTMIN+6 40) SIGRTMIN+7 41) SIGRTMIN+8 42) SIGRTMIN+9
43) SIGRTMIN+10 44) SIGRTMIN+11 45) SIGRTMIN+12 46) SIGRTMIN+13
47) SIGRTMIN+14 48) SIGRTMIN+15 49) SIGRTMAX-14 50) SIGRTMAX-13
51) SIGRTMAX-12 52) SIGRTMAX-11 53) SIGRTMAX-10 54) SIGRTMAX-9
55) SIGRTMAX-8 56) SIGRTMAX-7 57) SIGRTMAX-6 58) SIGRTMAX-5
59) SIGRTMAX-4 60) SIGRTMAX-3 61) SIGRTMAX-2 62) SIGRTMAX-1
63) SIGRTMAX
信号操作
函数 signal 设置对信号的操作动作,原型如下:
#include <signal.h>
void (*signal (int sig, void (*f) (int()) (int);
这是个复杂的函数原型, 不果可以分开看:
typedef void (*func)(int);
func signal(int sig, func f);
其中, func 参数有三种选择:SIG_DFL(恢复信号默认处理机制), SIG_IGN(忽略信号处理)和函数地址(调
用信号捕获函数执行处理).
首先看一个忽略终止信号 SIGINT 的例子.
[bill@billstone Unix_study]$ cat sig1.c
#include <signal.h>
#include <stdio.h>
int main()
{
signal(SIGINT, SIG_IGN);
sleep(10); // 睡眠 10 秒
return 0;
}
[bill@billstone Unix_study]$ make sig1
cc sig1.c -o sig1
[bill@billstone Unix_study]$ ./sig1
[bill@billstone Unix_study]$
在程序运行的 10 秒内,即使你键入 Ctrl+C 中断命令, 进程也不退出.
再看一个捕获自定义信号的例子.
[bill@billstone Unix_study]$ cat sig2.c
#include <signal.h>
#include <stdio.h>
int usr1 = 0, usr2 = 0;
void func(int);
int main()
{
signal(SIGUSR1, func);
signal(SIGUSR2, func);
for(;;)
sleep(1); // 死循环, 方便运行观察
return 0;
}
void func(int sig){
if(sig == SIGUSR1)
usr1++;
if(sig == SIGUSR2)
usr2++;
fprintf(stderr, "SIGUSR1[%d], SIGUSR2[%d]\n", usr1, usr2);
signal(SIGUSR1, func);
signal(SIGUSR2, func);
}
在后台运行, 结果如下:
[bill@billstone Unix_study]$ make sig2
cc sig2.c -o sig2
[bill@billstone Unix_study]$ ./sig2& // 后台运行
[2] 13822
[bill@billstone Unix_study]$ kill -USR1 13822 // 发送信号 SIGUSR1
SIGUSR1[1], SIGUSR2[0]
[bill@billstone Unix_study]$ kill -USR2 13822 // 发送信号 SIGUSR2
SIGUSR1[1], SIGUSR2[1]
[bill@billstone Unix_study]$ kill -USR2 13822 // 发送信号 SIGUSR2
SIGUSR1[1], SIGUSR2[2]
[bill@billstone Unix_study]$ kill -9 13822 //发送信号 SIGSTOP, 杀死进程
[bill@billstone Unix_study]$
[2]+ 已杀死 ./sig2
[bill@billstone Unix_study]$
UNIX 应用程序可以向进程显式发送任意信号, 原型如下:
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int signo); 向pid发送信号
int raise(int signo); 向自身发送信号
看一个发送和捕获 SIGTERM 终止信号的例子
[bill@billstone Unix_study]$ cat sig3.c
#include <signal.h>
#include <stdio.h>
#include <assert.h>
#include <unistd.h>
#include <sys/types.h>
void childfunc(int sig){
fprintf(stderr, "Get Sig\n");
}
int main()
{
pid_t pid;
int status;
assert((pid = fork()) >= 0);
if(pid == 0){
signal(SIGTERM, childfunc); //信号捕获
sleep(30);
exit(0);
}
fprintf(stderr, "Parent [%d] Fork child pid=[%d]\n", getpid(), pid);
sleep(1);
kill(pid, SIGTERM); //向子进程发送信号
wait(&status);
fprintf(stderr, "Kill child pid=[%d], exit status[%d]\n", pid, status>>8);
return 0;
}
[bill@billstone Unix_study]$ make sig3
cc sig3.c -o sig3
[bill@billstone Unix_study]$ ./sig3
Parent [13898] Fork child pid=[13899]
Get Sig
Kill child pid=[13899], exit status[0]
[bill@billstone Unix_study]$
安装信号处理函数:signal() sigset() sigaction()
信号阻塞:sigprocmask()
下面是一个 信号处理的综合例子:
1 | #include <stdio.h> |
2 | #include <signal.h> |
3 | #include <unistd.h> |
4 | |
5 | void my_handler(int sig) |
6 | { |
7 | printf("Signal catched: %d.\n",sig); |
8 | } |
9 | |
10 | int main() |
11 | { |
12 | struct sigaction act,oact; |
13 | act.sa_handler=my_handler; |
14 | sigemptyset(&act.sa_mask); |
15 | act.sa_flags=0; |
16 | sigaction(SIGINT,&act,&oact); |
17 | |
18 | // 首先阻塞信号SIGINT |
19 | sigset_t mask,omask; |
20 | sigemptyset(&mask); |
21 | sigaddset(&mask,SIGINT); |
22 | if(sigprocmask(SIG_BLOCK,&mask,&omask)<0) |
23 | { |
24 | perror("SIGBLOCK error"); |
25 | } |
26 | sleep(5); |
27 | |
28 | //处理阻塞的信号 |
29 | sigpedding(&mask); |
30 | if(sigismember(&mask,SIGINT)) |
31 | printf("SIGINT pending\n"); |
32 | |
33 | // 恢复信号集 |
34 | sigprocmask(SIG_SETMASK,&omask,NULL); |
35 | |
36 | while(1) |
37 | { |
38 | printf("..............\n"); |
39 | // 只有执行了一次信号处理并返回 pause才能返回 |
40 | pause(); |
41 | } |
42 | return 0; |
43 | } |
44 |
执行结果:
1 | $./a.out |
2 | ^C^C^CSIGINT pending |
3 | Signal catched: 2. |
4 | .............. |
5 | ^CSignal catched: 2. |
6 | .............. |
7 | ^CSignal catched: 2. |
8 | .............. |
9 | ^CSignal catched: 2. |
10 | .............. |
11 | ^\Quit |
12 |
定时器
UNIX 下定时器可以分为普通的定时器和精确的定时器.
普通定时器通过 alarm 函数实现, 它的精度是秒, 而且每调用一次 alarm 函数只能产生一次定时操作,
如果需要反复定时, 就要多次调用 alarm. 调用 fork 后, 子进程中的定时器将被取消, 但调用 exec 后, 定时
器仍然有效.
在 UNIX 中使用普通定时器需要三个步骤:
(1) 调用 signal 函数设置捕获定时信号 signal(SIGALRM,timefunc);
(2) 调用函数 alarm 定时. alarm(1);
(3) 编写响应定时信号函数.
下面是一个定时器的例子, 每隔 1 秒向进程发送定时信号,用户可键入 Ctrl+C 或 Delete 结束程序.
[bill@billstone Unix_study]$ cat time2.c
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
int n = 0;
void timefunc(int sig){
fprintf(stderr, "Alarm %d\n", n++);
signal(SIGALRM, timefunc);
alarm(1);
}
int main()
{
int status;
signal(SIGALRM, timefunc);
alarm(1);
while(1);
return 0;
}
[bill@billstone Unix_study]$ make time2
cc time2.c -o time2
[bill@billstone Unix_study]$ ./time2
Alarm 0
Alarm 1
Alarm 2
// 按 Ctrl+C 结束
[bill@billstone Unix_study]$
函数 alarm 设置的定时器只能精确到秒, 而下面函数理论上可以精确到毫秒:
getitimer, setitimer - get or set value of an interval timer
SYNOPSIS
#include <sys/time.h>
int getitimer(int which, struct itimerval *value);
int setitimer(int which, const struct itimerval *value, struct itimer-
val *ovalue);
函数 setitimer 可以提供三种定时器, 它们相互独立, 任意一个定时完成都将发送定时信号到进程, 并且
重新计时. 参数 which 确定了定时器的类型:
(1) ITIMER_REAL. 定时真实时间, 与 alarm 类型相同. 对应信号为 SIGALRM.
(2) ITIMER_VIRT. 定时进程在用户态下的实际执行时间. 对应信号为 SIGVTALRM.
(3) ITIMER_PROF. 定时进程在用户态和核心态下的实际执行时间. 对应信号为 SIGPROF.
在一个 UNIX 进程中, 不能同时使用 alarm 和 ITIMER_REAL 类定时器.
结构 itimerval 描述了定时器的组成:
struct itimerval{
struct timeval it_interval;
struct timeval it_value;
}
结构成员 it_value 指定首次定时的时间, 结构成员 it_interval 指定下次定时的时间. 定时器工作时, 先
将 it_value 的时间值减到 0, 发送一个信号, 再将 it_vale 赋值为 it_interval 的值, 重新开始定时, 如此反复.
如果 it_value 值被设置为 0, 则定时器停止定时.
结构 timeval 秒数了一个精确到微秒的时间:
struct timeval{
long tv_sec;
long tv_usec;
}
下面设计了一个精确定时器的例子, 进程每 1.5 秒发送定时信号 SIGPROF, 用户可键入 Ctrl+C 或
Delete 结束程序.
[bill@billstone Unix_study]$ cat time3.c
#include <sys/select.h>
#include <sys/time.h>
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
int n = 0;
void timefunc(int sig){
fprintf(stderr, "ITIMER_PROF[%d]\n", n++);
signal(SIGPROF, timefunc);
}
int main()
{
struct itimerval value;
value.it_value.tv_sec = 1;
value.it_value.tv_usec = 500000;
value.it_interval.tv_sec = 1;
value.it_interval.tv_usec = 500000;
signal(SIGPROF, timefunc);
setitimer(ITIMER_PROF, &value, NULL);
while(1);
return 0;
}
[bill@billstone Unix_study]$ make time3
cc time3.c -o time3
[bill@billstone Unix_study]$ ./time3
ITIMER_PROF[0]
ITIMER_PROF[1]
ITIMER_PROF[2]
[bill@billstone Unix_study]$
全局跳转
UNIX 下的 C 语言中,有一对特殊的调用:跳转函数, 原型如下:
#include <setjmp.h>
int setjmp(jmp_buf env);
void longjump(jmp_buf env, int val);
函数 setjmp 存储当前的堆栈环境(包括程序的当前执行位置)到参数 env 中,当函数正常调用成功时返回
0. 函数 longjmp 恢复保存在 env 中堆栈信息, 并使程序转移到 env 中保存的位置处重新执行. 这两个函数
联合使用, 可以实现程序的重复执行.
函数 longjmp 调用成功后, 程序转移到函数 setjmp 处执行, 函数 setjmp 返回 val. 如果参数 val 的取值
为 0, 为了与上次正常调用 setjmp 相区别,函数 setjmp 将自动返回 1.
下面是一个使用了跳转语句的例子, 它跳转两次后退出.
[bill@billstone Unix_study]$ cat jmp1.c
#include <setjmp.h>
int j = 0;
jmp_buf env;
int main()
{
auto int i, k = 0;
i = setjmp(env);
printf("setjmp = [%d], j = [%d], k = [%d]\n", i, j++, k++);
if(j > 2)
exit(0);
sleep(1);
longjmp(env, 1);
return 0;
}
[bill@billstone Unix_study]$ make jmp1
cc jmp1.c -o jmp1
[bill@billstone Unix_study]$ ./jmp1
setjmp = [0], j = [0], k = [0]
setjmp = [1], j = [1], k = [1]
setjmp = [1], j = [2], k = [2]
[bill@billstone Unix_study]$
其中, j 记录了程序的执行次数. 按理说, k 的值应该保持不变, 因为当返回到 setjmp 重新执行时, 保存
的堆栈中 k 应该保持 0 不变, 但实际上却变化了. 请高手指点, 是不是 setjmp 本身实现的问题(我用的环境
是 Red Hat 9)?
单线程 I/O 超时处理
UNIX 下的 I/O 超时处理是一个很常见的问题, 它的通常做法是接收输入(或发送输出)后立刻返回, 如
果无输入(或输出)则 n 秒后定时返回.
一般情况下, 处理 UNIX 中 I/O 超时的方式有终端方式, 信号跳转方式和多路复用方式等三种.
本节设
计一个定时 I/O 的例子, 它从文件描述符 0 中读取一个字符, 当有输入时继续, 或者 3 秒钟后超时退出,并打
印超时信息.
(1) 终端 I/O 超时方式
利用 ioctl 函数, 设置文件描述符对应的标准输入文件属性为”接收输入后立刻返回, 如无输入则 3 秒后
定时返回.
[bill@billstone Unix_study]$ cat timeout1.c
#include <unistd.h>
#include <termio.h>
#include <fcntl.h>
int main()
{
struct termio old, new;
char c = 0;
ioctl(0, TCGETA, &old);
new = old;
new.c_lflag &= ~ICANON;
new.c_cc[VMIN] = 0;
new.c_cc[VTIME] = 30; // 设置文件的超时时间为 3 秒
ioctl(0, TCSETA, &new);
if((read(0, &c, 1)) != 1)
printf("timeout\n");
else
printf("\n%d\n", c);
ioctl(0, TCSETA, &old);
return 0;
}
[bill@billstone Unix_study]$ make timeout1
cc timeout1.c -o timeout1
[bill@billstone Unix_study]$ ./timeout1
x
120
[bill@billstone Unix_study]$ ./timeout1
timeout
[bill@billstone Unix_study]$
(2) 信号与跳转 I/O 超时方式
在 read 函数前调用 setjmp 保存堆栈数据并使用 alarm 设定 3 秒定时.
[bill@billstone Unix_study]$ cat timeout2.c
#include <setjmp.h>
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
int timeout = 0;
jmp_buf env;
void timefunc(int sig){
timeout = 1;
longjmp(env, 1);
}
int main()
{
char c;
signal(SIGALRM, timefunc);
setjmp(env);
if(timeout == 0){
alarm(3);
read(0, &c, 1);
alarm(0);
printf("%d\n", c);
}
else
printf("timeout\n");
return 0;
}
[bill@billstone Unix_study]$ make timeout2
cc timeout2.c -o timeout2
[bill@billstone Unix_study]$ ./timeout2
v // 需要按 Enter 健激活输入
118
[bill@billstone Unix_study]$ ./timeout2
timeout
[bill@billstone Unix_study]$
(3) 多路复用 I/O 超时方式
一个进程可能同时打开多个文件, UNIX 中函数 select 可以同时监控多个文件描述符的输入输出, 进程
将一直阻塞, 直到超时或产生 I/O 为止, 此时函数返回, 通知进程读取或发送数据.
函数 select 的原型如下:
#include <sys/types.h>
#include <sys/times.h>
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
FD_CLR(int fd, fd_set *fdset); // 从 fdset 中删去文件描述符 fd
FD_ISSET(int fd, fd_set *fdset); // 查询文件描述符是否在 fdset 中
FD_SET(int fd, fd_set *fdset); // 在 fdset 中插入文件描述符 fd
FD_ZERO(fd_set *fdset); // 清空 fdset
参数 nfds 是 select 监控的文件描述符的时间, 一般为监控的最大描述符编号加 1.
类型 fd_set 是文件描述符集合, 其元素为监控的文件描述符.
参数 timeout 是描述精确时间的 timeval 结构,它确定了函数的超时时间,有三种取值情况:
a) NULL. 函数永远等待, 直到文件描述符就绪.
b) 0. 函数不等待, 检查文件描述符状态后立即返回.
c) 其他值. 函数等待文件描述符就绪, 或者定时完成时返回.
函数 select 将返回文件描述符集合中已准备好的文件总个数. 函数 select 返回就绪文件描述符数量后,
必须执行 read 等函数, 否则函数继续返回就绪文件数.
[bill@billstone Unix_study]$ cat timeout3.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/times.h>
#include <sys/select.h>
int main()
{
struct timeval timeout;
fd_set readfds;
int i;
char c;
timeout.tv_sec = 3;
timeout.tv_usec = 0;
FD_ZERO(&readfds);
FD_SET(0, &readfds);
i = select (1, &readfds, NULL, NULL, &timeout);
if(i > 0){
read(0, &c, 1);
printf("%d\n", c);
}
else if(i == 0)
printf("timeout\n");
else
printf("error\n");
return 0;
}
[bill@billstone Unix_study]$ make timeout3
cc timeout3.c -o timeout3
[bill@billstone Unix_study]$ ./timeout3
x
120
[bill@billstone Unix_study]$
[bill@billstone Unix_study]$ ./timeout3
timeout
[bill@billstone Unix_study]$
参考:《精通UNIX下C语言编程与项目实践》