IPC方法-管道/fifo/mmap

2020-03-08  本文已影响0人  D_Major

进程间通信方法:

  1. 管道(只能血缘关系)/fifo(非血缘关系), 队列只能读一次
  2. 信号(开销最小)
  3. 共享内存/共享映射区(非血缘关系, 可反复读取)
  4. 本地套接字(最复杂最稳定)

不同进程的内核地址空间指向内存的同一块地方, 我们可以用文件描述符表使两个进程修改同一个文件来通信(开销大已废弃), 也可以在内核区内使用一个管道进行通信(有血缘关系间单向流动).

管道

管道是内核缓冲区, 是不占用磁盘空间的伪文件, 读写两端分别对应两个fd, 数据写端流入读端流出. 常用的|(pipe())称作匿名管道, fifo为有名管道. 操作管道的进程被销毁之后, 管道自动被释放. 管道默认是阻塞的(读写操作均是, 所以不需要sleep()), 其数据结构是环形队列(缺点是只能读取一次), 大小默认为4k, 会适当调整.ulimit -a可以查看管道缓冲区大小.
int pipe(int fd[2])创建匿名管道, 传出参数为读写两端的文件描述符数组. fd[0]读, fd[1]写. 注意先pipe()后fork(), 使子进程继承父进程的PCB.

示例: 使用pipe函数实现ps aux | grep bash, 思路是用dup2()把标准输出重定向到写端(ps函数), 然后把标准输入重定向到读端(grep 函数). 由于队列只能读一次且是单向的, 所以读的那一方要关闭写端, 写的那一方要关闭读端(close(fd[0])). 如果需要双向通信, 需要两根管道.

/* 父子进程pipe单向通信 */
int fd[2];
int ret = pipe(fd);    // 先pipe后fork, 所以父子进程的fd编号是相同的
if (ret == -1)
{
    perror("pipe error");
    exit(1);
}
pid_t pid = fork();
if (pid == -1)
{
    perror("fork error");
    exit(1);
}
if (pid > 0)
{   // 父进程
    close(fd[0]);   // 写管道, 关闭读端
    dup2(fd[1], STDOUT_FILENO);  // 标准输出重定向到写端
    execlp("ps", "ps", "aux", NULL);
    perror("execlp");  // exec函数族正确时候无返回值, 错误才返回, 所以不用判断
    exit(1);
}
else if (pid == 0)
{
    close(fd[1]);
    dup2(fd[0], STDIN_FILENO);
    execlp("grep", "grep", "--color=auto", "bash", NULL);
    perror("execlp");
    exit(1);
}
close(fd[0]);
close(fd[1]);
return 0;

如果是兄弟进程间通信, 父进程的读写两端都要关闭, 只负责回收PCB.
示例: 兄弟进程使用pipe函数实现ps aux | grep bash, 先循环创建子进程, 通过i而不是pid来判断父子进程, 子进程1执行ps, 子进程2执行grep, 内部代码不变, 父进程把fd[0]和fd[1]都关闭并回收子进程.

/* 兄弟进程pipe单向通信, 并由父进程回收 */
int i;
for (i=0; i<2; i++) 
{
    pid_t pid = fork();
    if (pid == -1)
    {
        perror("fork error");
        exit(1);
    } 
    else if (pid == 0) 
        break;
}
if (i == 0) { 子进程1执行ps, 同上 }
else if (i == 1) { 子进程2执行grep, 同上 }
else if (i == 2) { 
    // 父进程关闭读写端, 回收子进程
    close(fd[0]);
    close(fd[1]);
    // 轮询非阻塞回收子进程, wpid==0说明子进程正在运行, 
    // 如果wpid==-1说明子进程已被回收完毕.
    pid_t = wpid;
    while((wpid = waitpid(-1, NULL, WNOHANG)) != -1)
    {
        if (wpid == 0) continue;
        printf("child died, pid = %d\n", wpid);
    }
}
管道读写行为

读管道: 1. 管道中有数据, read返回读到的字节数. 2. 管道中无数据时, 若写端全关闭, read返回0; 若仍有写端打开, 阻塞等待.
写管道: 1. 读端全关闭, 进程异常终止(SIGPIPE信号). 2. 有读端打开时, 若管道未满, 继续写数据返回字节数; 若管道已满, 阻塞(少见).

设置读端为非阻塞

默认两端都是阻塞的, 这里以读端示例改为非阻塞, 使用fcntl()函数

//获取原来的flags
int flags = fcntl(fd[0], F_GETFL);
//给flags添加新的属性, |=类似于+=, 或操作后再赋值
flags |= O_NONBLOCK
//设置新的flags
fcntl(fd[0], F_SETFL, flags);

fifo

fifo也叫有名管道, 适用于无血缘关系的进程间通信. fifo是一个伪文件(属性为p, 即第一列d/-位置的字符), 其大小永远为0, 并在内核中映射为一个对应的缓冲区. 多个进程使用的fifo必须在同一个目录下, fifo可以有多个读端和多个写端.
创建方式: 1. 命令mkfifo 管道名, 2. 函数mkfifo(pathname, mode), 真实权限为(mode & ~umask).
操作时需要先open(因为是文件), 可以执行read/write操作, 但是不能执行lseek操作(因为是伪文件).

// 非血缘关系进程通信
//先创建fifo文件myfifo
mkfifo("myfifo", 0664);
//然后A进程a.c里执行读操作
int fd = open("myfifo", O_RDONLY);
read(fd, buf, sizeof(buf));
close(fd);
//之后B进程b.c里执行写操作
int fd1 = open("myfifo", O_WRONLY);
write(fd1, "hello,world", 11);
close(fd1);

mmap()

void* mmap(void* addr, size_t length, int prot, int flags, int fd, off_t offset)

mmap()函数成功返回映射区首地址, 失败返回MAP_FAILED. 有了首地址就可以通过指针操作映射区(如映射区声明为char*时可以用strcpy赋值).

使用munmap(p, len)释放映射区, p必须为首地址(不能p++), 失败返回-1, 成功返回0. 文件描述符可以先于mmap映射区关闭, 映射区一旦创建成功就可以操作指针来访问, 不再需要文件的句柄.

进程间通信用来创立映射区的一般是临时文件, 对于临时文件, 可以使用unlink("temp")删除临时文件目录项, 使文件可以在进程不占用后被释放. 目录项(dentry)包含文件名和inode编号, 通过inode编号可以找到inode文件, inode文件中stat结构体包含了存储指针地址, 再通过该指针找到文件在磁盘上的位置. 目录项的本质就是硬链接.

如果不想使用临时文件, 可以使用匿名映射区. flags参数改为MAP_SHARED | MAP_ANONYMOUS, fd参数改为-1. 该宏为linux独有, 在类Unix系统(mac)上, 可以使用/dev/zero文件作为fd传入, 可以提供任意大小的空间. 与之对应的是/dev/null, 可以往里写无限的内容(无底洞).

父子进程只能共享: 1. 打开的文件, 2. mmap建立的映射区(但必须使用MAP_SHARED). 子进程改变映射区的首地址后, 父进程是同步改变的, 所以二者可以共同操作映射区, 其他的全局变量等并不共享.

对于非血缘关系进程通信, 写进程先执行, 拥有读写权限; 读进程后执行, 只有读的权限. 写进程可以使用如memcpy(p, &student, sizeof(student))等操作内存的函数进行写入.

上一篇下一篇

猜你喜欢

热点阅读