Linux多进程开发(三)

1.进程间通信

进程间通讯概念

进程是一个独立的资源分配单元,不同进程(通常指的是用户进程)之间的资源是独立的,没有关联,不能在一个进程中直接访问另一个进程的资源。

但是,进程不是孤立的,不同的进程需要进行信息的交互和状态的传递等,因此需要进程间通信( IPC:Inter Processes Communication )

进程间通信的目的:

  • 数据传输:一个进程需要将它的数据发送给另一个进程。
  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
  • 资源共享:多个进程之间共享同样的资源。为了做到这一点,需要内核提供互斥和同步机制。
  • 进程控制:有些进程希望完全控制另一个进程的执行(如 Debug 进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

Linux进程间通信的方式

在这里插入图片描述

匿名管道

匿名管道概念

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

如:统计一个目录中文件的数目命令:ls | wc –l
ls为获取当前目录的文件列表,|为管道符(即匿名管道),wc为统计个数,-l为参数
为了执行该命令,shell创建了两个进程来分别执行 lswcwc需要ls的数据去统计个数,这里就用到了进程间的通信

ls命令产生的进程会得到当前目录的文件数据,并将标准输出指向管道的写入端,wc命令产生的进程的标准输入指向管道的读取端
在这里插入图片描述

管道的特点(匿名、有名共有)

  • 管道是一个在内核内存中维护的缓冲器,这个缓冲器的存储能力是有限的,不同的操作系统大小不一定相同。
  • 管道拥有文件的特质:读操作、写操作,匿名管道没有文件实体,有名管道有文件实体,但不存储数据。可以按照操作文件的方式对管道进行操作。
  • 一个管道是一个字节流,使用管道时不存在消息或者消息边界的概念,从管道读取数据的进程可以读取任意大小的数据块,而不管写入进程写入管道的数据块的大小是多少。
  • 通过管道传递的数据是顺序的,从管道中读取出来的字节的顺序和它们被写入管道的顺序是完全一样的。
  • 在管道中的数据的传递方向是单向的,一端用于写入,一端用于读取,管道是半双工的(单项传递是单工、双向传递是双工、同一时间只能单向传递是半双工)。
  • 从管道读数据是一次性操作,数据一旦被读走,它就从管道中被抛弃,释放空间以便写更多的数据,在管道中无法使用 lseek() 来随机的访问数据。
  • 匿名管道只能在具有公共祖先的进程(父进程与子进程,或者两个兄弟进程,具有亲缘关系)之间使用。
    在这里插入图片描述
    为什么可以使用管道进行进程间通信(为什么管道只能用在具有关系的进程之间):
    右边的文件描述符表是左边文件描述符表fork得到,此时父子进程共享文件描述符表,
    假设父进程的文件描述符3对应文件A、文件描述符4对应文件B,那么子进程的文件描述符3也对应文件A、文件描述符4也对应文件B
    假设父进程的文件描述符5对应管道的写入端、文件描述符6对应管道的读取端,那么子进程的文件描述符5对应管道的写入端、文件描述符6对应管道的读取端
    当父进程通过文件描述符5向管道写入数据,那么子进程就可以通过文件描述符6从管道读取数据;当子进程通过文件描述符5向管道写入数据,那么父进程就可以通过文件描述符6从管道读取数据

一句话就是因为父子进程是共享文件描述符。
在这里插入图片描述
管道的数据结构:
环形队列(循环队列)
在这里插入图片描述

匿名管道的使用

//创建匿名管道
#include <unistd.h>
int pipe(int pipefd[2]);
//查看管道缓冲大小命令
ulimit –a
//查看管道缓冲大小函数
#include <unistd.h>
long fpathconf(int fd, int name);

int pipe(int pipefd[2]);

#include <unistd.h>
int pipe(int pipefd[2]);
    功能:创建一个匿名管道,用于进程间通信
    参数:int pipefd[2] 这个数组是一个传出参数。
          pipefd[0] 对应的是管道的读取端
          pipefd[1] 对应的是管道的写入端
    返回值:成功返回0,失败返回-1

管道默认是阻塞的:如果管道中没有数据,read1阻塞,如果管道满了,write阻塞
注意:匿名管道只能用于具有关系的进程之间的通信(父子进程,兄弟进程)

示例:

// 子进程发送数据给父进程,父进程读取到数据输出;父进程发送数据给子进程,子进程读取到数据输出
int main() {

    //要在fork之前创建管道
    int pipefd[2];
    int ret = pipe(pipefd);
    //创建子进程
    pid_t pid = fork();
    if(pid > 0) {                //父进程
        printf("i am parent process, pid : %d\n", getpid());
        char buf[1024] = {0};    //从管道的读取端读取数据
        while(1) {
            int len = read(pipefd[0], buf, sizeof(buf));
            printf("parent recv : %s, pid : %d\n", buf, getpid());
            
            // 向管道中写入数据
            char * str = "hello,i am parent";
            write(pipefd[1], str, strlen(str));
            sleep(1);
        }

    } else if(pid == 0){          //子进程
        printf("i am child process, pid : %d\n", getpid());
        char buf[1024] = {0};
        while(1) {
            //向管道中写入数据
            char * str = "hello,i am child";
            write(pipefd[1], str, strlen(str));
            sleep(1);

            int len = read(pipefd[0], buf, sizeof(buf));
            printf("child recv : %s, pid : %d\n", buf, getpid());
        }
    }
    return 0;
}

在这里插入图片描述
int pipe(int pipefd[2]);的示例中,将两个sleep(1)隐去,再运行
在这里插入图片描述

出现这种情况是因为:
比如子进程先向管道中写入数据,写入之后子进程又读取了管道中的数据
但是之前加了sleep就没有这种问题,因为在sleep中父进程会读取管道中的数据

在开发中一般不会实现相互发送数据,即父进程读取、子进程写入或父进程写入、子进程读取
为了不产生这种问题,如果是父进程读取、子进程写入,那么就通过close(pipefd[1]);关闭写端、close(pipefd[0]);关闭读端
在这里插入图片描述

ulimit –a //查看管道缓冲大小命令
在这里插入图片描述
long fpathconf(int fd, int name); //查看管道缓冲大小函数

int main() {

    int pipefd[2];
    int ret = pipe(pipefd);

    // 获取管道的大小
    long size = fpathconf(pipefd[0], _PC_PIPE_BUF);
    printf("pipe size : %ld\n", size);

    return 0;
}

在这里插入图片描述

匿名管道通信案例

/*
    实现 ps aux | grep xxx 父子进程间通信
    
    子进程: ps aux, 子进程结束后,将数据发送给父进程
    父进程:获取到数据,过滤
    pipe()
    execlp()
    子进程将标准输出 stdout_fileno 重定向到管道的写端(dup2)
*/

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

int main() {

    // 创建一个管道
    int fd[2];
    int ret = pipe(fd);
    
    pid_t pid = fork();          // 创建子进程
    if(pid > 0) {                // 父进程
        close(fd[1]);            // 关闭写端
        // 从管道中读取
        char buf[1024] = {0};
        int len = -1;
        while((len = read(fd[0], buf, sizeof(buf) - 1)) > 0) {   //大于0说明有数据
            // 过滤数据输出
            printf("%s", buf);
            memset(buf, 0, 1024);       //清空buf数组
        }

        wait(NULL);             //回收子进程资源

    } else if(pid == 0) {       // 子进程
        close(fd[0]);           // 关闭读端
        dup2(fd[1], STDOUT_FILENO);          // 文件描述符的重定向 stdout_fileno -> fd[1]
        execlp("ps", "ps", "aux", NULL);     // 执行 ps aux
        perror("execlp");
        exit(0);
    } else {
        perror("fork");
        exit(0);
    }


    return 0;
}

在这里插入图片描述

管道的读写特点

使用管道时,需要注意以下几种特殊的情况(默认都是阻塞I/O操作):
1.所有的指向管道写端的文件描述符都关闭了(管道写端引用计数为0)
此时有进程从管道的读端读数据,那么管道中剩余的数据被读取以后,再次read会返回0,就像读到文件末尾一样。

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

3.如果所有指向管道读端的文件描述符都关闭了(管道的读端引用计数为0)
此时有进程向管道中写数据,那么该进程会收到一个信号SIGPIPE, 通常会导致进程异常终止。

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

总结:
读管道:
管道中有数据,read返回实际读到的字节数。
管道中无数据:
—写端被全部关闭,read返回0(相当于读到文件的末尾)
—写端没有完全关闭,read阻塞等待

写管道:
管道读端全部被关闭,进程异常终止(进程收到SIGPIPE信号)
管道读端没有全部关闭:
—管道已满,write阻塞
—管道没有满,write将数据写入,并返回实际写入的字节数

设置管道非阻塞:
和文件描述符非阻塞一样

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

有名管道

有名管道概念

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

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

一旦打开了 FIFO,就能在它上面使用与操作匿名管道和其他文件的系统调用一样的I/O系统调用了(如read()、write()和close())。与管道一样,FIFO 也有一个写入端和读取端,并且从管道中读取数据的顺序与写入的顺序是一样的。FIFO 的名称也由此而来:先入先出。

有名管道和匿名管道不同之处:

  1. FIFO 在文件系统中作为一个特殊文件存在,但 FIFO 中的内容却存放在内存中。
  2. 当使用 FIFO 的进程退出后,FIFO 文件将继续保存在文件系统中以便以后使用。
  3. FIFO 有名字,不相关的进程可以通过打开有名管道进行通信。

有名管道的使用

//通过命令创建有名管道
mkfifo 名字   
//通过函数创建有名管道
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);

注:
一旦使用 mkfifo 创建了一个 FIFO,就可以使用 open 打开它,
常见的文件I/O函数都可用于fifo,如:close、read、write、unlink 等。

FIFO 严格遵循先进先出(First in First out),对管道及 FIFO 的读总是
从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如 lseek() 
等文件定位操作。

int mkfifo(const char *pathname, mode_t mode);

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
    参数:
        - pathname: 管道名称的路径
        - mode: 文件的权限 和 open 的 mode 是一样的,是一个八进制的数
    返回值:成功返回0,失败返回-1,并设置错误号

示例 :
通过函数创建有名管道:

int main() {

    int ret = access("fifo", F_OK);      // 判断文件是否存在
    if(ret == -1) {
        printf("管道不存在,创建管道\n");
        ret = mkfifo("fifo1", 0664);

    return 0;
}

在这里插入图片描述
再创建一个write.c,向管道中写入数据:

int main() {

    //判断文件是否存在
    int ret = access("test", F_OK);
    if(ret == -1) {
        printf("管道不存在,创建管道\n");
        //若不存在创建管道文件
        ret = mkfifo("test", 0664);  
    }
    //以只写的方式打开管道
    int fd = open("test", O_WRONLY);
    //写数据
    for(int i = 0; i < 100; i++) {
        char buf[1024];
        sprintf(buf, "hello, %d\n", i);    //向数组中写入数据
        printf("write data : %s\n", buf);  //打印写入数组的数据
        write(fd, buf, strlen(buf));       //向管道中写入数据
        sleep(1);
    }
    close(fd);
    return 0;
}

创建一个read.c,从管道中读取文件:

// 从管道中读取数据
int main() {
    // 打开管道文件
    int fd = open("test", O_RDONLY);
    // 读数据
    while(1) {
        char buf[1024] = {0};
        int len = read(fd, buf, sizeof(buf));
        if(len == 0) {
            printf("写端断开连接了...\n");
            break;
        }
        printf("recv buf : %s\n", buf);
    }
    close(fd);

    return 0;
}

在两个终端中分别运行write、read,然后先停止read 或 先停止write,看两个终端的返回

有名通道的读写特点

有名管道的注意事项:
1.一个为只读而打开一个管道的进程会阻塞,直到另外一个进程为只写打开管道(比如上边案例先打开write进程,并不会有读取数据,直到打开read进程)
2.一个为只写而打开一个管道的进程会阻塞,直到另外一个进程为只读打开管道(同理先打开read进程也不会写入数据)

读管道
管道中有数据,read返回实际读到的字节数
管道中无数据:
—管道写端被全部关闭,read返回0,(相当于读到文件末尾)
—写端没有全部被关闭,read阻塞等待

写管道:
管道读端被全部关闭,进行异常终止(收到一个SIGPIPE信号)
管道读端没有全部关闭:
—管道已经满了,write会阻塞
—管道没有满,write将数据写入,并返回实际写入的字节数。

有名管道实现简单版聊天功能

简单版聊天功能

内存映射

内存映射概念

内存映射(Memory-mapped I/O)是将磁盘文件的数据映射到内存,用户通过修改内存就能修改磁盘文件。
在这里插入图片描述

内存映射系统调用

#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);     //映射文件到内存中
int munmap(void *addr, size_t length);    //解除映射

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

#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
    功能:将一个文件或者设备的数据映射到内存中
    参数:
         - void *addr: NULL, 由内核指定
         - length : 要映射的数据的长度,这个值不能为0。建议使用文件的长度(获取文件的长度:stat或lseek) 
         - prot : 对申请的内存映射区的操作权限
                  -PROT_EXEC :可执行的权限
                  -PROT_READ :读权限
                  -PROT_WRITE :写权限
                  -PROT_NONE :没有权限
                  要操作映射内存,必须要有读的权限
                  一般要么只有读权限PROT_READ,要么有读写权限PROT_READ|PROT_WRITE
         - flags :
                  - MAP_SHARED : 映射区的数据会自动和磁盘文件进行同步,进程间通信,必须要设置这个选项
                  - MAP_PRIVATE :不同步,内存映射区的数据改变了,对原来的文件不会修改,会重新创建一个新的文件(copy on write)
         - fd: 需要映射的那个文件的文件描述符,通过open得到,open的是一个磁盘文件
               注意:文件的大小不能为0,open指定的权限不能和prot参数有冲突。
                     如果prot: PROT_READ,那么open:只读或读写;如果prot: PROT_READ | PROT_WRITE,那么open:读写
         - offset:偏移量,一般不用。必须指定的是4k的整数倍,0表示不便宜。
    返回值:成功返回创建的内存的首地址,失败返回MAP_FAILED,(void *) -1

int munmap(void *addr, size_t length);

#include <sys/mman.h>
int munmap(void *addr, size_t length);
    功能:释放内存映射
    参数:
        - addr : 要释放的内存的首地址
        - length : 要释放的内存的大小,要和mmap函数中的length参数的值一样

使用内存映射实现进程间通信:
1.有关系的进程(父子进程):
在还没有子进程的时候,通过唯一的父进程,先创建内存映射区,
有了内存映射区以后,创建子进程,
父子进程共享创建的内存映射区。

2.没有关系的进程间通信
准备一个大小不是0的磁盘文件,
进程1 通过磁盘文件创建内存映射区,得到一个操作这块内存的指针,
进程2 通过磁盘文件创建内存映射区,得到一个操作这块内存的指针,
使用内存映射区通信。

注意:内存映射区通信,是非阻塞

示例:

int main() {
    int fd = open("test.txt", O_RDWR);  // 打开一个文件
    int size = lseek(fd, 0, SEEK_END);  // 获取文件的大小
    void *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);   // 创建内存映射区
    pid_t pid = fork();      // 创建子进程
    if(pid > 0) {            // 父进程
        wait(NULL);
        char buf[64];
        strcpy(buf, (char *)ptr);
        printf("read data : %s\n", buf);
    }else if(pid == 0){     // 子进程
        strcpy((char *)ptr, "nihao a, son!!!");
    }
    munmap(ptr, size);     // 关闭内存映射区

    return 0;
}

内存映射的注意事项

1.如果对mmap的返回值(ptr)做++操作(ptr++), munmap是否能够成功?
void * ptr = mmap(…);
ptr++; 可以对其进行++操作
munmap(ptr, len); // 错误,进行++操作之前保存ptr地址才能释放成功

2.如果open时O_RDONLY, mmap时prot参数指定PROT_READ | PROT_WRITE会怎样?
错误,返回MAP_FAILED
open()函数中的权限要和prot参数的权限保持一致

3.如果文件偏移量为1000会怎样?
偏移量必须是4K的整数倍,返回MAP_FAILED

4.mmap什么情况下会调用失败?
- 第二个参数:length = 0
- 第三个参数:prot
- 只指定了写权限
- prot PROT_READ | PROT_WRITE
第五个参数fd 通过open函数时指定的 O_RDONLY / O_WRONLY

5.可以open的时候O_CREAT一个新文件来创建映射区吗?
可以,但是创建的文件的大小如果为0就不行
也可以对新的文件进行扩展( lseek() 或 truncate() )

6.mmap后关闭文件描述符,对mmap映射有没有影响?
int fd = open(“XXX”);
mmap(,fd,0);
close(fd);
映射区还存在,创建映射区的fd被关闭,没有任何影响

7.对ptr越界操作会怎样?
void * ptr = mmap(NULL, 100,);
4K
越界操作操作的是非法的内存,会产生段错误

使用内存映射实现文件拷贝的功能

将文件1、文件2映射到内存,就可以将文件1的内容复制到文件2中

int main() {

    int fd = open("english.txt", O_RDWR);                // 对原始的文件进行内存映射
    int len = lseek(fd, 0, SEEK_END);                    // 获取原始文件的大小
    int fd1 = open("cpy.txt", O_RDWR | O_CREAT, 0664);   // 创建一个新文件
    truncate("cpy.txt", len);                            // 对新创建的文件进行拓展
    write(fd1, " ", 1);                                  // 对新文件进行写操作

    // 分别做内存映射
    void * ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    void * ptr1 = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd1, 0);

    memcpy(ptr1, ptr, len);                              // 内存拷贝
    // 释放资源
    munmap(ptr1, len);
    munmap(ptr, len);

    close(fd1);
    close(fd);

    return 0;
}

匿名映射

匿名映射:不需要文件实体进程内存映射,只能做父子进程间的进程通信

其实就是在ptr()中多加了个MAP_ANONYMOUS权限且偏移量设置为0

int main() {

    // 1.创建匿名内存映射区
    int len = 4096;
    void * ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    if(ptr == MAP_FAILED) {
        perror("mmap");
        exit(0);
    }

    // 父子进程间通信
    pid_t pid = fork();

    if(pid > 0) {
        // 父进程
        strcpy((char *) ptr, "hello, world");
        wait(NULL);
    }else if(pid == 0) {
        // 子进程
        sleep(1);
        printf("%s\n", (char *)ptr);
    }

    // 释放内存映射区
    int ret = munmap(ptr, len);

    if(ret == -1) {
        perror("munmap");
        exit(0);
    }
    return 0;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值