进程间通信
进程间通信(IPC)是一组编程接口,让程序员能够协调不同的进程,使之能在一个操作系统里同时运行,并相互传递、交换信息。
通信目的
- 数据传输:一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几兆字节之间。
- 共享数据:多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 资源共享:多个进程之间共享同样的资源。为了作到这一点,需要内核提供锁和同步机制。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
进程通过与内核及其它进程之间的互相通信来协调它们的行为。Linux支持多种进程间通信(IPC)机制,信号和管道是其中的两种。除此之外,Linux还支持System V 的IPC机制(用首次出现的Unix版本命名)。
基础知识
- 原子操作(atomic operation)
原子操作意为不可被中断的一个或一系列操作,也可以理解为就是一件事情要么做了,要么没做。而原子操作的实现,一般是依靠硬件来实现的。
- 同步与互斥
同步:在访问资源的时候,以某种特定顺序的方式去访问资源
互斥:一个资源每次只能被一个进程所访问。
- 临界资源
不同进程能够看到的一份公共的资源(如:打印机,磁带机等),且一次仅允许一个进程使用的资源称为临界资源。
- 临界区
临界区是一段代码,在这段代码中进程将访问临界资源(例如:公用的设备或是存储器),当有进程进入临界区时,其他进程必须等待,有一些同步的机制必须在临界区段的进入点和离开点实现,确保这些共用资源被互斥所获得。
进程间通信分类
管道
————————-把一个进程连接到另一个进程的一个数据流称为一个“管道”。
1. 匿名管道pipe
创造一个无名管道:
#include <unistd.h>
int pipe(int pipefd[2]);
fd:文件描述符数组,fd[0]:读端,fd[1]:写端
1 //要求实现子进程往管道写,父进程从管道里读,只有子进程写了,父进程才能读
2 //fds[0]:读管道 fds[1]:写管道
3 #include<stdio.h>
4 #include<stdlib.h>
5 #include<unistd.h>
6
7 int main()
8 {
9 int fds[2];
10 if(pipe(fds)==-1)//创建管道
11 perror("pipe"),exit(1);
12 if(fork()==0)//子进程
13 {
14 close(fds[0]);//关掉子进程读
15 sleep(3);
16 write(fds[1],"adcbgfcry",10);
17 close(fds[1]);
18 exit(0);
19 }
20 else
21 {
22 close(fds[1]);//关掉父进程写
23 char buf[100]={};
24 printf("before read\n");
25 read(fds[0],buf,100);
26 printf("after read\n");
27 close(fds[0]);
28 printf("[%s]\n",buf);
29 exit(0);
30 }
31 }
1 //实现ls -l | wc -l
2 #include<stdio.h>
3 #include<string.h>
4 #include<unistd.h>
5 #include<stdlib.h>
6
7 int main()
8 {
9 int fds[2];
10 pipe(fds);
11 if(fork()==0)//子进程写
12 {
13 close(fds[0]);//关掉读
14 close(1);//关掉标准输出
15 dup(fds[1]);
16 close(fds[1]);
17 execlp("ls","ls","-l",NULL);
18 exit(0);
19 }
20 else//父进程读
21 {
22 close(fds[1]);//关掉写
23 close(0);//关掉标准输入
24 dup(fds[0]);
25 close(fds[0]);
26 execlp("wc","wc","-l",NULL);
27 exit(0);
28 }
29 }
管道特点:
- 只能用于具有共同祖先的进程之间进行通信
- 管道提供流式服务
- 一般而言,进程退出,管道释放,所以管道的生命周期随进程。
- 内核一般会对管道操作进行同步与互斥
- 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立两个管道。
2.命名管道
——-可以在不相关的进程之间交换数据,可以使用FIFO 文件来完成,它通常被称为命名管道。
用命令创建:mkfiko filename
函数创建:int mkfifo( const char *filename,mode_t mode);
1 #include<stdio.h>
2 #include<stdlib.h>
3
4
5 int main()
6 {
7
8 mkfifo("p2",0644);
9 return 0;
10 }
例子—-用命名管道实现server&client的通信
serverPipe.c
13 int main()
14 {
15 umask(0);
16 if(mkfifo("mypipe",0644)<0)
17 {
18 ERR_EXIT("mkfifo");
19 }
20 int rfd=open("mypipe",O_RDONLY);
21 if(rfd<0)
22 {
23 ERR_EXIT("open");
24 }
25 char buf[1024];
26 while(1)
27 {
28 buf[0]=0;
29 printf("please wait...\n");
30 ssize_t s=read(rfd,buf,sizeof(buf)-1);
31 if(s<0)
32 {
33 buf[s-1]=0;
34 printf("client say# %s\n",buf);
35 }
36 else if(s==0)
37 {
38 printf("client quit,exit now!\n");
39 exit(EXIT_SUCCESS);
40 }
41 else
42 {
43 ERR_EXIT("read");
44 }
45 }
46 close(rfd);
47 return 0;
48 }
clientPipe.c
15 int main()
16 {
17 int wfd=open("mypipe",O_WRONLY);
18 if(wfd<0)
19 {
20 ERR_EXIT("open");
21 }
22 char buf[1024];
23 while(1)
24 {
25 buf[0]=0;
26 printf("please enter...\n");
27 fflush(stdout);
28 ssize_t s=read(0,buf,sizeof(buf)-1);
29 if(s>0)
30 {
31 buf[s]=0;
32 write(wfd,buf,sizeof(buf)-1);
33 }
34 else if(s<=0)
35 {
36 ERR_EXIT("read");
37 }
38 }
39 close(wfd);
40 return 0;
41 }
System V IPC
1. 消息队列
消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。每个数据块都被认为含有一个类型,接收进程可以独立地接收含有不同类型的数据结构。我们可以通过发送消息来避免命名管道的同步和阻塞问题。但是消息队列与命名管道一样,每个数据块都有一个最大长度的限制。
有关命令:
ipcs -q 消息队列列表
ipcrm -Q msqid(要删除的消息队列ID)
消息队列相关函数:
msgget函数:msgget 通常是调用的第一个函数,功能是创建一个新的或已经存在的消息队列。此消息队列与key相对应。
int msgget(key_t key, int msgflag);
第一个参数key由ftok创建的key值;
第二个参数_msgflg的低位用来确定消息队列的访问权限。
msgsnd函数和msgrcv函数:
msgsnd 将数据放到消息队列中
msgrcv 从消息队列中读取数据
int msgsnd(int msqid, const void* msgp, size_t msgsz, int msgflag);
第一个参数key由ftok创建的key值;
第二个参数_msgflg的低位用来确定消息队列的访问权限。
第三个参数为接收消息的大小,其数据类型为:size_t,即unsigned int类型。其大小为0到系统对消息队列的限制值。
第四个参数用来执行在达到系统为消息队列所定的界限(如达到字数限制)时应采取的操作。
注意:msgp:指向消息缓冲区的指针,用来暂时存储发送和接受的消息。是一个允许用户定义的通用结构,如下:
struct msgubf
{
long mtype; // 消息类型, 必须大于零
char mtext[SIZE]; // 消息文本
}
ssize_t msgrcv(int msqid, void *ptr, size_t nbytes, long type, int msgflag);
第一个参数为读的对象,即从哪个消息队列获取信息。
第二个参数为一个临时消息数据结构,用来保存读取的信息。
msgctl函数
可以直接控制消息队列的行为
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
cmd操作:IPC_STAT、IPC_SET 、IPC_RMID。
IPC_STAT:该命令用来获取消息队列信息,返回的信息存贮在buf指向的msqid结构中;
IPC_SET:该命令用来设置消息队列的属性,要设置的属性存储在buf指向的msqid结构中;可设置属性包括:msg_perm.uid、msg_perm.gid、msg_perm.mode以及msg_qbytes,同时,也影响msg_ctime成员。
IPC_RMID:删除msqid标识的消息队列;
代码:
comm.h
1 #ifndef _COMM_H_
2 #define _COMM_H_
3
4 #include<stdio.h>
5 #include<stdlib.h>
6 #include<sys/types.h>
7 #include<string.h>
8 #include<sys/msg.h>
9 #include<sys/ipc.h>
10
11 #define PATHNAME "."
12 #define PROJ_ID 0x6666
13
14 #define SERVER_TYPE 1
15 #define CLIENT_TYPE 2
16
17 struct msgbuf
18 {
19 long mtype;
20 char mtext[1024];
21 };
22
23 int createMsgQueue();
24 int getMsgQueue();
25 int destroyMsgQueue(int msgid);
26 int sendMsg(int msgid,int who,char *msg);
27 int recvMsg(int msgid,int recvType,char out[]);
28
29 #endif
comm.c
1 #include"comm.h"
2
3 static int commMsgQueue(int flags)
4 {
5 key_t key=ftok(PATHNAME,PROJ_ID);
6 if(key<0)
7 {
8 perror("ftok");
9 return -1;
10 }
11 int msgid=msgget(key,flags);
12 if(msgid<0)
13 {
14 perror("msgget");
15 }
16 return msgid;
17 }
18 int createMsgQueue()//创建消息队列
19 {
20 return commMsgQueue(IPC_CREAT|IPC_EXCL|0666);
21 }
22 int getMsgQueue()
23 {
24 return commMsgQueue(IPC_CREAT);
25 }
26 int destroyMsgQueue(int msgid)//销毁消息队列
27 {
28 if(msgctl(msgid,IPC_RMID,NULL)<0)
29 {
30 perror("msgctl");
31 return -1;
32 }
33 return 0;
34 }
35 int sendMsg(int msgid,int who,char *msg)//发送
36 {
37 struct msgbuf buf;
38 buf.mtype=who;
39 strcpy(buf.mtext,msg);
40 if(msgsnd(msgid,(void *)&buf,sizeof(buf.mtext),0)<0)
41 {
42 perror("msgsnd");
43 return -1;
44 }
45 return 0;
46 }
47 int recvMsg(int msgid,int recvType,char out[])//接收
48 {
49 struct msgbuf buf;
50 if(msgrcv(msgid,(void *)&buf,sizeof(buf.mtext),recvType,0)<0)
51 {
52 perror("msgrcv");
53 return -1;
54 }
55 strcpy(out,buf.mtext);
56 return 0;
57 }
client.c
1 #include"comm.h"
2
3 int main()
4 {
5 int msgid=getMsgQueue();
6
7 char buf[1024];
8 while(1)
9 {
10 buf[0]=0;
11 printf("Please Enter#");
12 fflush(stdout);
13 ssize_t s=read(0,buf,sizeof(buf));
14 if(s>0)
15 {
16 buf[s-1]=0;
17 sendMsg(msgid,CLIENT_TYPE,buf);
18 printf("send done,wait recv...\n");
19 }
20 recvMsg(msgid,SERVER_TYPE,buf);
21 printf("server # %s\n",buf);
22 }
23 return 0;
24 }
server.c
1 #include"comm.h"
2
3 int main()
4 {
5 int msgid=createMsgQueue();
6
7 char buf[1024];
8 while(1)
9 {
10 buf[0]=0;
11 recvMsg(msgid,CLIENT_TYPE,buf);
12 printf("client #%s\n",buf);
13
14 printf("Please Enter#");
15 fflush(stdout);
16 ssize_t s=read(0,buf,sizeof(buf));
17 if(s>0)
18 {
19 buf[s-1]=0;
20 sendMsg(msgid,SERVER_TYPE,buf);
21 printf("send done,wait recv...\n");
22 }
23 }
24 destroyMsgQueue(msgid);
25 return 0;
26 }
当异常终止server和client时,再次运行server会报错。
当再用ipcrm -q 163841后,运行server就好了。
2. .共享内存
共享内存就是允许两个不相关的进程访问同一个逻辑内存。共享内存是在两个正在运行的进程之间共享和传递数据的一种非常有效的方式。不同进程之间共享的内存通常安排为同一段物理内存。进程可以将同一段共享内存连接到它们自己的地址空间中,所有进程都可以访问共享内存中的地址,就好像它们是由用C语言函数malloc分配的内存一样。而如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。
相关命令:
ipcs -m:查看共享内存
ipcrm -M key:删除共享内存
共享内存相关函数:
shmget//创建共享内存函数
int shmget(key_t key, size_t size, int shmflg);
第一个参数,与信号量的semget函数一样,程序需要提供一个参数key(非0整数),它有效地为共享内存段命名,shmget()函数成功时返回一个与key相关的共享内存标识符(非负整数),用于后续的共享内存函数。调用失败返回-1.
第二个参数,size以字节为单位指定需要共享的内存容量
第三个参数,shmflg是权限标志,它的作用与open函数的mode参数一样,如果要想在key标识的共享内存不存在时,创建它的话,可以与IPC_CREAT做或操作。共享内存的权限标志与文件的读写权限一样,举例来说,0644,它表示允许一个进程创建的共享内存被内存创建者所拥有的进程向共享内存读取和写入数据,同时其他用户创建的进程只能读取共享内存。
shmat//启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间。
void *shmat(int shmid, const void *shmaddr, int shmflg);
第一个参数,shm_id是由shmget()函数返回的共享内存标识。
第二个参数,shm_addr指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址。
第三个参数,shm_flg是一组标志位,通常为0。
调用成功时返回一个指向共享内存第一个字节的指针,如果调用失败返回-1.
shmdt()函数//该函数用于将共享内存从当前进程中分离
int shmdt(const void *shmaddr);
shmctl///用来控制共享内存`
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
第一个参数,shm_id是shmget()函数返回的共享内存标识符。
第二个参数,command是要采取的操作,它可以取下面的三个值 :
IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值。
IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值
IPC_RMID:删除共享内存段
第三个参数,buf是一个结构指针,它指向共享内存模式和访问权限的结构。
shmid_ds结构 至少包括以下成员:
struct shmid_ds
{
uid_t shm_perm.uid;
uid_t shm_perm.gid;
mode_t shm_perm.mode;
};
创建一个共享内存,完成写和读的功能。
shm_create.c
7 struct stu
8 {
9 int id;
10 char name[10];
11 };
12 int main()
13 {
14 key_t key=ftok(".",'a');
15 int id=shmget(key,sizeof(struct stu),IPC_CREAT|0644);
16 if(id==-1)
17 perror("shmget"),exit(1);
18 printf("create ok\n");
19 }
shm_write.c
7 struct stu
8 {
9 int id;
10 char name[10];
11 };
12 int main()
13 {
14 key_t key=ftok(".",'a');
15 int id=shmget(key,0,0);
16 if(id==-1)
17 perror("shmget"),exit(1);
18 struct stu *p=(struct stu *)shmat(id,NULL,0);
19 p->id=20;
20 strcpy(p->name,"龙堡成");
21
22 sleep(10);
23 shmdt(p);
24 }
shm_read.c
7 struct stu
8 {
9 int id;
10 char name[10];
11 };
12 int main()
13 {
14 key_t key=ftok(".",'a');
15 int id=shmget(key,0,0);
16 if(id==-1)
17 perror("shmget"),exit(1);
18 struct stu *p=(struct stu *)shmat(id,NULL,0);
19 printf("id=%d,name=%s\n",p->id,p->name);
20
21 shmdt(p);
22 // shmctl(id,IPC_RMID,0);//删除共享内存
23 }
用ipcs -m查看为:
3. 信号量
信号量(Semaphore)可以被看做是一种具有原子操作的计数器,它控制多个进程对共享资源的访问,通常描述临界资源当中,临界资源的数目,常常被当做锁(lock)来使用,防止一个进程访问另外一个进程正在使用的资源。
相关命令
ipcs -s 显示已存在的信号量
ipcrm -S 删除指定信号量
相关函数
semget(得到一个信号量集标识符或创建一个信号量集对象):
semop(完成对信号量的P操作或V操作)
semctl (得到一个信号量集标识符或创建一个信号量集对象)
创建一个信号量,并对其进行p,v操作。
sem_creat.c–创建一个信号量
1 //信号量
2 #include<stdio.h>
3 #include<sys/ipc.h>
4 #include<sys/sem.h>
5 #include<stdlib.h>
6 #include<unistd.h>
7
8
9 int main()
10 {
11 int id=semget(1234,1,IPC_CREAT|0600);
12 if(id==-1)
13 perror("semget"),exit(1);
14 printf("create ok\n");
15 }
setval.c–设置初值
1 //设置初值
2 #include<stdio.h>
3 #include<sys/ipc.h>
4 #include<sys/sem.h>
5 #include<stdlib.h>
6 #include<unistd.h>
7
8 union semun
9 {
10 int val;
11 };
12 int main()
13 {
14 int id=semget(1234,0,0);
15 if(id==-1)
16 perror("semget"),exit(1);
17 union semun su;
18 su.val=5;
19 semctl(id, 0,SETVAL,su) ;
20 }
getval.c–得到初值
1 #include<stdio.h>
2 #include<sys/ipc.h>
3 #include<sys/sem.h>
4 #include<stdlib.h>
5 #include<unistd.h>
6
7 int main()
8 {
9 int id=semget(1234,0,0);
10 if(id==-1)
11 perror("shmget"),exit(1);
12 int num=semctl(id, 0,GETVAL);
13 printf("num=%d\n",num);
14 }
p.c–p操作
1 //p操作
2 #include<stdio.h>
3 #include<sys/ipc.h>
4 #include<sys/sem.h>
5 #include<stdlib.h>
6 #include<unistd.h>
7
8 void p(int id)
9 {
10 struct sembuf sb[1];
11
12 sb[0].sem_num=0;
13 sb[0].sem_op=-1;
14 sb[0].sem_flg=0;
15
16 semop(id,sb,1);
17 }
18
19 int main()
20 {
21 int id=semget(1234,0,0);
22 if(id==-1)
23 perror("shmget"),exit(1);
24 p(id);
25 }
v.c–v操作
7 void v(int id)
8 {
9 struct sembuf sb[1];
10
11 sb[0].sem_num=0;
12 sb[0].sem_op=1;
13 sb[0].sem_flg=0;
14
15 semop(id,sb,1);
16 }
17
18 int main()
19 {
20 int id=semget(1234,0,0);
21 if(id==-1)
22 perror("shmget"),exit(1);
23 v(id);
24 }
可以看见设置信号量初值为5,当进行p操作后会减一,v操作后会加一。