前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >理解进程间通信

理解进程间通信

作者头像
theanarkh
发布2023-10-30 15:49:29
1200
发布2023-10-30 15:49:29
举报
文章被收录于专栏:原创分享原创分享

进程是一个实体,两个实体间的通信就需要介质。使用不同的介质,就对应了不同的通信方式。进程的通信方式分为两种,同主机和不同主机。下面我们来逐个分析。

1 匿名管道

匿名管道是进程间通信中比较简单的一种,他只用于有继承关系的进程,因为匿名,非继承关系的进程无法找到这个管道,也就无法完成通信,而有继承关系的进程,是通过fork出来的,父子进程可以获得得到管道。进一步来说,子进程可以使用继承于父进程的资源,但是他无法使用叔伯进程的资源。管道通信的原理如下。

在这里插入图片描述 父子进程通过fork后,子进程继承了父进程的文件描述符。所以他们指向同一个数据结构。父子进程通常只需要单向通信,父子进程各关闭自己的一端。当父子进程对管道进程读写的时候,操作系统会控制这一切,包括数据的读取和写入,进程的挂起和唤醒。

2 命名管道

正如1中所说,匿名管道可以完成进程间通信,但是他有一定的限制,他的限制来自于他是匿名的,所有其他进程无法找到他,命名管理就是用来解决这个问题。有名字,进程们就可以通过名字去找到这个管道来通信。原理如下。

在这里插入图片描述 首先创建一个文件名为my_fifo的文件,然后进程们以读或写的方式去打开这个文件(以什么方式打开则具有对应的能力)。因为一个文件对应一个inode,所以不同的文件以同样的文件名打开一个文件时,他指向的inode是一样的。所以这个inode就是进程间通信的介质。他指向一块内存用于通信。然后其他的就和匿名管道一样了。

3 消息队列

进程间通信的前提是需要共享介质,所以不同的进程间通信,就是找到不同的共享介质。消息队列的原理就是操作系统维护一块数据,然后各个进程通过key来换取一个id,后续通过id进行消息的存取。使用过程。

代码语言:javascript
复制
// 发送进程
msgid = msgget((key_t)1234, 0666 | IPC_CREAT);
msgsnd(msgid, (void *)&data, MAX_TEXT, 0)
// 接收进程
msgid = msgget((key_t)1234, 0666 | IPC_CREAT);
msgrcv(msgid, (void *)&data, BUFSIZ, msgtype, 0)

在这里插入图片描述 进程通过key可以获取或者创建(系统维护)一个msqid_ds结构体。然后操作系统会返回key对应的一个id。后续进程通过id对msqid_ds进行存取。msqid_ds是表示一个消息队列的管理者。各个进程使用一个msqid_ds进行通信。操作系统会有权限控制,大小控制等。

4 共享内存

共享内存的原理和消息队列类型,都是开辟一块内存作为通信的介质。 共享内存的使用步骤。

代码语言:javascript
复制
// 通过key拿到一个id,每个进程都通过同样的key则可以得到同样的id
id= shmget((key_t)key, ...);
/*
    挂载到进程的地址空间,我们知道进程的地址是使用vma管理的,
    这里就是插入一个新的vma,拿到共享内存的首地址address,
    接下来就可以对这块内存进行读写了。
*/
address = shmat(shmid, 0, 0);
// 操作
// 从进程地址空间剥离出来,即不再使用这个空间
shmdt(shm);

1 操作系统有一个全局的结构体数据,每次需要一块共享的内存时(shmget),从里面取一个结构体,记录相关的信息。并返回一个id。 2 调用shmat的时候传入shmget返回的id。shmat根据id找到对应的shmid_ds 结构体。新建一个vm_area_struct结构体。开始地址和结束地址根据shmid_ds 中的信息计算,也就是用户申请的大小。接着把vm_area_struct插入进程中管理vm_area_struct的avl树。并且把一些上下文信息保存到页表项。 3 进程访问共享内存范围中的地址时,触发缺页中断。 4 如果还没分配物理地址则分配,否则直接范围已经分配的地址。如果分配了物理地址,则把物理地址写入进程的页表项。下次就不会缺页中断了。 5 其他进程共享该块内存的时候,如果访问范围内的地址,处理过程是类似的。进程访问某一个地址,发生缺页中断,发现这时候共享内存已经映射了物理地址。最后改写自己的页表项。因为各个进程都对应同一块内存,所以操作的时候会互相感知,实现通信。

在这里插入图片描述

5 信号

信号通信是进程通信中最简单的一种,但是他所能携带的信息有限,他只是通知其他进程一个信号,而不能发送具体的数据。我们先看一下进程结构体中关于信号的字段。

代码语言:javascript
复制
struct task_struct {
    ...
    // 收到的信号
    long signal;
    // 每个信号对应的处理函数和一些标记
    struct sigaction sigaction[32];
    // 当前屏蔽的信号
    long blocked;   
};

当一个进程发送一个信号给另一个进程的时候,会调用kill函数。我们看看这个函数的逻辑。

代码语言:javascript
复制
int sys_kill(int pid,int sig)
{
    struct task_struct **p = NR_TASKS + task;
    int err, retval = 0;
    /*
         pid等于0则给当前进程的整个组发信号,大于0则给某个进程发信号,
        -1则给全部进程发,小于-1则给某个组发信号
    */
    if (!pid) while (--p > &FIRST_TASK) {
        if (*p && (*p)->pgrp == current->pid) 
            if (err=send_sig(sig,*p,1))
                retval = err;
    } else if (pid>0) while (--p > &FIRST_TASK) {
        if (*p && (*p)->pid == pid) 
            if (err=send_sig(sig,*p,0))
                retval = err;
    } else if (pid == -1) while (--p > &FIRST_TASK)
        if (err = send_sig(sig,*p,0))
            retval = err;
    else while (--p > &FIRST_TASK)
        if (*p && (*p)->pgrp == -pid)
            if (err = send_sig(sig,*p,0))
                retval = err;
    return retval;
}

/*
  发送信号给进程sig是发送的信号,p是接收信号的进程,priv是权限,
  1是代表可以直接设置,比如给自己发信息,priv为0说明需要一定的权限
*/
static inline int send_sig(long sig,struct task_struct * p,int priv)
{
    if (!p || sig<1 || sig>32)
        return -EINVAL;
    // 这里使用euid,即进程设置了suid位的话,可以扩大权限,即拥有文件属主的权限
    if (priv || (current->euid==p->euid) || suser())
        p->signal |= (1<<(sig-1));
    else
        return -EPERM;
    return 0;
}

我们看到发送信号的逻辑很简单,收到判断要给哪些进程发送信号,然后判断有没有发送权限,最后修改另一些进程结构体中的signal字段。这就表示那些进程收到了信号。那么在某个时机,那些进程就会处理这个信号。这些时机包括,系统调用返回和时钟中断返回等。

6 socket

socket通信的原理比较简单,但是他的实现非常复杂,因为网络的情况比较多样复杂。socket通信的方式有基于连接和不基于连接的。不管是否基于连接,socket通信都是基于四元组(源ip、源端口、目的ip、目的端口)。 1 不基于连接,比如UDP,那两端就直接发送数据,对端接收就可以。 2 基于连接,基于连接的,流程会比较长,大概分为建立连接,通信,关闭连接三个步骤。 基于连接的进程间通信,首先需要有一个进程在监听某个端口(监听型socket),我们叫他为服务进程。如果哪个进程想和这个服务进程通信,那么就要先和服务进程完成三次握手。完成后,服务进程会新建一个通信型socket和客户进程进行数据通信。服务进程继续监听。

7 unix域

unix域是基于socket通信的一个特例,因为他的实现中使用了socket技术,但是他是基于单个主机上的进程间通信。因为在同一个主机内,所以就少了很多网络上的问题,那就减少了复杂度。unix域和传统的socket通信类型,服务器监听,客户端连接,由于在同主机,就不必要使用ip和端口的方式,浪费一个端口。unix域采用的是一个文件作为标记。大致原理如下。 1 服务器首先拿到一个socket结构体,和一个unix域相关的unix_proto_data结构体。 2 服务器bind一个文件。对于操作系统来说,就是新建一个文件,然后把文件路径信息存在unix_proto_data中。 3 listen 4 客户端通过同样的文件路径调用connect去连接服务器。这时候客户端的结构体插入服务器的连接队列,等待处理。 5 服务器调用accept摘取队列的节点,然后新建一个通信socket进行通信。 unix域通信本质还是基于内存之间的通信,客户端和服务器都维护一块内存,然后实现全双工通信,而unix域的文件路径,只不过是为了让客户端进程可以找到服务端进程。而通过connect和accept让客户端和服务器对应的结构体关联起来,后续就可以互相往对方维护的内存里写东西了。就可以实现进程间通信。

在这里插入图片描述

8 mmap

mmap可以映射文件,从而达到进程间通信的目前,mmap的原理是 1 打开一个文件,拿到一个文件描述符。 2 根据mmap的参数,申请一个vma结构体。并且传入fd表示映射文件。 3 把vma插入到调用进程的vma链表和树中。返回首地址(用户指定或者系统默认分配)。 4 用户通过3中返回的地址,进行内存的读写,这时候对应的是文件的读写。 5 另一个进程同样执行1-4的步骤,即有两个进程都映射到同一个文件。两个进程进行读写的时候,就完成了进程间通信。

在这里插入图片描述

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-04-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 编程杂技 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 匿名管道
  • 2 命名管道
  • 3 消息队列
  • 4 共享内存
  • 5 信号
  • 6 socket
  • 7 unix域
  • 8 mmap
相关产品与服务
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档