进程之间的通信
每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进行之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷贝到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程之间通信(IPC)
进程间通信
1.pipe管道
可以用环形队列实现。队列满的话会阻塞。管道是一种最基本的IPC机制,由pipe函数创建
#include<unistd.h> int pipe(int filedes[2]);
管道作用于有血缘关系的进程之间,通过fork来传递。
调用pipe后,父进程创建管道,fd[1]管道写端,fd[0]管道读端,都是文件描述符,描述符分配是未被使用的最小单元,若最小未被使用的文件描述符是3,则3记录管道的读端,4记录管道的写端,总的来说读端的文件描述符较小,写端的文件描述符较大。父进程fork出子进程,上图中的左边是父进程,右边是子进程。子进程会进程父进程的文件描述表,3仍然指向管道的读端,4指向写端。创建好管道后,要确定好通信方向,有父写子读(关闭父读,关闭子写)和子写父读(关闭子读,关闭父写)两种选择,是单工方式工作。若需要双向通信,需要创建管道,仍是先创建管道,后fork。
#include <stdio.h> #include <string.h> #include <unistd.h> #include <stdlib.h> #include <fcntl.h> #include <errno.h> int main(void) { int fd[2]; char str[1024] = "hello itcast"; char buf[1024]; pid_t pid; //fd[0] 读端 //fd[1] 写端 if (pipe(fd) < 0) { perror("pipe"); exit(1); } pid = fork(); //父写子读 if (pid > 0) { //父进程里,关闭父读 close(fd[0]); sleep(5); write(fd[1], str, strlen(str)); close(fd[1]); wait(NULL); } else if (pid == 0) { int len, flags; //子进程里,关闭子写 close(fd[1]); flags = fcntl(fd[0], F_GETFL); flags |= O_NONBLOCK; fcntl(fd[0], F_SETFL, flags); tryagain: len = read(fd[0], buf, sizeof(buf)); if (len == -1) { if (errno == EAGAIN) { write(STDOUT_FILENO, "try again\n", 10); sleep(1); goto tryagain; } else { perror("read"); exit(1); } } write(STDOUT_FILENO, buf, len); close(fd[0]); } else { perror("fork"); exit(1); } return 0; }
运行结果:
使用管道需要注意以下4种特殊情况(假设都是阻塞I/O操作,没有设置O_NONBLOCK标志):
(1) 如果所有指向管道写端的文件描述符都关闭了(管道写端的引用计数等于0),而仍然有进程从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样。
(2)如果有指向管道写端的文件描述符没关闭(管道写端的引用计数大于0),而持有管道写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回。
(3) 如果所有指向管道读端的文件描述符都关闭了(管道读端的引用计数等于0),这时有进程向管道的写端write,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止
(4) 如果有指向管道读端的文件描述符没关闭(管道读端的引用计数大于0),而持有管道读端的进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再次write会阻塞,直到管道中有空位置了才写入数据并返回。
简而言之:写关闭,读端读取管道里内容时,再次读,返回0,相当于读到EOF;写端未关闭,写端暂时无数据,读端读完管道里数据时,再次读会阻塞;读端关闭,写端写管道,产生SIGPIPE信号,写进程默认情况下会终止进程;读端未读管道数据,当写端写满管道后,再次写,阻塞。
管道的这四种特殊情况具有普遍意义。
非阻塞管道,fcntl函数设置O_NONBLOCK标志
fpathconf(int fd,int name)测试管道缓冲区大小,_PC_PIPE_BUF。
2.fifo有名管道
创建一个有名管道,解决无血缘关系的进程通信,fifo:
fifo是一个索引节点,不会再磁盘下留下任何大小,所以没有myfifo的大小为0;
//写管道#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <fcntl.h> #include <sys/stat.h> #include <string.h> void sys_err(char *str, int exitno) { perror(str); exit(exitno); } int main(int argc, char *argv[]) { int fd; char buf[1024] = "hello xwp\n"; if (argc < 2) { printf("./a.out fifoname\n"); exit(1); } //fd = open(argv[1], O_RDONLY); fd = open(argv[1], O_WRONLY); if (fd < 0) sys_err("open", 1); write(fd, buf, strlen(buf)); close(fd); return 0; }
//读管道 #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <fcntl.h> #include <sys/stat.h> #include <string.h> void sys_err(char *str, int exitno) { perror(str); exit(exitno); } int main(int argc, char *argv[]) { int fd, len; char buf[1024]; if (argc < 2) { printf("./a.out fifoname\n"); exit(1); } fd = open(argv[1], O_RDONLY); if (fd < 0) sys_err("open", 1); len = read(fd, buf, sizeof(buf)); write(STDOUT_FILENO, buf, len); close(fd); return 0; }
gcc fifo_w.c -o fifo_w
gcc fifo_r.c -o fifo_r
./fifo_w myfifo
./fifo_r myfifo
//函数形式,在编程中使用#include<sys/types.h> #include<sys/stat.h> int mkfifo(const char *pathname,mode_t mode);
3.内存共享映射
mmap/munmap
mmap可以把磁盘文件的一部分直接映射到内存,这样文件中的位置直接就对应内存地址,对文件的读写可以直接用指针而不需要read/write函数。
mmap
#include<sys/mman.h>
void *mmap(void *addr,size_t length,int prot,int flags,int fd,off_t offsize);
具体参数含义
addr : 指向欲映射的内存起始地址,通常设为 NULL,代表让系统自动选定地址,映射成功后返回该地址。
length: 代表将文件中多大的部分映射到内存。
prot : 映射区域(内存)的保护方式。可以为以下几种方式的组合:
PROT_EXEC 映射区域可被执行
PROT_READ 映射区域可被读取
PROT_WRITE 映射区域可被写入
PROT_NONE 映射区域不能存取
flags : 影响映射区域的各种特性。在调用mmap()时必须要指定MAP_SHARED 或MAP_PRIVATE。
MAP_FIXED 如果参数start所指的地址无法成功建立映射时,则放弃映射,不对地址做修正。通常不鼓励用此旗标。
MAP_SHARED 对映射区域的写入数据会复制回文件内,而且允许其他映射该文件的进程共享。
MAP_PRIVATE 对映射区域的写入操作会产生一个映射文件的复制,即私人的“写入时复制”(copy on write)对此区域作的任何修改都不会写回原来的文件内容。
MAP_ANONYMOUS建立匿名映射。此时会忽略参数fd,不涉及文件,而且映射区域无法和其他进程共享。
MAP_DENYWRITE只允许对映射区域的写入操作,其他对文件直接写入的操作将会被拒绝。
MAP_LOCKED 将映射区域锁定住,这表示该区域不会被置换(swap)。
fd : 要映射到内存中的文件描述符。如果使用匿名内存映射时,即flags中设置了MAP_ANONYMOUS,fd设为-1。有些系统不支持匿名内存映射,则可以使用fopen打开/dev/zero文件,
然后对该文件进行映射,可以同样达到匿名内存映射的效果。
offset:文件映射的偏移量,通常设置为0,代表从文件最前方开始对应,offset必须是PAGE_SIZE(页面大小)的整数倍。
返回值:
若映射成功则返回映射区的内存起始地址,否则返回MAP_FAILED(-1),错误原因存于errno 中。
错误代码:
EBADF 参数fd 不是有效的文件描述词
EACCES 存取权限有误。如果是MAP_PRIVATE 情况下文件必须可读,使用MAP_SHARED则要有PROT_WRITE以及该文件要能写入。
EINVAL 参数start、length 或offset有一个不合法。
EAGAIN 文件被锁住,或是有太多内存被锁住。
ENOMEM 内存不足。
用户层的调用很简单,其具体功能就是直接将物理内存直接映射到用户虚拟内存,使用户空间可以直接对物理空间操作。但是对于内核层而言,其具体实现比较复杂。
#include <stdio.h> #include <unistd.h> #include <sys/stat.h> #include <sys/types.h> #include <fcntl.h> #include <sys/mman.h> #include <stdlib.h> int main(void) { int fd, len; int *p; fd = open("hello", O_RDWR); if (fd < 0) { perror("open"); exit(1); } len = lseek(fd, 0, SEEK_END); p = mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); if (p == MAP_FAILED) { perror("mmap"); exit(1); } close(fd);
//映射并没有解除
p[0] = 0x30313233; munmap(p, len); return 0; }
修改磁盘文件时,对应映射的内存也会改变。
mmap的实现原理