一.进程间通讯

1.1 什么是进程间通信

我们运行起来的进程,相互之间资源是独立的,不能在一个进程中直接访问另一个进程的资源。
但是很多时候不同的进程需要进行信息的交互和状态的传递等,譬如数据传输,一个进程需要将它的数据发送给另一个进程,或者多个进程间资源共享,或者一个进程需要控制另一个进程的执行,再或者,一个进程要给另一个进程发送消息等,就需要进程间通信( IPC:Inter Processes Communication )。

1.2 进程间通信的方式

进程间通信的方式有很多,文件、管道、信号、共享内存映射、消息队列、套接字、命名管道等。这里说管道pipe,命名管道fifo,共享内存映射。

二.管道PIPE

2.1 管道概述

管道是一种最基本的IPC机制,也称匿名管道、无名管道,应用于有血缘关系的进程之间,完成数据传递。
所有的 UNIX 系统都支持这种通信机制。

管道的本质是一块内核缓冲区,由两个文件描述符引用,一个表示读端,一个表示写端。
管道是半双工,规定数据从管道的写端流入管道,从读端流出,当两个进程都终结的时候,管道也自动消失,管道的读端和写端默认都是阻塞的。

管道的数据一旦被读走,便不在管道中存在,不可反复读取,数据只能在一个方向上流动,若要实现双向流动,必须使用两个管道。

只能在有血缘关系的进程间使用管道

创建管道非常简单,调用pipe函数即可

2.2 pipe函数

#include <unistd.h>
​
int pipe(int pipefd[2]);
功能:创建无名管道。
​
参数:
    pipefd : 为 int 型数组的首地址,其存放了管道的文件描述符 pipefd[0]、pipefd[1]。
    
    当一个管道建立时,它会创建两个文件描述符 fd[0] 和 fd[1]。其中 fd[0] 固定用于读管道,而 fd[1] 固定用于写管道。一般文件 I/O的函数都可以用来操作管道(lseek() 除外)。
​
返回值:
    成功:0
    失败:-1

代码示例

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>

/* pipe函数演示 */
int main(){
    int fd[2];
    int ret = pipe(fd); //创建管道

    if(ret<0){ 
        //创建管道失败
        perror("pipe");
        return -1;
    }

    pid_t pid = fork();
    if(pid < 0){
        // 创建进程失败
        perror("fork");
        return -1;
    }else if(pid > 0){
        // 父进程 
        close(fd[0]);
        // 父进程写入fd[1]
        write(fd[1],"hello",strlen("hello"));
        wait(NULL);

    }else {
        // 子进程读取
        char buf[128];
        memset(buf,0,sizeof(buf));
        int n = read(fd[0],buf,sizeof(buf));
        printf("%s\n",buf);
    }
}

2.3 管道的读写行为注意事项

这里假定都是阻塞I/O操作,我们使用管道需要注意以下4种特殊情况:
1) 如果所有指向管道写端的文件描述符都关闭了,而仍然有进程从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样。

2) 如果有指向管道写端的文件描述符没关闭,而持有管道写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回。

3) 如果所有指向管道读端的文件描述符都关闭了,这时有进程向管道的写端write,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止。当然也可以对SIGPIPE信号实施捕捉,不终止进程。具体方法信号章节详细介绍。

4) 如果有指向管道读端的文件描述符没关闭,而持有管道读端的进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再次write会阻塞,直到管道中有空位置了才写入数据并返回。

默认管道的读写两端为阻塞的IO操作,但也可以设置为非阻塞,方法也很简单,就是使用fcntl函数,设置O_NONBLOCK标志标志。

//获取原来的flags
int flags = fcntl(fd[0], F_GETFL);
// 设置新的flags
flag |= O_NONBLOCK;
// flags = flags | O_NONBLOCK;
fcntl(fd[0], F_SETFL, flags);

2.4 查看缓冲区

管道本质是一个内核缓冲区,那么如何查看缓冲区大小呢?
1)ulimit -a

(base) zhaow@zhaow-610:~$ ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 62944
max locked memory       (kbytes, -l) 65536
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8 // 管道缓冲区大小
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 62944
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

2)fpathconf函数

#include <unistd.h>
​
long fpathconf(int fd, int name);
功能:该函数可以通过name参数查看不同的属性值
参数:
    fd:文件描述符
    name:
        _PC_PIPE_BUF,查看管道缓冲区大小
        _PC_NAME_MAX,文件名字字节数的上限
返回值:
    成功:根据name返回的值的意义也不同。
    失败: -1

示例

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <fcntl.h>

int main(){
    int fd[2];
    int ret = pipe(fd);
    if(ret <0){
        perror("pipe");
        return -1;
    }
    printf("pipe read size==[%ld]\n", fpathconf(fd[0], _PC_PIPE_BUF));
    printf("pipe write size==[%ld]\n", fpathconf(fd[1], _PC_PIPE_BUF));

    return 0;

}

三.命名管道(FIFO)

3.1 概述

管道(pipe)只能用于“有血缘关系”的进程间通信,为了弥补这个缺陷,提出了命名管道(FIFO),也叫有名管道、FIFO文件。

命名管道(FIFO)提供了一个路径名与之关联,以 FIFO 的文件形式存在于文件系统中,这样即使与 FIFO 的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过 FIFO 相互通信,因此,通过 FIFO 不相关的进程也能交换数据。

FIFO是Linux基础文件类型中的一种(文件类型为p,可通过ls -l查看文件类型)。
FIFO文件在磁盘上没有数据块,仅仅用来标识内核中一条通道,进程打开这个文件进行read/write,实际上是在读写内核缓冲区。

3.2 创建fifo

1)命令mkfifo

(base) zhaow@zhaow-610:demo$ mkfifo myfifo
(base) zhaow@zhaow-610:demo$ ls -l
总用量 0
prw-rw-r-- 1 zhaow zhaow 0 8月  21 02:42 myfifo  //文件类型 p

2)函数mkfifo

#include <sys/types.h>
#include <sys/stat.h>
​
int mkfifo(const char *pathname, mode_t mode);
功能:
    命名管道的创建。
参数:
    pathname : 普通的路径名,也就是创建后 FIFO 的名字。
    mode : 文件的权限,与打开普通文件的 open() 函数中的 mode 参数相同。(0666)
返回值:
    成功:0   状态码
    失败:如果文件已经存在,则会出错且返回 -1。

3.3 代码示例

进程A 写入

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
    //创建fifo文件
    // 1.检查是否存在,不存在则创建
    int ret = access("./myfifo", F_OK);
    if(ret!=0)
    {        
        ret = mkfifo("./myfifo", 0777);
        if(ret<0)
        {
            perror("mkfifo error");
            return -1;
        }
    }

    //打开文件
    int fd = open("./myfifo", O_RDWR);
    if(fd<0)
    {
        perror("open error");
        return -1;
    }

    //写fifo文件
    int i = 0;
    char buf[64];
    while(1)
    {
        memset(buf, 0x00, sizeof(buf));
        sprintf(buf, "%d:%s", i, "hello world");
        write(fd, buf, strlen(buf));
        sleep(1);

        i++;
    }

    
    close(fd);


    return 0;
}

进程B 读取

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>


int main(){
    // 1.检查要创建的fifo文件是否存在,若不存在则创建
    int ret = access("./myfifo",F_OK);
    if(ret != 0){
        ret = mkfifo("./myfifo",0777);
        if(ret < 0){
            perror("mkfifo");
            return -1;
        }
    }

    // 打开fifo

    int fd = open("./myfifo",O_RDWR);
    if(fd<0){
        perror("open");
        return -1;
    }

    // 读取
    int n;
    char buf[128];
    while (1)
    {
        memset(buf,0x00,sizeof(buf));
        n = read(fd,buf,sizeof(buf));
        printf("buf=[%s]\n",buf);
    }
    
    close(fd);

    return 0;
}

四.共享存储映射

4.1 概述

存储映射I/O (Memory-mapped I/O) 就是使一个磁盘文件与存储空间中的一个缓冲区相映射。
从缓冲区中取数据,就如同读文件中的相应字节;同样,将数据写入缓冲区,则会将数据写入文件。

4.2 存储映射函数

1)mmap

#include <sys/mman.h>
​
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
1 用途:一个文件或者其它对象映射进内存)
2 参数说明:
    addr :  指定映射的起始地址, 通常设为NULL, 由系统指定
    length:映射到内存的文件长度
    prot:  映射区的保护方式, 常用的三种
        1) 读:PROT_READ
        2) 写:PROT_WRITE
        3) 读写:PROT_READ | PROT_WRITE
    flags:  映射区的特性, 可以是
        1) MAP_SHARED : 写入映射区的数据会复制回文件, 且允许其他映射该文件的进程共享。
        2) MAP_PRIVATE : 对映射区的写入操作会产生一个映射区的复制(copy - on - write), 对此区域所做的修改不会写回原文件。
    fd:由open返回的文件描述符, 代表要映射的文件。
    offset:以文件开始处的偏移量, 必须是4k的整数倍, 通常为0, 表示从文件头开始映射
返回值:
    成功:返回创建的映射区首地址
    失败:MAP_FAILED宏
​

2)munmap


#include <sys/mman.h>
​
int munmap(void *addr, size_t length);
功能:
    释放内存映射区
参数:
    addr:使用mmap函数创建的映射区的首地址
    length:映射区的大小
返回值:
    成功:0
    失败:-1

代码示例 父子进程间通信

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/mman.h>

int main(){
    int fd = open("./ts.log",O_RDWR); //当映射文件大小为0时,不能创建映射区,所以,用于映射的文件必须要有实际大小;
    if(fd < 0){
        perror("open");
        return -1;
    }

    int len = lseek(fd,0,SEEK_END);

    void * addr = mmap(NULL,len,PROT_READ |PROT_WRITE,MAP_SHARED,fd,0);

    if(addr == MAP_FAILED){ // mmap创建映射区出错概率非常高,一定要检查返回值,确保映射区建立成功再进行后续操作
        perror("mmap");
        return -1;
    }

    close(fd); //映射区的释放与文件关闭无关,只要映射建立成功,文件可以立即关闭。

    pid_t pid = fork();
    if(pid < 0){
        perror("fork");
        return -1;
    }else if(pid > 0){
        memcpy(addr,"hello",strlen("hello"));
        wait(NULL);
    }else{
        char buf[64];
        memset(buf,0x00,sizeof(buf));
        memcpy(buf,addr,5);
        printf("%s\n",buf);
    }

    return 0;
}

4.3 匿名映射实现父子进程通信

使用映射区来完成文件读写操作十分方便,父子进程间通信也较容易,但比较麻烦的是,每次创建映射区一定要依赖一个文件才能实现,为了建立映射区要open一个temp文件,创建好了再unlink、close掉,很麻烦。
Linux系统提供了创建匿名映射区的方法,无需依赖一个文件即可创建映射区。同样需要借助标志位参数flags来指定。

int *p = mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
参数说明:
    4"随意,该位置表示映射区大小,可依实际需要填写。
    MAP_ANONYMOUS和MAP_ANON这两个宏是Linux操作系统特有的宏。

代码示例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/mman.h>

int main(){
    void * addr = mmap(NULL,4096,PROT_READ|PROT_WRITE,MAP_SHARED | MAP_ANONYMOUS,-1,0);
    if(addr==MAP_FAILED)
    {
        perror("mmap");
        return -1;
    }

    pid_t pid  = fork();
    if(pid < 0){
        perror("fork");
        return -1;
    }else if(pid > 0){
        memcpy(addr,"hello",strlen("hello"));
        wait(NULL);
    }else{
        char buf[64];
        memset(buf,0x00,strlen(buf));
        memcpy(buf,addr,5);
        printf("%s\n",buf);
    }

    return 0;
}

leafgood
183 声望15 粉丝

踏入IT这个圈子,就成了一名玩家,想要走的更远,就要不断给自己技能加点才行。


引用和评论

0 条评论