一、进程间通讯概述
1. 目的(为什么进程间需要通信?)
1)数据传输:一个进程需要将它的数据发送给另一个进程。
2)资源共享:多个进程之间共享同样的资源。
3)通知事件:一个进程需要向另一个或一组进程发送消息,通知它们发生了某种事件。
4)进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有操作,并能够及时知道它的状态改变。
2. 发展
Linux进程间通信(IPC)由以下几部分发展而来:
1)UNIX进程间通信
2)基于System V进程间通信
3)POSIX进程间通信
POSIX(Portable Operating System Interface)表示可移植操作系统接口。电气和电子工程师协会(Institute of Electrical and Electronics Engineers,IEEE)最初开发 POSIX 标准,是为了提高 UNIX 环境下应用程序的可移植性。然而,POSIX 并不局限于 UNIX,许多其它的操作系统,例如 DEC OpenVMS 和 Microsoft Windows,都支持 POSIX 标准。
System V,也被称为 AT&T System V,是Unix操作系统众多版本中的一支。
3. 分类
现在Linux使用的进程间通信方式包括:
1)管道(pipe)和有名管道(FIFO)
2)信号(signal)
3)消息队列
4)共享内存
5)信号量
6)套接字(socket)
二、 管道通信
1. 管道
管道是单向的、先进先出的,它把一个进程的输出和另一个进程的输入连接在一起。一个进程(写进程)在管道的尾部写入数据,另一个进程(读进程)从管道的头部读出数据。
数据被一个进程读出后,将被从管道中删除,其它读进程将不能再读到这些数据。管道提供了简单的流控制机制,进程试图读空管道时,进程将阻塞。同样,管道已经满时,进程再试图向管道写入数据,进程将阻塞。
2. 管道创建
管道包括无名管道和有名管道两种,前者用于父进程和子进程间的通信,后者可用于运行于同一系统中的任意两个进程间的通信。
无名管道由pipe()函数创建:
int pipe(int filedis[2]);当一个管道建立时,它会创建两个文件描述符:filedis[0] 用于读管道, filedis[1] 用于写管道,如图所示:
注意上图中,管道头和管道尾以及使用正确的文件描述符fd。
3. 管道关闭
关闭管道只需将这两个文件描述符关闭即可,可以使用普通的close函数逐个关闭。
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
int pipe_fd[2];
if(pipe(pipe_fd)<0)
{
printf("pipe create error\n");
return -1;
}
else
printf("pipe create success\n");
close(pipe_fd[0]);
close(pipe_fd[1]);
}
4. 管道读写
管道用于不同进程间通信。通常先创建一个管道,再通过fork函数创建一个子进程,该子进程会继承父进程所创建的管道。
注意事项:必须在系统调用fork( )前调用pipe( ),否则子进程将不会继承文件描述符。此时会创建2个管道,并且这两个管道不能进行通信。
pipe_rw.c程序:
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main()
{
int pipe_fd[2];
pid_t pid;
char buf_r[100];
char* p_wbuf;
int r_num;
memset(buf_r,0,sizeof(buf_r));
/*创建管道*/
if(pipe(pipe_fd)<0)
{
printf("pipe create error\n");
return -1;
}
/*创建子进程*/
if((pid=fork())==0) //子进程执行序列
{
printf("\n");
close(pipe_fd[1]);//子进程先关闭了管道的写端,由于数据部分是是独立的,管道的头尾fd开始在两个进程中都是打开的
sleep(2); /*让父进程先运行,这样父进程先写子进程才有内容读*/
if((r_num=read(pipe_fd[0],buf_r,100))>0)
{
printf("%d numbers read from the pipe is %s\n",r_num,buf_r);
}
close(pipe_fd[0]);
exit(0);
}
else if(pid>0) //父进程执行序列
{
close(pipe_fd[0]); //父进程先关闭了管道的读端
if(write(pipe_fd[1],"Hello",5)!=-1)
printf("parent write1 Hello!\n");
if(write(pipe_fd[1]," Pipe",5)!=-1)
printf("parent write2 Pipe!\n");
close(pipe_fd[1]);
waitpid(pid,NULL,0); /*等待子进程结束*/
exit(0);
}
return 0;
}
memset的原型:void memset(void *s, int ch, unsigned n);
功能是将s所指向的某一块内存中的每个字节的内容全部设置为ch指定的ASCII值,块的大小由第三个参数指定,这个函数通常为新申请的内存做初始化工作。
5. 命名管道(FIFO)
命名管道和无名管道基本相同,但也有不同点:无名管道只能由父子进程使用;但是通过命名管道,不相关的进程也能交换数据。
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char * pathname, mode_t mode)
vpathname:FIFO文件名
vmode:属性(见文件操作章节)
一旦创建了一个FIFO,就可用open打开它,一般的文件访问函数(close、read、write等)都可用于FIFO。
当使用open()函数打开FIFO时,非阻塞标志(O_NONBLOCK)将对以后的读写产生如下影响:
1)没有使用O_NONBLOCK:访问要求无法满足时进程将阻塞。如试图读取空的FIFO,将导致进程阻塞。
2)使用O_NONBLOCK:访问要求无法满足时不阻塞,立刻出错返回,errno是ENXIO。
要演示命名管道,需要两个不相关的进程,即两个c程序文件,分别为fifo_read和fifo_write文件。
fifo_read.c代码如下:
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define FIFO "/tmp/myfifo"
int main(int argc,char** argv)
{
char buf_r[100];
int fd;
int nread;
printf("Preparing for reading bytes...\n");
memset(buf_r,0,sizeof(buf_r));
/* 打开管道 */
fd=open(FIFO,O_RDONLY|O_NONBLOCK,0);
if(fd==-1)
{
perror("open");
exit(1);
}
while(1)
{
memset(buf_r,0,sizeof(buf_r));
if((nread=read(fd,buf_r,100))==-1)
{
if(errno==EAGAIN)
printf("no data yet\n");
}
printf("read %s from FIFO\n",buf_r);
sleep(1);
}
//后面三句话是不会被运行到的,但不会影响程序运行的效果当程序在上面的死循环中执行时收到信号后会马上结束运行而没有执行后面的三句话。这些会在后面的信号处理中讲到,现在不理解没有关系,这个问题留给大家学习了信号处理之后来解决。
close(fd); //关闭管道
pause(); /*暂停,等待信号*/
unlink(FIFO); //删除文件
}
fifo_write.c代码如下:
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define FIFO_SERVER "/tmp/myfifo"
int main(int argc,char** argv)
{
int fd;
char w_buf[100];
int nwrite;
/*创建有名管道*/
if((mkfifo(FIFO_SERVER,O_CREAT|O_EXCL|O_RDWR)<0)&&(errno!=EEXIST))
printf("cannot create fifoserver\n");
/*打开管道*/
fd=open(FIFO_SERVER,O_RDWR|O_NONBLOCK,0);
if(fd==-1)
{
perror("open");
exit(1);
}
/*入参检测*/
if(argc==1)
{
printf("Please send something\n");
exit(-1);
}
strcpy(w_buf,argv[1]);
/* 向管道写入数据 */
if((nwrite=write(fd,w_buf,100))==-1)
{
if(errno==EAGAIN)
printf("The FIFO has not been read yet.Please try later\n");
}
else
printf("write %s to the FIFO\n",w_buf);
close(fd); //关闭管道
return 0;
}
三、信号通信
1. 信号通信
信号(signal)机制是Unix系统中最为古老的进程间通信机制,很多条件可以产生一个信号:
1)当用户按某些按键时,产生信号。
2)硬件异常产生信号:除数为0、无效的存储访问等等。这些情况通常由硬件检测到,将其通知内核,然后内核产生适当的信号通知进程,例如,内核对正访问一个无效存储区的进程产生一个SIGSEGV信号。
3)进程用kill函数将信号发送给另一个进程。
4)用户可用kill命令将信号发送给其他进程。
2. 信号类型
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL5) SIGTRAP 6) SIGIOT 7) SIGBUS 8) SIGFPE9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR213) SIGPIPE 14) SIGALRM 15) SIGTERM17) SIGCHLD 18) SIGCONT 19) SIGSTOP20) SIGTSTP 21) SIGTTIN 22) SIGTTOU23) SIGURG 24) SIGXCPU 25) SIGXFSZ26) SIGVTALRM 27) SIGPROF 28) SIGWINCH
29) SIGIO 30) SIGPWR
下面是几种常见的信号:
SIGHUP:从终端上发出的结束信号
SIGINT:来自键盘的中断信号(Ctrl-C)
SIGKILL:该信号结束接收信号的进程
SIGTERM:kill 命令发出的信号
SIGCHLD:标识子进程停止或结束的信号
SIGSTOP:来自键盘(Ctrl-Z)或调试程序的停止执行信号
1) SIGHUP 本信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联.
2) SIGINT 程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出
3) SIGQUIT 和SIGINT类似, 但由QUIT字符(通常是Ctrl-\)来控制. 进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号.
4) SIGILL 执行了非法指令. 通常是因为可执行文件本身出现错误, 或者试图执行数据段. 堆栈溢出时也有可能产生这个信号.
5) SIGTRAP 由断点指令或其它trap指令产生. 由debugger使用.
6) SIGABRT 程序自己发现错误并调用abort时产生.
7) SIGIOT 在PDP-11上由iot指令产生, 在其它机器上和SIGABRT一样.
8)SIGBUS 非法地址, 包括内存地址对齐(alignment)出错. eg: 访问一个四个字长的整数, 但其地址不是4的倍数.
9) SIGFPE 在发生致命的算术运算错误时发出. 不仅包括浮点运算错误, 还包括溢出及除数为0等其它所有的算术的错误.
10) SIGKILL 用来立即结束程序的运行. 本信号不能被阻塞, 处理和忽略.
11) SIGUSR1 留给用户使用
12) SIGSEGV 试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据.
13) SIGUSR2 留给用户使用
14) SIGPIPE Broken pipe
15) SIGALRM 时钟定时信号, 计算的是实际的时间或时钟时间. alarm函数使用该信号.
16) SIGTERM 程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和处理. 通常用来要求程序自己正常退出. shell命令kill缺省产生这个信号.
17) SIGCHLD 子进程结束时, 父进程会收到这个信号.
18) SIGCONT 让一个停止(stopped)的进程继续执行. 本信号不能被阻塞. 可以用一个handler来让程序在由stopped状态变为继续执行时完成特定的工作. 例如, 重新显示提示符
19) SIGSTOP 停止(stopped)进程的执行. 注意它和terminate以及interrupt的区别: 该进程还未结束, 只是暂停执行. 本信号不能被阻塞, 处理或忽略.
20) SIGTSTP 停止进程的运行, 但该信号可以被处理和忽略. 用户键入SUSP字符时(通常是Ctrl-Z)发出这个信号
21) SIGTTIN 当后台作业要从用户终端读数据时, 该作业中的所有进程会收到SIGTTIN信号. 缺省时这些进程会停止执行.
22) SIGTTOU 类似于SIGTTIN, 但在写终端(或修改终端模式)时收到.
23) SIGURG 有紧急数据或out-of-band数据到达socket时产生.
24) SIGXCPU 超过CPU时间资源限制. 这个限制可以由getrlimit/setrlimit来读取/改变
25) SIGXFSZ 超过文件大小资源限制.
26) SIGVTALRM 虚拟时钟信号. 类似于SIGALRM, 但是计算的是该进程占用的CPU时间.
27) SIGPROF 类似于SIGALRM/SIGVTALRM, 但包括该进程用的CPU时间以及系统调用的时间.
28) SIGWINCH 窗口大小改变时发出.
29) SIGIO 文件描述符准备就绪, 可以开始进行输入/输出操作.
30) SIGPWR Power failure
3. 信号处理
当某信号出现时,将按照下列三种方式中的一种进行处理:
1)忽略此信号
大多数信号都按照这种方式进行处理,但有两种信号却决不能被忽略。它们是:SIGKILL和SIGSTOP。这两种信号不能被忽略的原因是:它们向超级用户提供了一种终止或停止进程的方法。
2)执行用户希望的动作
通知内核在某种信号发生时,调用一个用户函数。在用户函数中,执行用户希望的处理。
3)执行系统默认动作
对大多数信号的系统默认动作是终止该进程。
4. 信号发送
发送信号的主要函数有 kill和raise,其他还有alarm、pause。
区别:Kill既可以向自身发送信号,也可以向其他进程发送信号。与kill函数不同的是,raise函数是向进程自身发送信号。
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int signo)
int raise(int signo)
kill的pid参数有四种不同的情况:
1)pid>0
将信号发送给进程ID为pid的进程。
2)pid == 0
将信号发送给同组的进程。
3)pid < 0
将信号发送给其进程组ID等于pid绝对值的进程。
4)pid ==-1
将信号发送给所有进程。
另外,使用alarm函数可以设置一个时间值(闹钟时间),当所设置的时间到了时,产生SIGALRM信号。如果不捕捉此信号,则默认动作是终止该进程。
#include <unistd.h>
unsigned int alarm(unsigned int seconds)
seconds:经过了指定的seconds秒后会产生信号SIGALRM。
注意:1)每个进程只能有一个闹钟时间。如果在调用alarm时,以前已为该进程设置过闹钟时间,而且它还没有超时,以前登记的闹钟时间则被新值代换。
2)如果有以前登记的尚未超过的闹钟时间,而这次seconds值是0,则表示取消以前的闹钟。
pause函数使调用进程挂起直至捕捉到一个信号。
#include <unistd.h>
int pause(void)
只有执行了一个信号处理函数后,挂起才结束。
5. 信号的处理
当系统捕捉到某个信号时,可以忽略该信号或是使用指定的处理函数来处理该信号,或者使用系统默认的方式。
信号处理的主要方法有两种,一种是使用简单的signal函数,另一种是使用信号集函数组。
#include <signal.h>
void (*signal (int signo, void (*func)(int)))(int)
如何理解呢?可分为两部分:
typedef void (*sighandler_t)(int)
sighandler_t signal(int signum, sighandler_t handler))
Func可能的值是:
1)SIG_IGN:忽略此信号;
2)SIG_DFL: 按系统默认方式处理;
3)信号处理函数名:使用该函数处理。
mysignal.c程序代码如下:
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
void my_func(int sign_no)
{
if(sign_no==SIGINT)
printf("I have get SIGINT\n");
else if(sign_no==SIGQUIT)
printf("I have get SIGQUIT\n");
}
int main()
{
printf("waiting for signal SIGINT or SIGQUIT \n");
// 注册信号处理函数
signal(SIGINT,my_func);
signal(SIGQUIT,my_func);
pause();
exit(0);
}
怎么想应用程序发送sign信号呢,其实Ctrl-\就是SIGQUIT ,还有就是通过kill指令发送信号,如下:
1)如果不知道进程mysignal的pid,可以通过ps aux进行查询
2)在另一个终端执行指令:kill -s SIGQUIT (pid)
四、 共享内存
1. 共享内存
共享内存是被多个进程共享的一部分物理内存。共享内存是进程间共享数据的一种最快的方法,一个进程向共享内存区域写入了数据,共享这个内存区域的所有进程就可以立刻看到其中的内容。
共享内存实现分为两个步骤:
1)创建共享内存,使用shmget函数。
2)映射共享内存,将这段创建的共享内存映射到具体的进程空间去,使用shmat函数。
2. 创建
int shmget ( key_t key, int size, int shmflg )
key标识共享内存的键值: 0/IPC_PRIVATE。 当key的取值为IPC_PRIVATE,则函数shmget()将创建一块新的共享内存;如果key的取值为0,而参数shmflg中又设置IPC_PRIVATE这个标志,则同样会创建一块新的共享内存,size为共享内存大小。
返回值:如果成功,返回共享内存标识符(联想到文件描述符fd);如果失败,返回-1。
3. 映射
int shmat ( int shmid, char *shmaddr, int flag)
参数:
vshmid:shmget函数返回的共享存储标识符
vflag:决定以什么方式来确定映射的地址(通常为0)
返回值:如果成功,则返回共享内存映射到进程中的地址(即指针);如果失败,则返回-1。
4. 脱离
当一个进程不再需要共享内存时,需要把它从进程地址空间中脱离。
int shmdt ( char *shmaddr ),使用shmat的返回值即可。
shmem.c程序代码如下:
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#define PERM S_IRUSR|S_IWUSR // 可读可写
int main(int argc,char **argv)
{
int shmid;
char *p_addr,*c_addr;
if(argc!=2)
{
fprintf(stderr,"Usage:%s\n\a",argv[0]);
exit(1);
}
if((shmid=shmget(IPC_PRIVATE,1024,PERM))==-1) // 创建共享内存,返回共享内存id号
{
fprintf(stderr,"Create Share Memory Error:%s\n\a",strerror(errno));
exit(1);
}
if(fork())// 父进程写
{
// 将共享内存中的地址映射到本进程上来
// 第二个参数传入0,表示由系统自动找一个地址,也可以自己指定,但指定之后,不一定可以使用
// 这个函数返回指定的地址
p_addr=shmat(shmid,0,0);
memset(p_addr,'\0',1024); // 清空共享内存
strncpy(p_addr,argv[1],1024); // 将数据拷贝到共享内存中
wait(NULL); // 等待子进程读
exit(0);
}
else
{
sleep(1); // 暂停一秒,防止子进程先运行
c_addr=shmat(shmid,0,0); // 先使用映射,将共享内存的地址映射到本进程中的地址上来
printf("Client get %s\n",c_addr); //
exit(0);
}
}
其实使用共享内存这种通信方法很简单:创建和映射内存之后,就可以像使用指针一样去使用内存了,比管道、消息队列要简单一些。