Linux 进程间通信相关知识总结

进程间通信

进程间通信:两个进程之间的数据交互(数据传输,数据共享)

进程间为什么不能直接通信?

因为进程之间具有独立性,每个进程都有自己的虚拟地址空间,访问的都是自己的虚拟地址空间,因此进程间是无法直接通信的,因此需要操作系统提供中间媒介进行通信.

管道通信的本质: 实际上就是内核中的一块缓冲区,多个进程访问同一个缓冲区实现通信.

管道的分类:

匿名管道:这个内核中的缓冲区没有标识符(无法被其它进程直接找到)

只能用于具有亲缘关系的进程间通信

命名管道:这个内核中的缓冲区具有标识符,所有进程只要知道标识符,就可以通过相同的标识符,打开一个管道进行通信.

可以用于同一主机上的任意进程间通信.

匿名管道:

父进程创建一个匿名管道,操作系统会返回一个管道的操作句柄(文件描述符),在这之后,父进程创建一个子进程,子进程复制父进程,也拥有这个描述符,指向了内核中同一个管道.

  1. 进程通过系统调用接口在内核中创建管道 int pipe(int pipefd[2]);
  2. 通过这个接口,操作系统返回管道的操作句柄
  3. 管道的操作方式类似于文件操作(通过文件描述符,以及IO接口进行访问)
  4. 创建子进程,子进程复制了父进程,也拥有(同一个)管道的操作句柄.
int pipe(int pipefd[2]);

pipefd[2]: 拥有两个整形元素的数组,用于接收管道的操作句柄

pipefd[0]: 用于从管道中读取数据

pipefd[1]:用于向管道中写入数据

返回值:成功返回0,失败返回-1.

管道的特性:

管道是半双工通信(可以选择方向的单向通信)

管道自带同步与互斥:

同步:管道的写操作在不超过PIPE_BUF(4096字节),保证原子性 用于保证安全操作

互斥:让资源的访问操作按照某种规则有序进行,避免产生饥饿问题  用于保证合理操作

管道提供字节流服务:有序的,可靠的,不限制大小的,基于连接的.

基于连接的: 

  • 有读就必须有写,有写就必须有读
  • 若所有读端被关闭,则继续写入就会触发异常,导致进程退出
  • 若所欲写端关闭,则读完数据之后不再阻塞,而是返回0

管道的生命周期随进程:在不人为干预的情况,所有打开管道的进程退出后,管道被释放

| 管道符:连接两个命令,将前面命令的输出结果交给后面的命令作为输入进行处理

例: ps -ef | grep ssh

命令1:ps -ef 运行了一个ps进程

命令2: grep ssh 运行了一个grep进程

这两个进程间使用了一个匿名管道进行数据传输

模拟实现:

shell进程:

1.创建管道 pipefd[0/1]

2.创建一个子进程运行ps (复制了父进程,拥有管道的操作句柄)

3.创建一个子进程运行grep (复制了父进程,拥有管道的操作句柄)

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

int main(){
    int fd[2];
    if (pipe(fd) < 0)
        perror("pipe"),exit(0);

    pid_t ps_fd = fork();
    if(ps_fd == 0){
        close(fd[0]);
        dup2(fd[1],1);
        execlp("ps","ps","-ef",NULL);
        exit(0);
    }
    pid_t grep_fd = fork();
    if(grep_fd == 0){
        close(fd[1]);
        dup2(fd[0],0);
        execlp("grep","grep","ssh",NULL);
        exit(0);
    }
    close(fd[0]);
    close(fd[1]);
    return 0;
}

命名管道

内核中的缓冲区具有标识符,可以用于同一主机的任意间进程通信.其本质依然是内核中的缓冲区,只是标识符是可见于文件系统的管道文件.多个进程通过打开同一个管道文件,访问到同一个缓冲区实现通信,管道的本质并没有改变,因此缓冲区大小不受磁盘空间影响.

int mkfifo(cosnt char* pathname,mode_t mode);
pathname:管道标识符文件的名称  
mode:文件权限
返回值: 成功返回0  失败返回-1

命名管道的打开特性:

如果命名管道没有被写的方式打开,则只读打开会阻塞.

如果命名管道没有被读的方式打开,则只写打开会阻塞.

共享内存

本质:开辟的一块物理内存,多个进程通过将同一块物理内存映射到自己的虚拟地址空间,通过自己的虚拟地址进行访问,实现数据共享.

特性:共享内存是最快的通信方式

原因:共享内存的操作相较于其它操作少了两步用户态与内核态之间的数据拷贝

操作流程:

①.创建(打开)共享内存

#include <sys/ipc.h>
#include <sys/shm.h>

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

key:共享内存的标识符(多个进程通过相同的标识符打开同一个共享内存)
size:共享内存的大小
shmflg:操作方式 IPC_CREAT | IPC_EXCL 操作权限0664
返回值:成功返回非负整数(共享内存代码中的操作句柄),失败返回-1

②.将这块共享内存映射到进程的虚拟地址空间

#include <sys/shm.h>

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

shmid:shmget返回的操作句柄
shnaddr:虚拟地址空间的映射首地址(NULL)
shmflg:SHM_RDONLY(只读),否则填0(表示可读可写)
返回值:成功返回实际映射在虚拟地址空间的首地址,失败返回(void*)-1.

③.内存操作

④.解除进程与共享内存的映射关系

#include <sys/shm.h>

int shmdt(const void *shmaddr);

shmaddr:虚拟地址空间的映射首地址
返回值:成功返回0  失败返回-1

⑤.删除共享内存

#include <sys/shm.h>

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

shmid:shmget返回的操作句柄
cmd:具体对共享内存要进行的操作 IPC_RMID(删除)
buf:主要用于设置或者获取共享内存的信息,这里直接设置NULL即可
返回值:对于IPC_RMID操作来说,成功返回0,失败返回-1.

每个共享内存都有一个映射连接数(表示当前有多少进程与自己建立了映射链接),因此删除共享内存,共享内存只有在映射连接数为0的时候才会被删除,如果当前不为0,则从当前开始拒绝后续的映射连接请求,直到映射连接数为0时删除.

共享内存通信实例:

  • 发送信息端:
//发送信息端
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/shm.h>

#define IPC_KEY 0x123456
int main(){
    //创建共享内存
    int shmid = shmget(IPC_KEY,32,IPC_CREAT|0664);
    if(shmid == -1){
        perror("shmget failed\n");
        exit(0);
    }

    //将共享内存映射到虚拟内存
    void* start_shm = shmat(shmid,NULL,0);
    if(start_shm == (void*)-1){
        perror("shmat failed\n");
        exit(0);
    }
    int i = 0;
    while(1){
        sprintf(start_shm,"鱼塘%d号",i++);
        sleep(1);
    }

    //解除映射
    shmdt(start_shm);
    //删除共享内存
    shmctl(shmid,IPC_RMID,NULL);

    return 0;
}
  •  接收信息端:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/shm.h>

#define IPC_KEY 0x123456
int main(){
    //创建/打开共享内存
    int shmid = shmget(IPC_KEY,32,IPC_CREAT|0664);
    if(shmid == -1){
        perror("shmget failed\n");
        exit(0);
    }
    //将共享内存映射到自己的地址空间
    void* start_shm = shmat(shmid,NULL,0);
    if(start_shm == (void*)-1){
        perror("shmat failed\n");
        exit(0);
    }
    while(1){
        printf("%s\n",(char*)start_shm);
        sleep(1);
    }
    //解除映射
    shmdt(start_shm);
    //删除共享内存
    shmctl(shmid,IPC_RMID,NULL);
    return 0;
}

注意事项:

  • 共享内存的操作并非是安全的,有可能同学多个进程写入则会造成数据交叉或覆盖的危险
  • 共享内存的操作是需要保护的(通过同步与互斥进行保护)

消息队列

本质:指针内核中创建了一个优先级队列,多个进程可以通过相同的标识符找到内核中放入同一个消息队列,通过添加或者获取节点实现数据传输.

  • 打开/创建一个消息队列
#include <sys/ipc.h>
#include <sys/msg.h>

int msgget(key_t key, int msgflg);
key:消息队列的标识符(多个进程通过相同的标识符打开同一个消息队列)

msgflg:操作方式 IPC_CREAT | IPC_EXCL 操作权限0664
返回值:成功返回非负整数(消息队列代码中的操作句柄),失败返回-1
  • 将数据放到消息队列上
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
 
msqid:消息队列标识
msgp:
    struct msgbuf {
         long mtype;      //管道号,一定大于0
         char mtext[1];   //保存要发送的数据
    };
msgsz:发送的数据大小
msgflg:缺省填0
  •  从消息队列读取数据
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
函数参数和msgsnd函数同理
  •  删除消息队列
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
magid:shmget返回的操作句柄
cmd:具体对消息队列要进行的操作 IPC_RMID(删除)
buf:主要用于设置或者获取消息队列的信息,这里直接设置NULL即可
返回值:对于IPC_RMID操作来说,成功返回0,失败返回-1.

信号量

本质:就是一个计数器(对资源进行计数)

功能:用于实现进程间的同步与互斥

信号量的操作:

P操作:在临界资源获取之前,先进行P操作,判断能否获取(能获取: -1 ,不能 : 等待)

V操作:在资源产生之后,进行V操作(计数+1,唤醒一个等待中的进程)

信号量实现互斥:同一时间只有一个进程能够访问资源,保证的临界资源访问的安全性实现互斥的原理,就是总是认为资源只有一个,计数最大为1;

一个进程在访问前,访问信号量,P操作计数-1,为0(其它进程再来就会被阻塞),等到这个进程访问完毕之后再进行V操作,计数+1.

 

 

 

 

 

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 深蓝海洋 设计师:CSDN官方博客 返回首页