IPC方法-管道/fifo/mmap

进程间通信方法:

  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)

  • addr映射区首地址由内核MMU指定, 直接填NULL.
  • length是映射区大小, 不可以为0, 所以新创建的文件必须通过lseek或ftruncate函数来拓展. 如果是已创建的文件, 映射区也要小于该文件的大小.
  • prot即映射区权限, 有三种PROT_READ, PROT_WRITE, PROT_READ|PROT_WRITE. 当flags为MAP_SHARED时, 映射区权限必须小于等于文件open时的权限, 且open必须包含读权限(因为mmap创建时会隐含着对文件读的操作). 当flags为MAP_PRIVATE时, 映射区权限可以大于文件权限, 但文件仍需要有读权限.
  • flags设置映射区所做修改是否会反映到物理设备上(磁盘文件), MAP_SHARED会, MAP_PRIVATE不会.
  • fd即用来建立映射区的文件.
  • offset是映射文件的偏移, 必须为4k的整数倍(一页的大小), 表示截取文件一部分进行映射, 0是不偏移.

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))等操作内存的函数进行写入.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 227,401评论 6 531
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 98,011评论 3 413
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 175,263评论 0 373
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 62,543评论 1 307
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 71,323评论 6 404
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 54,874评论 1 321
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 42,968评论 3 439
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 42,095评论 0 286
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 48,605评论 1 331
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 40,551评论 3 354
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 42,720评论 1 369
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 38,242评论 5 355
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 43,961评论 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 34,358评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 35,612评论 1 280
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 51,330评论 3 390
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 47,690评论 2 370