linux系统编程-进程通信

一、通信方式

Linux 操作系统支持的主要进程间通信的通信机制:

在这里插入图片描述

二、管道

2.1 无名管道

管道也叫无名管道,它是是 UNIX 系统 IPC(进程间通信) 的最古老形式,所有的 UNIX 系统都支持这种通信机制。

管道有如下特点:

    1. 半双工,数据在同一时刻只能在一个方向上流动。
    1. 数据只能从管道的一端写入,从另一端读出。
    1. 写入管道中的数据遵循先入先出的规则。
    1. 管道所传送的数据是无格式的,这要求管道的读出方与写入方必须事先约定好数据的格式,如多少字节算一个消息等。
    1. 管道不是普通的文件,不属于某个文件系统,其只存在于内存中。
    1. 管道在内存中对应一个缓冲区。不同的系统其大小不一定相同。
    1. 从管道读数据是一次性操作,数据一旦被读走,它就从管道中被抛弃,释放空间以便写更多的数据。
    1. 管道没有名字,只能在具有公共祖先的进程(父进程与子进程,或者两个兄弟进程,具有亲缘关系)之间使用。

对于管道特点的理解,我们可以类比现实生活中管子,管子的一端塞东西,管子的另一端取东西。

管道是一种特殊类型的文件,在应用层体现为两个打开的文件描述符。

2.1.2 无名管道在控制台中执行:

ps aux | grep l

在这里插入图片描述

2.2.2 无名管道在程序中执行:

int pipe(int fd[2])

这里表示创建一个匿名管道,并返回了两个描述符,一个是管道的读取端描述符 fd[0],另一个是管道的写入端描述符 fd[1]。注意,这个匿名管道是特殊的文件,只存在于内存,不存于文件系统中
这样操作会在一个进程中创建一个管道
在这里插入图片描述
其实,所谓的管道,就是内核里面的一串缓存。从管道的一段写入的数据,实际上是缓存在内核中的,另一端读取,也就是从内核中读取这段数据。另外,管道传输的数据是无格式的流且大小受限。

我们可以使用 fork 创建子进程,创建的子进程会复制父进程的文件描述符,这样就做到了两个进程各有两个「 fd[0] 与 fd[1]」,两个进程就可以通过各自的 fd 写入和读取同一个管道文件实现跨进程通信了。
在这里插入图片描述
管道只能一端写入,另一端读出,所以上面这种模式容易造成混乱,因为父进程和子进程都可以同时写入,也都可以读出。那么,为了避免这种情况,通常的做法是:

  • 父进程关闭读取的 fd[0],只保留写入的 fd[1];
  • 子进程关闭写入的 fd[1],只保留读取的 fd[0];
    在这里插入图片描述
    所以说如果需要双向通信,则应该创建两个管道。

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<unistd.h>
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
#define size 64 
int main()
{
    int fd_pipe[2] = { 0 };//1:write;0:read
    pid_t pid;
    if (pipe(fd_pipe) < 0)
    {// 创建管道
        perror("pipe");
    }
    pid = fork(); // 创建
    if (pid == 0)
    { // 子进程
        close(fd_pipe[0]);
        char buf[] = "I am mike";
        // 往管道写端写数据
        printf("i am son and i am writing ...\n");
        write(fd_pipe[1], buf, strlen(buf));
        exit(0);
    }
    else if (pid > 0)
    {// 父进程
        close(fd_pipe[1]);
        wait(NULL); // 等待子进程结束,回收其资源
        char str[50] = { 0 };
        // 从管道里读数据
        printf("i father and i am reading ...\n");
        read(fd_pipe[0], str, sizeof(str));
        printf("str=[%s]\n", str); // 打印数据
    }
    return 0;
}

结果:
在这里插入图片描述

2.2.3 查看管道缓冲区


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

实例:

#include<unistd.h>
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
#define size 64 
int main()
{
    int fd[2];
    int ret = pipe(fd);
    if (ret == -1)
    {
        perror("pipe error");
        exit(1);
    }
    long num = fpathconf(fd[0], _PC_PIPE_BUF);
    printf("num = %ld\n", num);
    return 0;
}

结果:
在这里插入图片描述

2.2.4 无名管道读写特点

读管道:

  • 管道中有数据,read返回实际读到的字节数。

  • 管道中无数据:

    • 管道写端被全部关闭,read返回0 (相当于读到文件结尾)
    • 写端没有全部被关闭,read阻塞等待(不久的将来可能有数据递达,此时会让出cpu)

写管道:

  • 管道读端全部被关闭, 进程异常终止(也可使用捕捉SIGPIPE信号,使进程终止)

  • 管道读端没有全部关闭:

    • 管道已满,write阻塞。
    • 管道未满,write将数据写入,并返回实际写入的字节数。

可以看出会有阻塞的情况发生,使用以下方法可以避免阻塞:
设置为非阻塞的方法

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

fcntl()详解:

#include<unistd.h>
#include<fcntl.h>
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd ,struct flock* lock);

int fcntl(int fd, int cmd, ... /* arg */ );

(经常用这个fcntl函数改变非阻塞)

参数:
fd:文件描述符
cmd:设置的命令
F_GETFL(常用)
F_SETFL(常用)
arg:可有可无,由第二个参数决定,比如get时候没有,set时候有值
返回值:
文件状态标志
-1 :失败

步骤:
int flag;
flag = fcntl(sockfd, F_GETFL, 0);
flag |= O_NONBLOCK;
fcntl(sockfd, F_SETFL, flag);

若你想设置回阻塞,只需要把Flags改为iFlags&~O_NONBLOCK即可。

函数传入值cmd
F_DUPFD:复制一个现存的描述符
F_GETFD:获得fd的close-on-exec(执行时关闭)文件描述符标志,若标志未设置,则文件经过exec()函数之后仍保持打开状态
F_SETFD:设置close-on-exec 标志,该标志由参数arg 的FD_CLOEXEC位决定
F_GETFL:得到open设置的标志
F_SETFL :改变open设置的标志
F_GETLK:根据lock参数值,决定是否可以上文件锁
F_SETLK:设置lock参数值的文件锁

2.2 有名管道

管道,由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了命名管道(FIFO),也叫有名管道、FIFO文件。

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

命名管道(FIFO)和无名管道(pipe)有一些特点是相同的,不一样的地方在于:

  1. FIFO 在文件系统中作为一个特殊的文件而存在,但 FIFO 中的内容却存放在内存中。

  2. 当使用 FIFO 的进程退出后,FIFO 文件将继续保存在文件系统中以便以后使用。

  3. FIFO 有名字,不相关的进程可以通过打开命名管道进行通信。

2.2.1 控制台使用有名管道

创建管道:

mkfifo myPipe

输出数据到管道:

echo "hello" > myPipe

在管道另一边接收结果:

cat < myPipe

结果:
在这里插入图片描述

2.2.3 有名管道在程序中执行

myfifo()

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

操作示范:

//进行1,写操作
int fd = open("my_fifo", O_WRONLY);char send[100] = "Hello Mike";
write(fd, send, strlen(send));//进程2,读操作
int fd = open("my_fifo", O_RDONLY);//等着只写  char recv[100] = { 0 };
//读数据,命名管道没数据时会阻塞,有数据时就取出来  
read(fd, recv, sizeof(recv));
printf("read from my_fifo buf=[%s]\n", recv);

实例(使用两个管道实现聊天功能):
在这里插入图片描述
补充:fgets()

char *fgets(char *str, int n, FILE *stream);
str:这是指向一个字符数组的指针,该数组存储了要读取的字符串。
n:这是要读取的最大字符数(包括最后的空字符)。通常是使用以 str 传递的数组长度。
stream:这是指向 FILE 对象的指针,该 FILE 对象标识了要从中读取字符的流。
返回值:如果成功,该函数返回相同的 str 参数。如果到达文件末尾或者没有读取到任何字符,str 的内容保持不变,并返回一个空指针。如果发生错误,返回一个空指针。
在读字符时遇到end-of-file,则eof指示器被设置,如果还没读入任何字符就遇到这种情况,则stream保持原来的内容,返回NULL;如果发生读入错误,error指示器被设置,返回NULL,stream的值可能被改变。

实例:
用户A:(先读后写)

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include<fcntl.h>
int main()
{
        int fdw = -1;
        int fdr = -1;
        int ret = -1;
        fdr = open("fifo1",O_RDONLY);
        fdw = open("fifo2",O_WRONLY);
        char buf[64]={0};
        printf("chat start!\n");
        while(1)
        {
                memset(buf,0,64);
                ret = read(fdr,buf,64);
                if(ret = -1);
                perror("read");
                printf("read:%s,\n",buf);

                memset(buf,0,64);
                printf("i am writing:\n");
                fgets(buf,64,stdin);
                if('\n' == buf[strlen(buf)-1])
                        buf[strlen(buf)-1] = '\0';
                write(fdw,buf,strlen(buf));
        }
        return 0;
}

用户B:(先写后读)

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include<fcntl.h>
int main()
{
        int fdw = -1;
        int fdr = -1;
        int ret = -1;
        fdw = open("fifo1",O_WRONLY);
        fdr = open("fifo2",O_RDONLY);
        char buf[64]={0};
        printf("chat start!\n");
        while(1)
        {
                memset(buf,0,64);
                printf("i am writing:\n");
                fgets(buf,64,stdin);
                if('\n' == buf[strlen(buf)-1])
                        buf[strlen(buf)-1] = '\0';
                write(fdw,buf,strlen(buf));

                memset(buf,0,64);
                ret = read(fdr,buf,64);
                if(ret = -1)
                perror("read");
                printf("read:%s,\n",buf);

        }
        return 0;
}

结果:
在这里插入图片描述
在这里插入图片描述

三、消息队列-管道改进

前面说到管道的通信方式是效率低的,因此管道不适合进程间频繁地交换数据。

对于这个问题,消息队列的通信模式就可以解决。比如,A 进程要给 B 进程发送消息,A 进程把数据放在对应的消息队列后就可以正常返回了,B 进程需要的时候再去读取数据就可以了。同理,B 进程要给 A 进程发送消息也是如此。

再来,消息队列是保存在内核中的消息链表,在发送数据时,会分成一个一个独立的数据单元,也就是消息体(数据块),消息体是用户自定义的数据类型,消息的发送方和接收方要约定好消息体的数据类型,所以每个消息体都是固定大小的存储块,不像管道是无格式的字节流数据。如果进程从消息队列中读取了消息体,内核就会把这个消息体删除。

消息队列生命周期随内核,如果没有释放消息队列或者没有关闭操作系统,消息队列会一直存在,而前面提到的匿名管道的生命周期,是随进程的创建而建立,随进程的结束而销毁。

消息这种模型,两个进程之间的通信就像平时发邮件一样,你来一封,我回一封,可以频繁沟通了。

但邮件的通信方式存在不足的地方有两点,一是通信不及时,二是附件也有大小限制,这同样也是消息队列通信不足的点。

消息队列不适合比较大数据的传输,因为在内核中每个消息体都有一个最大长度的限制,同时所有队列所包含的全部消息体的总长度也是有上限。在 Linux 内核中,会有两个宏定义 MSGMAX 和 MSGMNB,它们以字节为单位,分别定义了一条消息的最大长度和一个队列的最大长度。

消息队列通信过程中,存在用户态与内核态之间的数据拷贝开销,因为进程写入数据到内核中的消息队列时,会发生从用户态拷贝数据到内核态的过程,同理另一进程读取内核中的消息数据时,会发生从内核态拷贝数据到用户态的过程。

#共享内存

四、共享存储映射-避免用户态与内核态之间的数据拷贝开销

4.1 共享内存概念

存储映射I/O (Memory-mapped I/O) 使一个磁盘文件与存储空间中的一个缓冲区相映射。

共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中。这样这个进程写入的东西,另外一个进程马上就能看到了,都不需要拷贝来拷贝去,传来传去,大大提高了进程间通信的速度
在这里插入图片描述
于是当从缓冲区中取数据,就相当于读文件中的相应字节。于此类似,将数据存入缓冲区,则相应的字节就自动写入文件。这样,就可在不适用read和write函数的情况下,使用地址(指针)完成I/O操作。

共享内存可以说是最有用的进程间通信方式,也是最快的IPC形式, 因为进程可以直接读写内存,而不需要任何数据的拷贝。

4.2 存储映射函数

(1) mmap函数

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

关于mmap函数的使用总结:

    1. 第一个参数写成NULL
    1. 第二个参数要映射的文件大小 > 0
    1. 第三个参数:PROT_READ 、PROT_WRITE
    1. 第四个参数:MAP_SHARED 或者 MAP_PRIVATE
    1. 第五个参数:打开的文件对应的文件描述符
    1. 第六个参数:4k的整数倍,通常为0

(2) munmap函数

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

使用示例:

#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
#include<sys/mman.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
        //以读写方式打开一个文件(此文件用于映射)
        int fd = -1;
        int ret = -1;
        void *addr = NULL;
        fd = open("txt",O_RDWR);
        if(-1 == fd)
        {
                perror("open");
                return 1;
        }
        //将文件映射到内存中
        addr = mmap(NULL,1024,PROT_READ | PROT_WRITE,MAP_SHARED,fd,0);
        if(addr == MAP_FAILED)
        {
                perror("mmap");
                return 1;
        }
        printf("映射成功!")//关闭文件
        close(fd);
        //写映射区
        memcpy(addr,"123456789", 9);
        //断开映射区
        munmap(addr,1024);
        return 0;
}

结果:
txt:
在这里插入图片描述
执行:
在这里插入图片描述
txt:
在这里插入图片描述

4.3 父子进程使用存储映射通信

在这里插入图片描述
实例:

#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
#include<sys/mman.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
int main()
{
        //以读写方式打开一个文件(此文件用于映射)
        int fd = -1;
        int ret = -1;
        void *addr = NULL;
        pid_t pid = -1;
        fd = open("txt",O_RDWR);
        if(-1 == fd)
        {
                perror("open");
                return 1;
        }
        //将文件映射到内存中
        addr = mmap(NULL,1024,PROT_READ | PROT_WRITE,MAP_SHARED,fd,0);
        if(addr == MAP_FAILED)
        {
                perror("mmap");
                return 1;
        }
        //printf("映射成功!\n");
        //关闭文件
        close(fd);
        pid = fork();
        if(-1 == pid)
        {
                perror("pid");
                return 1;
        }
        if(0 == pid )
        {
                memcpy(addr,"123456789", 9);
        }else
        {
                wait(NULL);
                printf("addr:%s\n",(char *)addr);
        }
        //断开映射区
        munmap(addr,1024);
        return 0;
}



结果:
在这里插入图片描述

4.4 不同进程使用存储映射通信

实例:
进程a:

#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
#include<sys/mman.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
        //以读写方式打开一个文件(此文件用于映射)
        int fd = -1;
        int ret = -1;
        void *addr = NULL;
        fd = open("txt",O_RDWR);
        if(-1 == fd)
        {
                perror("open");
                return 1;
        }
        //将文件映射到内存中
        addr = mmap(NULL,1024,PROT_READ | PROT_WRITE,MAP_SHARED,fd,0);
        if(addr == MAP_FAILED)
        {
                perror("mmap");
                return 1;
        }
        printf("映射成功!")//关闭文件
        close(fd);
        //读映射区
        printf("addr:%s",(char *)addr);
        //断开映射区
        munmap(addr,1024);
        return 0;
}

进程b:

#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
#include<sys/mman.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
        //以读写方式打开一个文件(此文件用于映射)
        int fd = -1;
        int ret = -1;
        void *addr = NULL;
        fd = open("txt",O_RDWR);
        if(-1 == fd)
        {
                perror("open");
                return 1;
        }
        //将文件映射到内存中
        addr = mmap(NULL,1024,PROT_READ | PROT_WRITE,MAP_SHARED,fd,0);
        if(addr == MAP_FAILED)
        {
                perror("mmap");
                return 1;
        }
        printf("映射成功!")//关闭文件
        close(fd);
        //写映射区
        memcpy(addr,"123456789", 9);
        //断开映射区
        munmap(addr,1024);
        return 0;
}

结果:
在这里插入图片描述

在这里插入图片描述

4.5 使用匿名进程实现父子进程通信-父子通信不依赖文件

使用MAP_ANONYMOUS (或MAP_ANON)int *p = mmap(NULL, 1024, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);

	1024:该位置表示映射区大小,可依实际需要填写。
	MAP_ANONYMOUS和MAP_ANON这两个宏是Linux操作系统特有的宏。
	在类Unix系统中如无该宏定义,可使用如下两步来完成匿名映射区的建立。

实例:

#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
#include<sys/mman.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
int main()
{
// 创建匿名内存映射区
    int len = 4096;
    void *ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANON, -1, 0);

    if (ptr == MAP_FAILED)
    {
        perror("mmap error");
        exit(1);
    }
    // 创建子进程
    pid_t pid = fork();
    if (pid > 0) //父进程
    {
        // 写数据
        strcpy((char*)ptr, "hello mike!!");
        // 回收
        wait(NULL);
    }
    else if (pid == 0)//子进程
    {
        sleep(1);
        // 读数据
        printf("%s\n", (char*)ptr);
    }
    // 释放内存映射区
    int ret = munmap(ptr, len);
    if (ret == -1)
    {
        perror("munmap error");
        exit(1);
    }
    return 0;
}

结果:
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值