Linux之进程间通信

Posted 小赵小赵福星高照~

tags:

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

进程间通信

文章目录

进程间通信的目的

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

通信的本质是什么?

传递数据,是互相的

为何是进程间通信?进程间能"直接"相互传递数据吗?

这是不可能做到的!因为进程具有独立性,所有的数据操作都会发生写时拷贝,一定要通过中间媒介的方式来进行通信,进程间通信的本质:让不同的进程先看到同一份资源

这个资源是由操作系统提供的,其实就是内存,可以让不同的进程都看到

进程间通信,如何通过系统,让不同的进程看到同一份资源?

进程间通信分类

管道

  • 匿名管道pipe
  • 命名管道

System V IPC

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

POSIX IPC

  • 消息队列
  • 共享内存
  • 信号量
  • 互斥量
  • 条件变量
  • 读写锁

进程间通信的本质让不同的进程能看到同一份系统资源(系统通过某种方式提供的系统内存),正是方式不同,因此出现了不同的进程间的通信方式

管道

什么是管道

管道是Unix中最古老的进程间通信的形式。

我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”

匿名管道

匿名管道的特征

供具有血缘关系的进程,进行进程间通信。(常见于父子)

进程间通信首要任务是保证不同的进程看到同一份资源,父进程以读和写打开两次文件pipe_file,这就是创建管道,然后父进程fork子进程,子进程以父进程为模板进行创建,文件描述符表父子进程不共享,但是值是一样的,此时不同的进程就看到了同一份资源

这就使两个不同的进程看到了相同的内存空间

但是管道只能进行单向数据通信,父子进程需要关闭不需要的文件描述符,来达到构建单向通信的信道的目的,那么有这样几个问题:

  1. 为什么曾经要打开呢?

不打开读写,子进程拿到的文件打开方式必定和父进程一样,无法通信,打开为了使通信更加灵活

  1. 为何一定要关闭呢?

防止误操作

pipe函数

头文件:#include <unistd.h>
功能:创建一无名管道
原型:int pipe(int fd[2]);
参数fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码

fd是输出型参数,拿到打开的管道文件的描述符:

#include<stdio.h>
#include<unistd.h>
int main()

    int pipe_fd[2] = 0;
    if(pipe(pipe_fd) < 0)
    
        perror("pipe");
        return 1;
    
    printf("%d,%d\\n",pipe_fd[0],pipe_fd[1]);
    return 0;

可以看到输出了3和4,3是管道的读端,4是管道的写端

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()

    //创建管道
    int pipe_fd[2] = 0;
    if(pipe(pipe_fd) < 0)
    
        perror("pipe");
        return 1;
    
    printf("%d,%d\\n",pipe_fd[0],pipe_fd[1]);
    //fork创建子进程
    pid_t id = fork();
    if(id < 0)
    
        perror("fork");
        return 1;
    
    else if(id == 0) // write
    
        //child
        //关闭读端
        close(pipe_fd[0]);
        const char* msg = "hello parent\\n";
        int count = 5;
        while(count)
        
            write(pipe_fd[1],msg,strlen(msg));//写数据到写端
            sleep(1);
            count--;
        
        close(pipe_fd[1]);
        exit(0);
    
    else//read
    
        char buffer[64];
        //father
        //关闭写端
        close(pipe_fd[1]);
        while(1)
        
            buffer[0] = 0;
            ssize_t size =  read(pipe_fd[0],buffer,sizeof(buffer)-1);//以读的方式打开管道文件,返回读的个数
            if(size>0)
            
                buffer[size] = 0;
                printf("parent get message from child:%s\\n",buffer);
            
            else if(size == 0)
            
                printf("pipe file close,child quit\\n");
                break;
            
            else
            
                break;
            
        
        int status = 0;
        if(waitpid(id,&status,0))
        
            printf("child quit,wait success\\n");

        
    
    return 0;


可以看到文件描述符为3和4,父子进程成功的交换了数据

用fork来共享管道原理

fork之后:

fork之后关闭掉各自不需要的文件描述符:

站在文件描述符角度理解管道

  1. 父进程创建管道

  1. 父进程fork出子进程

  1. 父进程关闭写端文件描述符,子进程关闭读端文件描述符

管道读写规则

  • 当没有数据可读时

O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。

  • 当管道满的时候

O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
O_NONBLOCK enable:调用返回-1,errno值为EAGAIN

  • 如果所有管道写端对应的文件描述符被关闭,则read返回0
  • 如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出
  • 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
  • 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。

管道的特性

  • 管道自带同步机制

如果管道里面没有消息,父进程(读端),在干什么呢?

父进程在进行等待,等管道内部有数据就绪

如果管道里面写端已经写满了,继续写入,还能写吗?

不能,子进程在等待,等待管道内部有空闲空间

这就叫进程间同步

  • 匿名管道是单向通信的
  • 匿名管道是面向字节流的

管道是面向字节流的(发送方发送的方式和接受方接收方式不要求一样,比如A一次发50个字节,可B可以一次接收5个,接收10次)

  • 匿名管道只能够保证具有血缘关系的进程通信,常用于父子
  • 匿名管道可以保证一定程度的数据原子性

进程退出,曾经打开的文件也会被关掉,管道也是文件,管道的生命周期随进程的生命周期

下面我们验证部分特征

如果读端不读,并且关闭读端,那么写端如何呢?

管道一般是多大?

read端write端导致结果
不读write阻塞
不写read阻塞
不读并且关闭write端被OS发送SIGPIPE信号杀掉
不写并且关闭read读取到0,文件结束

如果读取关闭,一直写,有意义吗?毫无意义!一直在写,本质就是在浪费系统资源,写进程会立马被操作系统终止掉

写进程是子进程,操作系统通过发送信号的方式干掉子进程,父进程可以通过waitpid获得子进程的状态,下面我们通过代码来验证:

#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()

    int pipe_fd[2] = 0;
    if(pipe(pipe_fd) < 0)
    
        perror("pipe");
        return 1;
    
    printf("%d,%d\\n",pipe_fd[0],pipe_fd[1]);
    
    pid_t id = fork();
    if(id < 0)
    
        perror("fork");
        return 2;
    
    else if(id == 0) //write
    
        //child
        //fd[0]:读
        //fd[1]:写
        close(pipe_fd[0]);
        const char*msg = "hello parent\\n";
        int count = 5;
        while(count)
        
            write(pipe_fd[1],msg,strlen(msg));
            sleep(1);
            count--;
        
        close(pipe_fd[1]);
        exit(0);
    
    else			//read
    
        //parent
        close(pipe_fd[1]);
        char buffer[64];
        while(1)
        
            buffer[0] = 0;
            ssize_t size = read(pipe_fd[0],sizeof((buffer)-1);
            if(size> 0)
            
                buffer[size] = 0;
                printf("parent get messge from child :%s\\n",buffer);
               
            else if(size == 0)
            
                printf("pipe file close,child quit!\\n");
                break;
            
            else
            
                break;
            
            close(pipe_fd[0]);//关闭读端
            break;
        
        int status = 0;
        if(waitpid(id,&status,0) > 0)
        
            printf("child quit,wait success!,sig:%d\\n",status&0x7f);
        
    
    return 0;

可以看到终止信号为13,13信号是SIGPIPE:

写端被操作系统发送SIGPIPE杀掉

不超过4kb写入都是原子性的,最大为

可以看到是65536个字节,除以1024等于64,64KB

但是这里是512乘以8字节,是4KB

哪个是正确的呢?

man 7 pipe,PIPE_BUF在linux当中是4096字节

Writes of more than PIPE_BUF bytes may be nonatomic,我们可以看到这句话,写入的字节数大于PIPE_BUF时可能是非原子的,这就是为什么用ulimit查看系统资源为什么是4096字节,管道写入不超过4KB,写入都是原子性的

命名管道

管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。命名管道是一种特殊类型的文件

1、理解命名管道的原理

要让两个毫不相关的进程通信必须先保证两个进程能看到同一份资源,磁盘上的文件每一个都有路径,路径具有唯一性,可以让不同的进程分别以读和写打开一份文件,让不同的进程进行通信

普通文件是需要将数据刷新到磁盘的,持久化存储的,写入的时候需要将数据刷新到磁盘,读取的时候需要在磁盘上读,但是这样将数据存储在磁盘上效率太低了,实际上系统中上有fifo这样的特殊文件,这个文件有路径来标识自己,但是他不会将数据刷新到磁盘的,这就叫管道文件,当你想要使用命名管道时,普通文件不可以作为通信的媒介,必须是创建的管道文件

2、命令行实验

创建命名管道文件

mkfifo myfifo

可以看到它是一个管道文件

将数据重定向到myfifo文件当中:

while :; do echo "hello world"; sleep 1; done > myfifo

我们重新打开一个bash进程进行查看:

发现向myfifo当中写,文件大小是0,说明就和我们上面所说的不会将数据刷新到磁盘,我们在右边进程中将myfifo重定向到cat,可以看到显示到了该进程当中。此时就通过myfifo管道使两个进程进行了通信:

我们来实现一个客户端与服务端的通信:

我们touch两个文件:client.c和server.c,并且编写Makefile,一次生成两个可执行程序:

.PHONY:all
all:client server
client:client.c
    gcc -o $@ $^
server:server.c
    gcc -o $@ $^
.PHONY:clean
clean:
	rm -f client server

server.c(服务端)

创建命名管道函数:

第一个参数是要制作命名管道的路径,第二个是命名管道的权限

返回值:

成功返回0,失败返回-1

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

#define FIFO "./fifo"
int main()

    int ret = mkfifo(FIFO,0644);//创建命名管道
    if(ret < 0)
    
        //创建失败
        perror("mkfifo");
        return 1;
    
    int fd = open(FIFO,O_RDONLY);//以只读方式打开管道文件
    if(fd < 0)
    
        perror("open");
        return 2;
    
    
   	char buffer[128];
    while(1)
    
        buffer[0] = 0;
        ssize_t s = read(fd,buffer, sizeof(buffer)-1);//读到buffer
        if(s > 0)
        
            buffer[s] = 0;
            printf( "client# %s\\n", buffer);
        
        else if(s == 0)
        
        	printf ( "client quit. . ." );
        	break;
        
        else
        
            break;
        
    
    close(fd);
    return 0;

client.c(客户端)

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

#define FIFO "./fifo"
int main()

    int fd = open(FIFO,O_RDONLY);//以只读方式打开管道文件
    if(fd < 0)
    
        perror("open");
        return 2;
    
    
   	char buffer[128];
    while(1)
    
        printf("Please Enter# ");
        ffulush(stdout);
        buffer[0] = 0;
        ssize_t s = read(0,buffer, sizeof(buffer)-1);//stdin内容读到buffer
        if(s > 0)
        
            buffer[s] = 0;
            write(fd,buffer,strlen(buffer));
        
        else if(s == 0)
        
          	break;
        
        else
        
            break;
        
    
    close(fd);
    return 0;

可以看到在client端发送的消息,server端能够收到

system V共享内存

共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据

共享内存的基本原理

经过前面的学习我们知道了进程间通信的本质:

1.要先让不同的进程看到同一份资源

2.通信的过程

前面我们学了管道的通信方式,管道分为匿名管道和命名管道:

匿名管道的本质是利用了父子共享文件的特征

命名管道的本质是利用了文件路径具有唯一性,让进程看到同一个文件

上面的两种管道类通信方式看到的同一份资源都是文件资源

下面我们来看一下系统V的共享内存原理:

  1. 操作系统申请一块内存空间
  2. OS将该内存映射进对应进程的共享区中(堆栈之间)
  3. OS可以将映射后的虚拟地址返回给用户

有一个问题:如果操作系统愿意,能不能做到这些步骤呢?

OS当然能够做到,因为OS是内存管理者以及进程管理者,操作系统内部提供了通信机制(IPC),IPC模块

OS内可不可能提供大量的共享内存呢?

当然是有可能的,共享内存多了OS就要管理:先描述再组织,存在大量的数据结构来描述共享内存,对共享内存的管理就相当于是对数据结构的管理

OS在物理内存申请一块空间叫共享内存,将共享内存映射到进程1,再映射进进程2

  1. 申请共享内存
  2. 进程1和进程2分别挂接对应的共享内存到自己的地址空间(共享区)
  3. 双方就看到了同一份资源,可以进行正常通信了

这三个步骤有对应的系统调用接口,提供类似的服务

申请共享内存接口:shmget

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

第三个参数

IPC_CREAT:单独使用时表示目标共享内存不存在,创建之,有,获取之

IPC_EXCL:单独使用没有意义

两个一起使用:如果目标共享内存不存在,创建,如果存在,则出错返回

我们查文档发现当IPC_CREAT和IPC_EXCL一起使用时,如果目标共享内存存在了,则会出错,报出存在的错误信息

如果调用shmget成功,一定得到的是全新的共享内存

那么怎么保证多个进程看到的是同一个共享内存呢?

在创建好共享内存时,操作系统还会创建对应的结构体用来描述该共享内存

struct shm_ipc

	//...

每一个共享内存都有数据结构,数据结构里面有一个key值

可以通过Key来进行唯一性区分,两个进程在创建共享内存时传入同一个key,此时两个进程就看到了同一份数据结构,那么如何保证AB进程获得的key值一样呢?

此时就又用到了一个系统接口:ftok函数:

ftok接口(创建key值)

两个参数任意填写,但是必须保证AB进程填的是一样的,这样就可以保证这两个进程使用的同一个规则形成key,在底层会通过某种规则对这两个参数进行操作形成一个key值

创建key值:

#include"comm.h"
#include<sys/ipc.h>
#include<sys/shm.h>
#include<sys/types.h>
int main()

    //创建key值
    key_t k =ftok(PATH_NAME,PROJ_ID);//形成key值
    if(k<0)
    
        perror("ftok");
        return 1;
    
    printf("my key:%x\\n",k);
    return 0;
   

进程通信

linux信号量之进程间同步

进程间通信之-----信号量

临界区与互斥量区别

Linux系统编程 --进程间通信 -共享内存

Linux 进程间通信 --信号量