Appearance
进程间通信
IPC: InterProcess Communication
使用内核里面的一块缓冲区, 进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信
在进程间完成数据传递需要借助操作系统提供特殊的方法,如:文件、管道、信号、共享内存、消息队列、套接字、命名管道等。随着计算机的蓬勃发展,一些方法由于自身设计缺陷被淘汰或者弃用。现今常用的进程间通信方式有:
① 管道 (使用最简单, 这一个一般是有关系的进程间)
② 信号 (开销最小, 数据量受限)
③ 共享映射区 (无血缘关系)
④ 本地套接字 (最稳定, 网络中, 实现复杂)
管道
可以使用pipe函数进行创建, 以及命令mkfifo命令, 通过文件系统中的某一个文件名来建立,它允许无关进程之间的通信。使用有名管道时,需要先用 mkfifo 命令创建管道文件,然后通过文件 I/O 操作来进行数据的读写。在使用完毕后,需要手动删除该文件。
管道是一种最基本的IPC机制,作用于有血缘关系的进程之间,完成数据传递。调用pipe系统函数即可创建一个管道。有如下特质:
- 其本质是一个伪文件(实为内核缓冲区)
- 由两个文件描述符引用,一个表示读端,一个表示写端。
- 规定数据从管道的写端流入管道,从读端流出。
管道的原理: 管道实为内核使用环形队列机制,借助内核缓冲区(4k)实现。
管道的局限性:
① 数据不能进程自己写,自己读。
② 管道中数据不可反复读取。一旦读走,管道中不再存在。
③ 采用半双工通信方式,数据只能在单方向上流动。
常见的通信方式有,单工通信、半双工通信、全双工通信。
pipe
c
int pipe(int pipefd[2]);
pipe() creates a pipe, a unidirectional data channel that can be used for interprocess communication. The array pipefd is used to return two file descriptors referring to the ends of the pipe. pipefd[0] refers to the read end of the pipe. pipefd[1] refers to the write end of the pipe. Data written to the write end of the pipe is buffered by the kernel until it is read from the read end of the pipe.
参数是返回两个文件描述符, 0用来读, 1用来写, 这两个管道是被打开的
成功的话返回0
c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
int main(int argc, char *argv[]){
int ret;
int fd[2];
pid_t pid;
ret = pipe(fd);
if(ret == -1){
perror("pipe error");
exit(1);
}
pid = fork();
if(pid > 0){
//parent
close(fd[0]);//close read pipe
write(fd[1], "hello pipe", strlen("hello pipe"));
close(fd[1]);//关闭写的管道
}else if(pid == 0 ){
sleep(1);
char temp;
close(fd[1]);
while(read(fd[0], &temp, 1)){
putc(temp, stdout);
}
close(fd[0]);
}
}
使用管道需要注意以下4种特殊情况(假设都是阻塞I/O操作,没有设置O_NONBLOCK标志):
如果所有指向管道写端的文件描述符都关闭了(管道写端引用计数为0),而仍然有进程从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样。
如果有指向管道写端的文件描述符没关闭(管道写端引用计数大于0),而持有管道写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回。
如果所有指向管道读端的文件描述符都关闭了(管道读端引用计数为0),这时有进程向管道的写端write,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止。当然也可以对SIGPIPE信号实施捕捉,不终止进程。具体方法信号章节详细介绍。
如果有指向管道读端的文件描述符没关闭(管道读端引用计数大于0),而持有管道读端的进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再次write会阻塞,直到管道中有空位置了才写入数据并返回。
总结:
① 读管道: 1. 管道中有数据,read返回实际读到的字节数。
2. 管道中无数据:
(1) 管道写端被全部关闭,read返回0 (好像读到文件结尾)
(2) 写端没有全部被关闭,read阻塞等待(不久的将来可能有数据递达,此时会让出cpu)
② 写管道: 1. 管道读端全部被关闭, 进程异常终止(也可使用捕捉SIGPIPE信号,使进程不终止)
2. 管道读端没有全部关闭:
(1) 管道已满,write阻塞。
(2) 管道未满,write将数据写入,并返回实际写入的字节数。
c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
int main(int argc, char *argv[]){
int ret;
int fd[2];
pid_t pid;
ret = pipe(fd);
if(ret == -1){
perror("pipe error");
exit(1);
}
pid = fork();
if(pid > 0){
//parent
close(fd[0]);//close read pipe
dup2(fd[1], STDOUT_FILENO);
execlp("ls" ,"ls", "-l", NULL);
close(fd[1]);
}else if(pid == 0 ){
sleep(1);
close(fd[1]);
dup2(fd[0], STDIN_FILENO);
execlp("wc", "wc", "-l", NULL);
close(fd[0]);
}
}
注意
如果某一个进程里面没有使用这一个管道, 需要把这一个管道对应的文件关闭
管道缓冲区大小
可以使用ulimit –a 命令来查看当前系统中创建管道文件所对应的内核缓冲区大小。通常为:
pipe size (512 bytes, -p) 8
也可以使用fpathconf函数,借助参数 选项来查看。使用该宏应引入头文件<unistd.h>
long fpathconf(int fd, int name); 第二个参数_PC_PIPE_BUF成功:返回管道的大小 失败:-1,设置errno
管道的优劣
优点:简单,相比信号,套接字实现进程间通信,简单很多。
缺点:1. 只能单向通信,双向通信需建立两个管道。
- 只能用于父子、兄弟进程(有共同祖先)间通信。该问题后来使用fifo有名管道解决。
FIFO
FIFO常被称为命名管道,以区分管道(pipe)。管道(pipe)只能用于“有血缘关系”的进程间。但通过FIFO,不相关的进程也能交换数据。
FIFO是Linux基础文件类型中的一种。但,FIFO文件在磁盘上没有数据块,仅仅用来标识内核中一条通道。各进程可以打开这个文件进行read/write,实际上是在读写内核通道,这样就实现了进程间通信。
创建方式:
- 命令:mkfifo 管道名
- 库函数:int mkfifo(const char *pathname, mode_t mode);Mode是这一个文件的权限 成功:0; 失败:-1
一旦使用mkfifo创建了一个FIFO,就可以使用open打开它,常见的文件I/O函数都可用于fifo。如:close、read、write、unlink等。
1)、如果当前打开操作是为读而打开FIFO时,若已经有相应进程为写而打开该FIFO,则当前打开操作将成功返回;否则,可能阻塞直到有相应进程为写而打开该FIFO(当前打开操作设置了阻塞标志);或者,成功返回(当前打开操作没有设置阻塞标志)。 2)、如果当前打开操作是为写而打开FIFO时,如果已经有相应进程为读而打开该FIFO,则当前打开操作将成功返回;否则,可能阻塞直到有相应进程为读而打开该FIFO(当前打开操作设置了阻塞标志);或者,返回ENXIO错误(当前打开操作没有设置阻塞标志)。
总之,一旦设置了阻塞标志,调用mkfifo建立好之后,那么管道的两端读写必须分别打开,有任何一方未打开,则在调用open的时候就阻塞。对管道或者FIFO调用lseek,返回ESPIPE错误。
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/stat.h>
#include "string.h"
#include "fcntl.h"
int main(int argc, char *argv){
int ret = mkfifo("myfifo", 0664);
if(ret < 0){
perror("mkfifo err");
exit(1);
}
pid_t pid = fork();
if(pid == 0){
int fd2 = open("myfifo", O_WRONLY);
int num = write(fd2, "hello\n", strlen("hello\n"));
if(ret < 0){
perror("write error");
}
printf("write : %d\n", num);
close(fd2);
}else{
int fd1 = open("myfifo", O_RDONLY);
char buf[10];
int num = read(fd1, buf, strlen("hello\n"));
if(ret < 0){
perror("read error");
}
printf("read : %d\n", num);
close(fd1);
printf(buf);
}
}
存储映射I/O
使一个磁盘文件和存储空间里面的一个缓冲区映射, 从缓冲区里面读取数据实际就是从文件里面读取数据
把数据存入缓冲区实际是写文件, 可以在不使用read write的情况下完成文件的读写
可以使用mmap函数进行
mmap进行存储映射
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
addr: 指定映射区域的位置, 一般使用NULL,系统自动分配
length: 这一个区域的大小
port: 共享内存映射区域的属性PORT_READ, PORT_EXEC, PORT_WRITE, PORT_NONE
flags: 这一块共享内存的共享属性MAP_SHARED, MAP_PRIVATE, 这一个会决定这一个文件会不会反映到磁盘
fd: 用于创建的那一个文件的文件描述符
offset: 偏移, 这一个需要时4K的倍数, 这一个值是0的时候默认是这一个文件的全部
返回值: 这一个映射区域的首地址, 失败的话返回一个MAP_FAILED(实际是(void *)-1)
munmap取消映射
int munmap(void *addr, size_t length);
addr, mmap的返回值
length:大小
c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/mman.h>
#include <fcntl.h>
int main(int argc, char *argv[]){
char *p = NULL;
int fd = open("test.map", O_RDWR | O_CREAT | O_TRUNC, 0644);
if(fd == -1){
perror("creat errro");
exit(0);
}
/*
lseek(fd, 10, SEEK_END);
write(fd, "\0", 1);
*/
//进行映射
ftruncate(fd, 20);//拓展文件
int len = lseek(fd, 0, SEEK_END);
p = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if(p == MAP_FAILED){
perror("mmap error");
exit(1);
}
//测试
strcpy(p, "hello mmap");
printf("----%s----", p);
int ret = munmap(p, len);
if(ret == -1){
perror("unmap error");
exit(0);
}
return 0;
}
常见错误
实际使用的时候这一个文件的大小需要和映射的大小相同
文件大小为0, 出现总线错误, 映射区域大小为0, 返回无效参数, 文件的权限不对也是无效参数
这一个文件必须要有读权限, 映射的时候需要读文件
文件的描述符mmap之后可以关闭, 之后可以用地址访问
offset是4096的倍数
注:
- 这一个申请的内存不可以越界访问
- 获取的地址不能释放了, munmap会失败
- 使用私有的时候这一个文件只需要有读权限
实际使用(最保险的)
- open: 使用O_RDWR
- mmap(NULL, 有效的文件大小, PORT_READ | PORT_WRITE, MAP_SHARED, fd, 0)
父子进程间的通信
父子等有血缘关系的进程之间也可以通过mmap建立的映射区来完成数据通信。但相应的要在创建映射区的时候指定对应的标志位参数flags:
MAP_PRIVATE: (私有映射) 父子进程各自独占映射区;
MAP_SHARED: (共享映射) 父子进程共享映射, 应该使用这一个
- mmap建立映射
- 使用fork创建子进程
无关进程之间的通信
打开文件的时候使用同一个文件就可以了
匿名映射
使用映射的时候需要有一个文件, 但是这一个文件不需要一直存在, 这时候可以使用unlink对这一个文件进行删除, 这一个文件不被使用以后会被自动回收
可以直接使用匿名映射来代替。其实Linux系统给我们提供了创建匿名映射区的方法,无需依赖一个文件即可创建映射区。同样需要借助标志位参数flags来指定。
使用MAP_ANONYMOUS (或MAP_ANON), 如:
int *p = mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
"4"随意举例,该位置表大小,可依实际需要填写。
The mapping is not backed by any file; its contents are initialized to zero. The fd argument is ignored; however, some implementations require fd to be -1 if MAP_ANONYMOUS (or MAP_ANON) is specified, and portable applications should ensure this. The offset argument should be zero. The use of MAP_ANONYMOUS in conjunction with MAP_SHARED is supported on Linux only since kernel 2.4.
需注意的是,MAP_ANONYMOUS和MAP_ANON这两个宏是Linux操作系统特有的宏。在类Unix系统中如无该宏定义,可使用如下两步来完成匿名映射区的建立。
① fd = open("/dev/zero", O_RDWR);
② p = mmap(NULL, size, PROT_READ|PROT_WRITE, MMAP_SHARED, fd, 0);
/dev/null : 在类Unix系统中,/dev/null,或称空设备,是一个特殊的设备文件,它丢弃一切写入其中的数据(但报告写入操作成功),读取它则会立即得到一个EOF。 在程序员行话,尤其是Unix行话中,/dev/null 被称为位桶(bit bucket)或者黑洞(black hole)。空设备通常被用于丢弃不需要的输出流,或作为用于输入流的空文件。这些操作通常由重定向完成。
/dev/zero : 在类UNIX 操作系统中, /dev/zero 是一个特殊的文件,当你读它的时候,它会提供无限的空字符(NULL, ASCII NUL, 0x00)。
其中的一个典型用法是用它提供的字符流来覆盖信息,另一个常见用法是产生一个特定大小的空白文件。BSD就是通过mmap把/dev/zero映射到虚地址空间实现共享内存的。可以使用mmap将/dev/zero映射到一个虚拟的内存空间,这个操作的效果等同于使用一段匿名的内存(没有和任何文件相关)。
值得注意的是:MAP_ANON和 /dev/zero 都不能应用于非血缘关系进程间通信