linux - 进程间通信

进程间通信

概念

linux环境下,进程地址空间相互独立,每个进程都有各自不同的用户地址空间。任何一个进程的全局变量在另一个进程中看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核;在内核中开辟一块缓冲区,进程A把数据从用户空间拷贝到内核缓冲区,进程B再从内核缓冲区把数据读取,内核提供这种机制成为进程间通信,简称IPC;

通信方式

如:文件、管道、信号、共享内存、消息队列、套接字、命名套接字;

常用的包括不限于:

  • 管道
  • 信号
  • 共享映射区
  • 本地套接字

管道

概念

管道是一种最基本的ipc机制,也称匿名管道,应用于有血缘关系的进程之间,完成数据传递。调用pipe函数即可创建一个管道;

在这里插入图片描述

管道的特性:

  • 管道的本质是一块内核缓冲区
  • 由两个文件描述符引用,一个表示写,一个表示读
  • 规定数据从管道的写端流入管道,从读端流出。
  • 两个进程都终结的时候,管道也自动消失。
  • 管道的读端和写端默认都是阻塞的。
  • 默认缓冲区大小为4K,可以使用ulimit -a命令获取大小。

局限性

  • 数据一旦被读走,便不在管道中存在,不可反复读取。

  • 数据只能在一个方向上流动,若要实现双向流动,必须使用两个管道

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

使用

pipe

创建一个管道,若函数调用成功,fd[0]存放管道的读端,fd[1]存放管道的写端;

#include <unistd.h>

int pipe(int pipefd[2]);

#define _GNU_SOURCE             /* See feature_test_macros(7) */
#include <fcntl.h>              /* Obtain O_* constant definitions */
#include <unistd.h>

int pipe2(int pipefd[2], int flags);

函数调用成功返回读端和写端的文件描述符,其中fd[0]是读端, fd[1]是写端向管道读写数据是通过使用这两个文件描述符进行的,读写管道的实质是操作内核缓冲区。

父子进程管道通信

个进程在由pipe()创建管道后,一般再fork一个子进程,然后通过管道实现父子进程间的通信(因此也不难推出,只要两个进程中存在血缘关系,这里的血缘关系指的是具有共同的祖先,都可以采用管道方式来进行通信)。父子进程间具有相同的文件描述符,且指向同一个管道pipe,其他没有关系的进程不能获得pipe()产生的两个文件描述符,也就不能利用同一个管道进行通信。

第一步:父进程创建管道

在这里插入图片描述

第二步:父进程fork出子进程
在这里插入图片描述

第三步:父进程关闭fd[0],子进程关闭fd[1]

在这里插入图片描述

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <ctype.h>
#include <sys/wait.h>
int main(int argc,char* args[])
{
        int status;
        //创建管道
        int fd[2];
        int ret = pipe(fd);
        if(ret<0)
        {
                perror("pipe error");
                return -1;
        }
        //创建子进程
        pid_t pid = fork();
        if(pid<0)
        {
                perror("fork error");
                return -1;
        }
        else if(pid>0)
        {
                //关闭读端
                close(fd[0]);
                sleep(5);
                write(fd[1],"hello,world",strlen("hello,world"));
                //wait(NULL);
                waitpid(-1, &status, WNOHANG);
        }
        else
        {
                //关闭写端
                close(fd[1]);
                char buf[1024];
                memset(buf,0,sizeof(buf));
                int n = read(fd[0],buf,sizeof(buf));
                printf("read over:%s\n",buf);
        }
        return 0;
}
ps -aux
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <ctype.h>
#include <sys/wait.h>
int main(int argc,char* args[])
{
        int status;
        //创建管道
        int fd[2];
        int ret = pipe(fd);
        if(ret<0)
        {
                perror("pipe error");
                return -1;
        }
        //创建子进程
        pid_t pid = fork();
        if(pid<0)
        {
                perror("fork error");
                return -1;
        }
        else if(pid>0)
        {
                //关闭读端
                close(fd[0]);
                //将标准输出重定向到管道的写端
                dup2(fd[1],STDOUT_FILENO);

                execlp("ps","ps","aux",NULL);
                perror("execlp error");
                wait(NULL);
        }
        else
        {
                //关闭写端
                close(fd[1]);
                //将标准输入重定向到管道的读端
                dup2(fd[0],STDIN_FILENO);
                execlp("grep","grep","bash",NULL);
                perror("execlp error");

        }
        return 0;
}

管道读写
读操作
  • 有数据
    • read正常都,返回读出的字节大小
  • 无数据
    • 写端全部关闭,read解除阻塞,返回0,相当于读到了文件尾部;
    • 写端没有全部关闭,read阻塞
写操作
  • 读端全部关闭
    • 管道破裂,进程终止,内核给当前线程发送SIGSTOP信号
  • 读端没有全部关闭
    • 缓冲区写满
      • write阻塞
    • 缓冲区没有满
      • 继续write
兄弟进程通信
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char *argv[])
{
    int fd[2];   //用来标记管道的两端的文件描述符
    int ret = pipe(fd);  //创建管道 pipe. fd[2] 是固定的输出参数

    if (ret == -1) {
        perror("[pipe create file] ");
        return 0;
    }
        
    int pipeRead = fd[0];   
    int pipeWrite = fd[1];

    int i = 0;
    for ( ; i < 2; i++) {
        pid_t pid = fork();
        
        if (pid == 0)
            break;

        if (pid == -1)
            perror("[creator process file:]");

    }

    if (i == 0) {                         //child process 1
        dup2(pipeWrite, STDOUT_FILENO);   
        close(pipeRead);
        execlp("ps", "ps", "aux", NULL);
    } else if (i == 1) {                 //child process 2
        dup2(pipeRead, STDIN_FILENO);    
        close(pipeWrite);
        execlp("grep", "grep", "bash", "--color=auto", NULL);
    } else if (i == 2) {                 //parent process
        close(pipeWrite);
        close(pipeRead);

        // sleep(2);
        int wpid;
        while ( wpid = waitpid(-1, NULL, WNOHANG) != -1) {  //回收子进程
            if (wpid == 1) ///sbin/init splash 进程    /sbin/launchd
                continue;

            if (wpid == 0) 
                continue;

            printf("child dide pid = %d\n", wpid);
        }    
    }
    
    printf("pipeWrite = %d,  pipeRead = %d\n", pipeWrite, pipeRead);
    return 0;
}
管道非阻塞

默认情况下,管道的读写两端都是阻塞的,若要设置读或者写端为非阻塞,则可参考下列三个步骤进行:

第1步: int flags = fcntl(fd[0], F_GETFL, 0);

第2步: flag |= O_NONBLOCK;

第3步: fcntl(fd[0], F_SETFL, flags);

若是读端设置为非阻塞:

  • 写端没有关闭,管道中没有数据可读,则read返回-1;

  • 写端没有关闭,管道中有数据可读,则read返回实际读到的字节数

  • 写端已经关闭,管道中有数据可读,则read返回实际读到的字节数

  • 写端已经关闭,管道中没有数据可读,则read返回0

查看管道缓冲区大小
long fpathconf(int fd, int name);
long pathconf(const char *path, int name);


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <ctype.h>
#include <sys/wait.h>
int main(void)
{
        int fd[2];
        int ret = pipe(fd);
        int pipeSize = fpathconf(fd[0],_PC_PIPE_BUF);
        printf("pipesize = %d\n",pipeSize);
        return 0;
}

命名管道

概念

FIFO常被称为命名管道,以区分管道(pipe)。管道(pipe)只能用于“有血缘关系”的进程间通信。但通过FIFO,不相关的进程也能交换数据。

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

创建管道

适用于无血缘关系的进程之间的通信。进程之间是不要使用 sleep() 函数的,因为管道默认就是堵塞的。虽然实现形态上是文件,但是管道本身并不占用磁盘或者其他外部存储的空间。在Linux的实现上,它占用的是内核的一段缓冲区空间。

#include <sys/types.h>
#include <sys/stat.h>

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

#include <fcntl.h>           /* Definition of AT_* constants */
#include <sys/stat.h>

int mkfifoat(int dirfd, const char *pathname, mode_t mode);

在这里插入图片描述

fifo进程通信

//fifo_read.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <ctype.h>
#include <sys/wait.h>
#include <sys/stat.h>
int main()
{
        //int mkfifo(const char *pathname, mode_t mode);
        //创建fifo文件
        /*int ret = mkfifo("./myfifo",0777);
        if(ret<0)
        {
                perror("fifo error");
                return -1;
        }*/
        //打开文件
        int fd = open("./myfifo",O_RDWR);
        if(fd<0)
        {
                perror("open error");
                return -1;
        }

        char buf[64];
        memset(buf,0,sizeof(buf));
        int n = read(fd,buf,sizeof(buf));
        printf("%s\n",buf);
        //关闭文件
        close(fd);
        getchar();
        return 0;
}
//fifo_write.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <ctype.h>
#include <sys/wait.h>
#include <sys/stat.h>
int main()
{
        //int mkfifo(const char *pathname, mode_t mode);
        //创建fifo文件
        int ret = mkfifo("./myfifo",0777);
        if(ret<0)
        {
                perror("fifo error");
                return -1;
        }
        //打开文件
        int fd = open("./myfifo",O_RDWR);
        if(fd<0)
        {
                perror("open error");
                return -1;
        }
        //写文件
        write(fd,"hello,world",strlen("hello,world"));
        //关闭文件
        //close(fd);
        getchar();
        return 0;
}

内存映射区

简介

存储映射I/O (Memory-mapped I/O) 使一个磁盘文件与存储空间中的一个缓冲区相映射。从缓冲区中取数据,就相当于读文件中的相应字节;将数据写入缓冲区,则会将数据写入文件。这样,就可在不使用read和write函数的情况下,使用地址(指针)完成I/O操作。
使用存储映射这种方法,首先应通知内核,将一个指定文件映射到存储区域中。这个映射工作可以通过mmap函数来实现。
在这里插入图片描述

mmap

#include <sys/mman.h>

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

函数参数:
addr代表映射的起始地址,通常设置为NULL,系统指定即可
length代表映射内存的文件长度
port代表保护模式,最常用的包括

  • PROT_READ 读
  • PROT_WRITE 写
  • PROT_READ | PROT_WRITE 读写

flags代表映射区的特性,可以是

  • MAP_SHARED: 写入映射区的数据会写回文件, 且允许其他映射该文件的进程共享。
  • MAP_PRIVATE: 对映射区的写入操作会产生一个映射区的复制(copy-on-write), 对此区域所做的修改不会写回原文件。

fd代表由open打开的文件描述符
offset代表以文件处的偏移量,必须是4k的整数倍,通常设置为0,表示从文件头开始映射

munmap

释放mmap申请的内存映射区;

int munmap(void *addr, size_t length);

函数参数:
addr:调用mmap函数成功返回的映射区首地址
length:映射区大小(mmap函数的第二个参数)

mmap父子进程通信
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <ctype.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/mman.h>
int main(int argc,char* args[])
{
        //void *mmap(void *addr, size_t length, int prot, int flags,
    //              int fd, off_t offset);
        int fd = open("./a.txt",O_RDWR);
        if(fd<0)
        {
                perror("open error");
                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)
        {
                perror("mmap error");
                return -1;
        }


        pid_t pid = fork();
        if(pid<0)
        {
                perror("fork error");
                return -1;
        }
        else if(pid>0)
        {
                memcpy(addr,"hello,world",strlen("hello,world"));
                wait(NULL);
        }
        else
        {
                sleep(5);       //保证父进程先完成
                char *p = (char *)addr;
                printf("%s\n",p);
        }
}

mmap非父子进程通信
//mmap_read.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <ctype.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/mman.h>
int main(int argc,char* args[])
{
        //void *mmap(void *addr, size_t length, int prot, int flags,
    //              int fd, off_t offset);
        int fd = open("./a.txt",O_RDWR);
        if(fd<0)
        {
                perror("open error");
                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)
        {
                perror("mmap error");
                return -1;
        }
        char *p = (char *)addr;
        char buf[64];
        memset(buf,0,sizeof(buf));
        memcpy(buf,p,10);
        printf("%s\n",buf);
        return 0;
}
//mmap_write.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <ctype.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/mman.h>
int main(int argc,char* args[])
{
        //void *mmap(void *addr, size_t length, int prot, int flags,
    //              int fd, off_t offset);
        int fd = open("./a.txt",O_RDWR);
        if(fd<0)
        {
                perror("open error");
                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)
        {
                perror("mmap error");
                return -1;
        }
        char buf[64];
        memcpy(addr,"abcdefghijklmn",10);
        return 0;
}

mmap注意事项

mmap映射内存必须要有一个文件

  • 创建映射区的过程中,隐含着一次对映射文件的读操作,将文件内容读取到映射区
  • 当MAP_SHARED时,要求:映射区的权限应 <=文件打开的权限(出于对映射区的保护)。而MAP_PRIVATE则无所谓,因为mmap中的权限是对内存的限制。
  • 映射区的释放与文件关闭无关,只要映射建立成功,文件可以立即关闭。
  • 特别注意,当映射文件大小为0时,不能创建映射区。所以,用于映射的文件必须要有实际大小;mmap使用时常常会出现总线错误,通常是由于共享文件存储空间大小引起的。
  • munmap传入的地址一定是mmap的返回地址。坚决杜绝指针++操作。
  • 文件偏移量必须为0或者4K的整数倍
  • mmap创建映射区出错概率非常高,一定要检查返回值,确保映射区建立成功再进行后续操作。

匿名映射区

匿名映射不需要文件,只需要改变一个参数即可;

mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);

思考

可以open的时候O_CREAT一个新文件来创建映射区吗?
可以
如果open时O_RDONLY, mmap时PROT参数指定PROT_READ|PROT_WRITE会怎样?
无法打开
mmap映射完成之后, 文件描述符关闭,对mmap映射有没有影响?
没有映像
如果文件偏移量为1000会怎样?
必须是4的整数倍
对mem越界操作会怎样?
越界访问失败
如果mem++,munmap可否成功?
不能,如果要做指针偏移的的话,可以 char* pt = ptr;
mmap什么情况下会调用失败?
第二个参数为0,第三个参数必须要有权限,并且打开的权限必须大于mmap设定的权限
如果不检测mmap的返回值,会怎样?
失败了无法定位错误

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值