Linux操作系统进程间通信

Posted Ricky_0528

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux操作系统进程间通信相关的知识,希望对你有一定的参考价值。

文章目录

1. 进程间通信介绍

进程之间会出现协同工作的场景,一个进程需要把自己的数据交付给另一个进程,让其处理,这就产生了进程间通信,因此操作系统需要来设计通信方式

进程之间是具有独立性的,一个进程无法看到另一个进程的资源,因此交互数据会比较麻烦,两个进程要想相互通信,就得先看到一份公共的资源,也就是一段内存,这段内存属于操作系统

进程间通信的本质:由操作系统参与,提供一份所有通信进程能看到的公共资源,这可能以文件方式提供,可以能是队列的方式,也可能就是原始的内存块,这也就产生了多种通信方式

1.1 进程间通信的目的

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

1.2 进程间通信的发展

  • 管道
  • System V进程间通信
  • POSIX进程间通信

1.3 进程间通信分类

  • 管道
    • 匿名管道pipe
    • 命名管道
  • System V IPC
    • System V 消息队列
    • System V 共享内存
    • System V 信号量
  • POSIX IPC
    • 消息队列
    • 共享内存
    • 信号量
    • 互斥量
    • 条件变量
    • 读写锁

2. 管道

2.1 匿名管道

基于文件的通信方式——管道

管道可以理解为文件的内核缓冲区,它为父子进程所共享的一块内存

匿名管道特点:

  • 是一个只能单向通信的通信管道
  • 管道是面向字节流的
  • 仅限于父子进程之间通信
  • 管道自带同步机制,原子性写入
  • 管道的生命周期是随进程的

匿名管道通信实例

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

int main()

    int pipefd[2] = 0;
    if (pipe(pipefd) != 0) 
        perror("pipe error!");
        return 1;
    

    // 0:对应读取端    1:对应写入端
    printf("pipefd[0]: %d\\n", pipefd[0]);
    printf("pipefd[1]: %d\\n", pipefd[1]);
    
    // 让父进程进行读取,子进程进行写入
    if (fork() == 0) 
        // 子进程
        close(pipefd[0]);

        const char *msg = "hello\\n";
        while (1) 
            // pipe只要有缓冲区就会一直写入
            write(pipefd[1], msg, strlen(msg)); // 这里不需要+1,\\n是C语言级别的东西
            sleep(1);
        

        exit(0);
    

    // 父进程
    close(pipefd[1]);
    while (1) 
        // 没有让父进程sleep,写的快读的慢
        char buffer[64] = 0;
        // 只要有数据就可以一直读
        ssize_t s = read(pipefd[0], buffer, sizeof(buffer) - 1); // 如果read的返回值是0,意味着子进程关闭文件描述符
        if (s == 0) 
            break;
         else if (s > 0) 
            buffer[s] = 0;
            printf("%s", buffer);
         else 
            break;
        
    

    return 0;


// int pipe(int pipefd[2]);
// pipefd[2]是一个输出型参数,可以通过这个参数读取到打开的两个fd

验证管道是由大小的

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

int main()

    int pipefd[2] = 0;
    if (pipe(pipefd) != 0) 
        perror("pipe error!");
        return 1;
    
    
    // 让父进程进行读取,子进程进行写入
    if (fork() == 0) 
        // 子进程
        close(pipefd[0]);

        const char *msg = "hello\\n";
        int count = 0;
        while (1) 
            // pipe只要有缓冲区就会一直写入
            write(pipefd[1], "a", 1);
            count++;
            printf("count: %d\\n", count);
        

        exit(0);
    

    // 父进程
    close(pipefd[1]);
    while (1) 
		// 父进程不进行读取   
    

    return 0;

write写入65535个字节即64KB后就不再写了,说明管道是由大小的,会等待read来读,不继续写的本质就是等待对方来读

父进程只读取少量的字节,子进程并不会继续进行写入

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

int main()

    int pipefd[2] = 0;
    if (pipe(pipefd) != 0) 
        perror("pipe error!");
        return 1;
    
    
    // 让父进程进行读取,子进程进行写入
    if (fork() == 0) 
        // 子进程
        close(pipefd[0]);

        const char *msg = "hello\\n";
        int count = 0;
        while (1) 
            // pipe只要有缓冲区就会一直写入
            write(pipefd[1], "a", 1);
            count++;
            printf("count: %d\\n", count);
        

        exit(0);
    

    // 父进程
    close(pipefd[1]);
    while (1) 
        sleep(5);
        char c[64] = 0;
        read(pipefd[0], c, 64);
        printf("%s\\n", c);
    
    return 0;

原因:需要一次性读走4KB的数据,才会继续进行写入

  • 当要写入的数据量不大于PIPE_BUF时,Linux将保证写入的原子性
  • 当要写入的数据量大于PIPE_BUF时,Linux将不再保证写入的原子性

4种情况

  • 读端不读或者读的慢,写端要等待读端

  • 读端关闭,写端收到SIGPIPE信号直到终止

    这本质是在浪费系统资源,操作系统会终止写入进程,OS给目标进程发送信号SIGPIPE

  • 写端不写或者写的慢,读端要等待写端

  • 写端关闭,读端读完pipe内部的数据然后再读,会读到0,表明读到文件结尾

2.2 命名管道

使用命名管道,双方通信只需要按照文件操作即可

因为命名管道是基于字节流的,所以实际上在信息传递的时候,是需要通信双方制定协议的

命令行上直接使用

创建管道文件

mkfifo fifo

这个管道是有名字的——fifo,使用它即可在命令行上进行简单的字符串通信

使用代码操作管道

server.c

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

#define MY_FIFO "./fifo"

int main()

    umask(0); // 将系统的umask清零,这样下面我们创建管道时的文件权限就不会受系统的umask影响
    if (mkfifo(MY_FIFO, 0666) < 0) 
        perror("mkfifo");
        return 1;
    
    
    int fd = open(MY_FIFO, O_RDONLY);
    if (fd < 0) 
        perror("open");
        return 2;
    

    while (1) 
        char buffer[64] = 0;
        ssize_t s = read(fd, buffer, sizeof(buffer) - 1);
        if (s > 0) 
            buffer[s] = 0;
            if (strcmp(buffer, "show") == 0) 
                if (fork() == 0) 
                    execl("/usr/bin/ls", "ls", "-l", NULL);
                    exit(1);
                

                waitpid(-1, NULL, 0);
             else if (strcmp(buffer, "run") == 0) 
                if (fork() == 0) 
                    execl("/usr/bin/sl", "sl", NULL);
                    exit(1);
                

                waitpid(-1, NULL, 0);
             else 
                printf("client say#%s\\n", buffer);
            
         else if (s == 0) 
            printf("client quit\\n");
            break;
         else 
            perror("read");
        
    
    
    close(fd);

    return 0;

client.c

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

#define MY_FIFO "./fifo"


int main()

    int fd = open(MY_FIFO, O_WRONLY);
    if (fd < 0) 
        perror("open");
        return 1;
    

    while (1) 
        printf("请输入#");
        fflush(stdout);
        char buffer[64] = 0;
        ssize_t s = read(0, buffer, sizeof(buffer) - 1);
        if(s > 0) 
            buffer[s - 1] = 0;
            printf("%s\\n", buffer);
            write(fd, buffer, strlen(buffer));
        
    
    close(fd);
    return 0;

makefile

.PHONY:all
all:server client

server:server.c
	gcc -o $@ $^

client:client.c
	gcc -o $@ $^

.PHONYE:clean
clean:
	rm -f server client fifo

命名管道的数据不会刷新到磁盘,这样可以提高效率,刷新到磁盘需要时间

为什么pipe叫做匿名管道,fifo叫做命名管道

  • 匿名管道的文件没有名字,它是通过父子继承的方式来看到同一份资源,不需要名字来标识同一个资源
  • fifo一定要有名字,为了保证不同的进程看到同一个文件,所以必须要有名字

3. System V

同样是基于文件的通信方式,在OS层面专门为进程间通信设计的一个方案

操作系统不相信任何用户,给用户提供功能的时候,采用系统调用

System V进程间通信,一定会存在专门用来通信的接口(system call)

在同一主机内的进程间通信方法:System V方案

进程间通信的本质:让不同的进程看到同一份资源,因而System V有三种方案

  • 消息队列
  • 共享内存
  • 信号量

3.1 共享内存

如何让不同进程看到同一份资源

  • 通过某种调用,在内存中创建一份内存空间
  • 通过某种调用,让参与通信的多个进程“挂接”到这份新开辟的内存空间上
  • 去关联
  • 释放共享内存

操作系统中可能会存在多个进程,这样就会有很多不同的共享内存,因此OS就必须对这些共享内存进行管理,使不同的共享内存来进行进程间通信,管理的方式与之前类似:先描述,在组织;这就带来了一个问题,如何保证两个或多个进程看到的是同一份共享内存呢?共享内存一定会有标识唯一性的ID,方便让不同的进程识别同一个共享内存资源,而这个ID一定是存在于描述共享内存的数据结构中。这个唯一的标识符,就是用来进行进程间通信的,而这个ID也是需要由用户自己设定的,这样才能达到通信的目的

接口介绍

  • 创建共享内存

  • 控制共享内存

  • 装载/卸载共享内存

① shmget

int shmget(key_t key, size_t size, int shmflg);

key_t key:这个key就是会设置进内核中的关于shm在内核中的数据结构中

key如果由我们自己指定会很麻烦,且不能保证都记住,因此使用ftok函数进行生成

  • const char *pathname :自定义路径名,如"/tmp/Xxx"
  • int proj_id:自定义项目ID,如:0x6666

这样的话只要我们形成key的算法+原始数据是一样的,形成的key就是一样的,ID唯一

size_t size:建议是4KB的整数倍

int shmflg:标志位

  • IPC_CREAT:如果单独使用这个或者标志位为0,则创建一个共享内存,如果创建的共享内存以及存在,则直接返回当前已经存在的共享内存
  • IPC_EXCL:单独使用没有意义,IPC_CREAT | IPC_EXCL:如果不存在这个共享内存则直接创建,如果已经存在则报错
  • 还可以加上要创建的这个共享内存的权限:IPC_CREAT | IPC_EXCL | 0666,对应于perms

comm.h

#pragma once

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define PATH_NAME "./"
#define PROJ_ID 0x6666
#define SIZE 4096

server.c

#include "comm.h"

int main()

    key_t key = ftok(PATH_NAME, PROJ_ID);
    if (key < 0) 
        perror("ftok");
        return 1;
    

    int shmid = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666);
    if (shmid < 0) 
        perror("shmget");
        return 2;
    

    printf("key: %#x, shmid: %d\\n", key, shmid);

    return 0;

shmid为-1就表示创建失败了,即该共享内存已经存在

ipcs -m:查看系统存在的共享内存

./server已经执行结束了,但是发现共享内存是一直存在的,并没有被释放,这表明System V的IPC资源生命周期是随内核的,只能程序员手动释放或者操作系统手动重启

ipcrm -m [shmid]:释放共享内存

② shmctl

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

int shmid:共享内存的shmid

int cmd:对共享内存的操作

  • IPC_RMID:释放该共享内存——shmctl(shmid, IPC_RMID, NULL)
  • IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值
  • IPC_RMID:在进程有1足够权限的前提下,把共享内存的当前关联值设置为shmid_ds数据结构中给出的值

struct shmid_ds *buf:是一块结构体,描述的是shmid的数据结构

key vs shmid

  • key:只是在系统层面上来进行唯一性标识的,不能用来管理共享内存
  • shmid:是操作系统给用户返回的id,用来在用户层对共享内存进行管理

③ shmat

void *shmat(int shmid, const void *shmaddr, int shmflg);

DESCRIPTION
shmat() attaches the System V shared memory segment identified by shmid to the address space of the calling process.

RETURN VALUE
On success shmat() returns the address of the attached shared memory segment; on error (void *) -1 is returned, and errno is set to indicate the cause of the error.

这里的地址也是虚拟地址,跟malloc出来的一样

④ shmdt

int shmdt(const void *shmaddr);

并不是释放共享内存,而是取消当前进程与共享内存之间的关系

DESCRIPTION

shmdt() detaches the shared memory segment located at the address specified by shmaddr from the address space of the calling process.

RETURN VALUE

On success shmdt() returns 0; on error -1 is returned, and errno is set to indicate the cause of the error.


共享内存的整个生命周期

server.c

#include "comm.h"

int main()

    key_t key = ftok(PATH_NAME, PROJ_ID);
    if (key < 0) 
        perror("ftok");
        return 1;
    

    int shmid = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666);
    if (shmid < 0) 
        perror("shmget");
        return 2;
    

    printf("key: %#x, shmid: %d\\n", key, shmid);
    sleep(5);
    
    char *mem = (char*)shmat(shmid, NULL, 0);
    printf("attach shm success\\n");
    sleep(5);

    // 在这里进行通信


    shmdt(mem);
    printf("detach shm success\\n");
    sleep(5);

    shmctl(shmid, IPC_RMID, NULL);
    printf("key: %#x, shmid: %d -> shm delete success\\n", key, shmid);

    return 0;


使用共享内存进程间通信

server.c

#include "comm.h"

int main()

    key_t key = ftok(PATH_NAME, PROJ_ID);
    if (key < 以上是关于Linux操作系统进程间通信的主要内容,如果未能解决你的问题,请参考以下文章

进程通信

Linux 进程间通信 --信号量

Linux - 进程间通信 - 信号量

Linux进程间通信-信号量

进程间通信—信号量

进程间通信之信号量