1.进程间通信在linux下是非常重要的,我们知道在linux下不同的进程的操作是互不干扰的( 独立性,这是因为每个进程都有自己的独特的虚拟地址,而这个虚拟地址就是进程与物理内存沟通的桥梁,对于不同的进程,相同的虚拟地址位置会映射到不同的物理内存上,所以进程具有独立性),那么我们难免会遇到一个进程需要另一个进程的东西,那么对于进程间通信,在这些方面就非常重要。
2.进程间通信的目的:
- 数据传输:一个进程将自己的数据传输给另一个进程。
- 资源共享:多个进程共享同一个资源。
- 通知事件:一个进程要给另一个进程去发送消息。
- 进程控制:一个进程想要去控制另一个进程,此时的控制想要拦截另一个进程的所有陷入和异常,并能够及时知道其状态的改变。
管道
1.概念:管道为进程间通信的一种方式,如同在生活中一样,管道对于我们的作用非常的大,它可以帮助我们合理,有条的去输送和排放东西。而在进程间通信中,管道分为以下两种管道:匿名管道和命名管道,并且他们具有半双工通信的功能。(两者有不同的适用情况)
2.半双工通信:一个管道有两个端口,每个端口都可以进行写入,或者读取资源,但是同一实际,相同的端口只能写入或者删除,不可同时进行。
3.管道的本质:本质为内核的一个缓冲区(内核的一块物理内存),通过多个进程访问同一个缓冲区来实现通信(由于内核对所有的进程来说都是一样的,所以在内核中开辟一块内存用于交流)。
匿名管道
1.概念:在内核中开辟这块内存,但是没有标识符,无法被其他进程找到。(所以匿名管道是针对于有亲缘关系的进程使用,例如:父子进程,由于子进程在通过父进程创建的时候,复制了父进程的文件描述信息,所以子进程也就有这个文件描述符去操作这个管道)
所以说,对于匿名管道,只有通过子进程去复制父进程的方式,才能获取同一个管道的操作句柄。
2.匿名管道的创建:
①:创建函数接口:int pipe(int pipefd[2])
其中:
- pipefd[2]:为管道的两个文件描述符,分别代表的是读取和写入。(其中,pipefd[0]表示的是从管道中读取数据,pipefd[1]表示的给管道中写入数据,成功返回0,失败返回-1)
注意:匿名管道的建立一定要在创建子进程之前,这样才能让子进程获得管道操作句柄。
②:匿名管道的操作原理:
如下图①:
为匿名管道的内核与父子进程的关系图。
而对于父子进程的操作管道缓冲去的时候如下图:
其中图中的3,4分别为读取和写入端。
如下:我们对匿名管道进行操作:
1 #include<stdio.h>
2 #include<sys/wait.h>
3 #include<unistd.h>
4 #include<stdlib.h>
5 #include<string.h>
6 int main()
7 {
8 int pipefd[2];
9 char buf[1024];
10
11 if(pipe(pipefd) == -1)//创建匿名管道
12 {
13 perror("pipe error");
14 return -1;
15 }
16 pid_t pid = fork();
17 if(pid < 0)
18 {
19 perror("fork error");
20 return -1;
21 }
22 else if(pid > 0)//父进程向管道写入数据
23 {
24 close(pipefd[0]);
25 write(pipefd[1],"i am father",12);
26 }
27 else
28 {
29 close(pipefd[1]);
30 int res = read(pipefd[0],buf,12);
31 printf("%s\n",buf);
32 printf("%d\n",res);
33 }
34 wait(NULL);
35 close(pipefd[0]);
36 close(pipefd[1]);
37 return 0;
38 }
运行结果如下:
其中:
- 关闭文件描述符并不是释放缓冲区,也不是删除文件,只是断开了和管道的一端的连接。
- 缓冲区是当所有的进程都和其断开后,缓冲区才会释放。
- 缓冲区大小有限。
3.管道的特性:
- 如果管道中没有数据,则read堵塞,如果管道中数据满了,则write堵塞。
- 管道的读写操作是会堵塞的,如果对于一个管道,其写端全部关闭(不再向管道中写数据了),那么读端会将会将管道中的数据全部拿出,然后不再堵塞,返回0(这里的0表示的是不再写入的,因为是读,所以对读取文件没有操作,所以说,人家文件中还是有数据的)。
- 管道中的读端关闭了(没有人去写入数据了),那么write会出发异常,进程退出。
- 在匿名管道中,数据是先入先出的。
- 进程退出,管道释放,所有管道生命周期是跟随其进程的。
- 内核会对管道的操作进行同步和互斥。
命名管道
对于进程间的通信不可能只有父子间的通信,肯定还有没有亲缘关系的进程间通信,而对于这些进程,是通过命名管道进行通信的。
1.概念:内核中开辟这一块缓冲区,并具有标识符,可以被其他进程找到。(适用于同一主机中没有亲缘关系的进程间通信)
对于一个进程创建了命名管道,这个命名管道会在文件系统中创建出一个管道文件(实际上是管道的名称),多个进程通过打开同一个管道文件,访问内核中的同一个缓冲区实现通信。
2.命名管道的创建方法:
①:命令:mkfifo filename //创建命名管道文件
②:函数接口:int mkfifo(char* filename,mode_t mode)
其中:
- filename:为创建的命名管道文件的文件名
- mode:为文件的管理操作。
③:对于命名管道文件,其与内核的关系如图:
对于其的操作,如下代码:
①:为写端:
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4 #include<string.h>
5 #include<errno.h>
6 #include<fcntl.h>
7 #include<sys/stat.h>
8 int main()
9 {
10 int res = mkfifo("tp",0664);
11 if(res < 0 && errno != EEXIST)
12 {
13 perror("mkfifo error");
14 return -1;
15 }
16 int fd = open("tp",O_WRONLY);
17 if(fd < 0)
18 {
19 perror("open error");
20 return -1;
21 }
22 char* buf = "i am process A";
23 int ret = write(fd,buf,strlen(buf));
24 if(ret < 0)
25 {
26 close(fd);
27 perror("write error");
28 return -1;
29 }
30 close(fd);
31 return 0;
32 }
②:为读端:
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4 #include<string.h>
5 #include<errno.h>
6 #include<errno.h>
7 #include<fcntl.h>
8 #include<sys/stat.h>
9 int main()
10 {
11 int res = mkfifo("tp",0664);
12 if(res < 0 && errno != EEXIST)
13 {
14 perror("mkfifo error");
15 return -1;
16 }
17 int fd;
18 fd = open("tp",O_RDONLY);
19 if(fd < 0)
20 {
21 perror("open outfd error");
22 return -1;
23 }
24 char buf[1024] = {0};
25 int ret = read(fd,buf,1024);
26 if(ret < 0)
27 {
28 perror("read error");
29 close(fd);
30 return -1;
31 }
32 printf("%s\n",buf);
33 close(fd);
34 return 0;
35 }
操作方式为,先向一个缓冲区去写入数据,再向其中读出数据,如下是操作结果:
首先运行读取行为,此时处于阻塞状态,然后我们打开另一个进程,去运行写端函数
然后返回来看读端的结果:
然后就运行出来了。
3.命名管道的特性:
- 创建命名管道文件,并不会立即创建缓冲区,而是在有进程访问的时候才会创建(写时拷贝思想,提高资源,节省效率)。
- 如果命名管道文件只以读打开,那么就会堵塞,会一直堵塞到这个文件被其他程序以写打开。
- 如果命名管道文件只以写打开,那么就会堵塞,会一直堵塞到这个文件被其他程序一读打开。
特性2和3的原理:对于一个命名管道文件,在没有确定要写入数据和有进程去读取数据的时候,就没有必要去开辟,会浪费资源。
其他特性和匿名管道相同。
4.对于管道自带的同步和互斥
①:同步:按照一定的顺序去进行(写入数据的时候才能有数据被读取,没有数据,则read堵塞,数据满了,则write堵塞,读取了继续写)。
②:互斥:操作是安全可靠的(对于两个进程对一个管道文件写入的时候必须有数据,防止交叉写入,破坏数据原本的样子);并且读写大小不能超过PIPE_BUF(也就是4096字节),大小保证原子操作。
③:原子操作:一次性完成,中间不能被打断。
共享内存
1.特性:是最快的进程间通信(IPC)的一种方法。
2.原理:因为对于共享内存,他是在物理内存上开辟一个内存块,然后对于有需要的的进程,会连接这个内存块,将其映射到自己的虚拟地址上,这样对于不同的进程对于这个内存的操作就不会涉及内核了,所以大大的减少了内核和用户直接沟通的那段时间,提高了效率,所以是最快的IPC。
如下图:
3.共享内存的相关函数:
-
shmget函数(创建共享内存):
int shmget(key_t key,size_t size,int shmflg);
其中:
①:key为共享内存段的名字,为了让多个进程找到同一个。(其实key_t也是一个int的数据类型,而这个内存段的名字其实就是没每个内存所对应的id)其中,如果key为IPC_PRIVATE(也就是0)的话,则会建立新的内存对象,而自己设置为0~32位的数时要视参数shmflg来确定操作。通常要求此值来源于ftok返回的IPC键值。而对于ftok的函数定义如下:key_t ftok(const char* filename,int proj_id)
其中proj_id为项目工程的一个号,是由自己定义出,而对这个函数的操作,是将文件的编号和proj_id号合并的,而文件的编号可由如下求出,如下图:
②:size:需要创建共享内存的大小。(仅创建时候有效)
③:shmflag:一般情况下,我们会写:IPC_CREAT|IPC_EXCL|0664
注意:
IPC_CREAT:表示不存在则创建,存在则打开。
IPC_EXCL:表示存在则报错,不存在则创建打开,与IPC_CREAT搭配使用。
0664:表示的是对这个内存的访问权限。(文件创建的时候必须加上,不然会随机给文件操作权限,会出现一些不堪的情况)
④:成功返回一个非负整数----操作句柄,失败返回-1。 -
shamt函数(建立进程与共享内存间映射关系):
void* shmat(int shm_id,void* shmaddr,int shmflag)
其中:
①:shm_id:shmget函数的返回值(也就是共享内存的操作句柄)。
②:shmaddr:通常设置为NULL,让系统自动建立映射关系。
③:shmflag:设置为SHM_RDONLY为只读,0为默认的可读可写。
返回值:成功返回映射的首地址,失败返回(void*)-1。 -
shmdt函数(将共享内存与当前进程断开):
int shmdt(void* shmaddr)
其中:
①:shmaddr为shamt函数的返回值。
返回值:成功返回0,失败返回-1。 -
shmctl函数(控制共享内存):
int shmctl(int shmid,int cmd,struct shmid_ds *buf)
其中:
①:shmid:shmget函数的返回值。
②:cmd:IPC_RMID-标记函数(不会再接受新的映射)
③:buf:用于获取共享内存的信息,,不需要的话设置为NULL。
返回值:成功返回0,针对IPC_RMID的失败返回-1。
cmd一共有三个情况可取如下图:
4.共享内存的操作流程为以下:
- 创建或者打开共享内存
- 将共享内存映射到相应的进程虚拟地址空间中
- 对其进行操作
- 解除映射关系
- 删除共享内存
对于删除共享内存时候要注意的是:共享内存的删除是一种计数器的形式,当所有进程对其进行删除的时候(意思为计数器的数量为0的时候),共享内存才会被删除,并且释放其中的资源。
以下为共享内存的例子:
如下为写入:
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<string.h>
4 #include<unistd.h>
5 #include<sys/types.h>
6 #include<sys/ipc.h>
7 #include<sys/shm.h>
8
9 #define KEY 0x11111111
10 int main()
11 {
12 int shmid = shmget(KEY,4096,IPC_CREAT|0664);
13 if(shmid < 0)
14 {
15 perror("shmget error");
16 return -1;
17 }
18 void*shm_start = shmat(shmid,NULL,0);
19 if(shm_start == (void*)-1)
20 {
21 perror("shmat error");
22 return -1;
23 }
24 snprintf(shm_start,4096,"i am process A");
25 shmdt(shm_start);
26 shmctl(shmid,IPC_RMID,NULL);
27 return 0;
28 }
如下为读取:
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4 #include<sys/types.h>
5 #include<sys/ipc.h>
6 #include<sys/shm.h>
7
8 #define KEY 0x11111111
9 int main()
10 {
11 int shmid = shmget(KEY,4096,IPC_CREAT|0664);
12 if(shmid < 0)
13 {
14 perror("shmget error");
15 return -1;
16 }
17 void*shm_start = shmat(shmid,NULL,0);
18 if(shm_start == (void*)-1)
19 {
20 perror("shmat error");
21 return -1;
22 }
23 sleep(10);
24 printf("%s\n",(char*)shm_start);
25 shmdt(shm_start);
26 shmctl(shmid,IPC_RMID,NULL);
27 return 0;
28 }
运行结果如下:
先在写入端进程去写入:
然后在读取端函数进行读取,如下:
对于查看和删除进程间通信的还有两个指令:
①:ipcs指令查看。
②:ipcrm指令删除。
5.共享内存的特性:
- 共享内存是一种覆盖式操作。(写入数据时候会直接覆盖前面的内容)
- 共享内存的生命周期跟随的是内核,在非人为的操作下,共享内存即使连接数为0也不会去释放。
- 共享内存中没有互斥同步关系。
消息队列
1.本质:是内核中的一个优先级队列。
2.功能:具有标识符,可以被其他进程找到,多个进程通过访问同一个队列,通过添加或者获取节点实现通信。
3.传输的节点:消息队列传输的都是数据节点,并且节点中包含两个信息:类型和数据。(类型的作用是用于身份区分)
其队列的简单图如下:
其中用head和tail来识别要取出的数据段,而1和n代表的节点。
特点:
①:双工通信。
②:自带互斥与同步。
③:生命周期跟随内核。
信号量
1.本质:是内核中的一个计数器。
2.作用:实现进程间的同步与互斥(包含进程间对临界资源的访问操作)。
其中:临界资源就是大家都能访问到的资源,也就是我们需要传输的资源。
3.对于保护操作的方式也就是我们上面所说的同步和互斥:
①:同步:通过一些条件让资源访问有序。
②:互斥:通过让进程同一时间对资源进行唯一访问来保证资源安全。
4.信号量对同步和互斥的操作原理:
会通过计数器进行计数,若计数器大于0则表示可以访问资源,如果小于或者等于0,则不能去访问资源,进程堵塞。
其操作方式如下:
会有两个操作方式,一个管理输入,一个人管理输出。
①:P操作:管理输入,在进程被访问之前进行,首先判断计数是否大于0
如果大于0,则计数器-1,如果小于0计数器也是-1。(用来排队使用)
②:V操作:管理输出,当一个资源被操作完后,没有操作的时候,那么计数器就会+1,则会有一个空位,在外面排队的就可以进入一个。
5:对互斥和同步的擦着步骤:
①:互斥:
- 初始化临界资源计数器为1。
- 在访问临界资源之前进行P操作。
- 在访问临界资源之后进程V操作。
②:同步:
- 根据资源数量初始化计数器。
- 访问资源之前进行P操作。
- 产生一个新资源进行V操作。
对于互斥和同步就像是在停车场停车一样,同步是在停车场门口进行的操作,排队驶入,而互斥就像是停车场内部对一个停车位进行的操作。