[Linux]:进程间通信(下)
✨✨ 欢迎大家来到贝蒂大讲堂✨✨
🎈🎈养成好习惯,先赞后看哦~🎈🎈
所属专栏:Linux学习
贝蒂的主页:Betty’s blog
1. system V通信
前面我们所探究的通信方式都是基于管道文件的,而接下来我们将谈论的是在System V
标准下,进程间通信的方式。
System V
标准是在同一主机内的进程间通信方案,是站在OS层面专门为进程间通信设计的方案,其主要提供三个主流方案:system V共享内存,system V消息队列,system V信号量。
其中System V 共享内存和 System V 消息队列的目的在于传送数据。而 System V 信号量则是为确保进程间的同步与互斥而设计,虽然 System V 信号量看似与通信没有直接关联,但实际上它也属于通信范畴。
2. system V共享内存
2.1 共享内存的介绍
共享内存本质就是不同进程间能够访问同一块物理空间,从而实现进程间通信。
共享内存的实现方式是在物理内存中申请一块内存空间。接着,将这块内存空间与各个进程各自的页表分别建立映射,并在虚拟地址空间的共享区开辟空间,把虚拟地址填充到页表对应位置,从而建立虚拟地址与物理地址的对应关系。此时,不同进程便能访问同一份物理内存,此物理内存即共享内存。
2.2 共享内存的相关函数
一般来说我们创建共享内存大致可以分为两步:第一步就是在物理内存上申请共享内存。第二步将申请到的物理内存挂接到对应的地址空间上。这两步都分别对应两个函数:shmget
与shmat
。如果想释放共享内存,步骤就刚好相反,首先第一步将共享内存与地址空间去关联,即取消映射关系,第二步将释放共享内存空间,即将物理内存归还给系统。这两步都分别对应两个函数:shmdt
与shmctl
。
2.2.1 shmget函数
- 函数原型:int shmget(key_t key, size_t size, int shmflg);
- 参数:第一个参数
key
,表示待创建共享内存在系统当中的唯一标识。第二个参数size
,表示待创建共享内存的大小。第三个参数shmflg
,表示创建共享内存的方式。- 返回值:
shmget
调用成功,返回一个有效的共享内存标识符(用户层标识符),否则返回-1。
首先shmget
需要传入的第一个参数key
需要通过函数ftok
获取,其原型如下:
key_t ftok(const char *pathname, int proj_id);
其中pathname
代表一个已存在的路径名,proj_id
代表一个项目ID
,ftok
函数可以将通过特定的算法将这两个参数转换出对应的系统标识符key
,否则返回-1。
值得注意的是:
- 使用
ftok
函数生成key
值可能会产生冲突,此时需要对传入ftok
函数的参数进行修改。- 如果不同进程间需要通信,需要采用同样的路径名和和项目
ID
,进而生成同一个key
值,才能找到同一个共享资源。
第二个参数size
一般建议是4096的整数倍,假设如果传的是4097,操作系统实际上申请的空间大小是 4096*2,虽然操作系统多申请了,但是多余的部分用户不能使用,这样就可能造成空间的浪费。
第三个参数shmflag
标记位常用选项有两种:
IPC_CREAT
:如果申请的共享内存不存在,就创建,存在,就获取并返回。IPC_EXCL
:如果申请的共享内存存在,就出错返回。
IPC_CREAT | IPC_EXCL
:如果申请的共享内存不存在,就创建,存在就出错返回。这俩选项一起使用保证了,如果我们申请成功了一个共享内存,这个共享内存一定是一个新的。
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define MY_PATH "/home/beidi/tmp" //路径名
#define PROJ_ID 0x6666 //项目ID
#define SIZE 4096 //共享内存大小
int main()
{
//获取key
key_t key = ftok(MY_PATH,PROJ_ID);
if(key < 0)
{
perror("ftok:");
return 1;
}
int shmid = shmget(key,SIZE,IPC_CREAT|IPC_EXCL);
if(shmid < 0)
{
perror("shmget:");
return 2;
}
printf("key: %x\n",key);
printf("shmid : %d\n",shmid);
return 0;
}
并且我们也可能通过指令ipcs - m
查看相关信息。
其分别每一项的含义:
标题 | 含义 |
---|---|
key | 系统区别各个共享内存的唯一标识 |
shmid | 共享内存的用户层 id |
owner | 共享内存的拥有者 |
perms | 共享内存的权限 |
bytes | 共享内存的大小 |
nattch | 关联共享内存的进程数 |
status | 共享内存的状态 |
因为共享内存的生命周期是随内核的,所以如果不手动回收,这个共享内存就会一直存在。除了通过特定的函数外,我们也能够通过指令ipcrm -m shmid
释放指定的共享内存资源。
2.2.2 shmat函数
- 函数原型:void *shmat(int shmid, const void *shmaddr, int shmflg);
- 参数:第一个参数
shmid
,表示待关联共享内存的用户级标识符。第二个参数shmaddr
,指定共享内存映射到进程地址空间的某一地址,通常设置为NULL
,表示让内核自己决定一个合适的地址位置。第三个参数shmflg
,表示关联共享内存时设置的某些属性。- 返回值:
shmat
调用成功,返回共享内存映射到进程地址空间中的起始地址。否则,返回(void*)-1
。
一般shmat
函数的第三个参数传入的常用的选项有以下三种:
SHM_RDONLY
:关联共享内存后只进行读取操作。SHM_RND
:若shmaddr
不为NULL
,则关联地址自动向下调整为SHMLBA
的整数倍。0
:默认为读写权限。
2.2.3 shmdt函数
- 函数原型:int shmdt(const void *shmaddr);
- 参数:
shmaddr
为待去关联共享内存的起始地址,即调用shmat
函数时得到的返回值。- 返回值:
shmdt
调用成功,返回0。否则返回-1。
2.2.4 shmctl函数
- 函数原型:int shmctl(int shmid, int cmd, struct shmid_ds *buf);
- 参数:第一个参数
shmid
,表示所控制共享内存的用户级标识符。第二个参数cmd
,表示具体的控制动作。第三个参数buf
,用于获取或设置所控制共享内存的数据结构。- 返回值:
shmctl
调用成功,返回0。否则返回-1。
其中第二个选项cmd
常见有三个选项:
IPC_STAT
:获取共享内存的当前关联值,此时参数buf
作为输出型参数。IPC_SET
:在进程有足够权限的前提下,将共享内存的当前关联值设置为buf
所指的数据结构中的值.IPC_RMID
:删除共享内存段,此时buf
可以传NULL
。
以下就是一个完整的共享内存的使用方式:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define MY_PATH "/home/beidi/tmp" //路径名
#define PROJ_ID 0x6666 //项目ID
#define SIZE 4096 //共享内存大小
int main()
{
//获取key
key_t key = ftok(MY_PATH,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\n",key);
printf("shmid : %d\n",shmid);
//关联
char*mem = (char*)shmat(shmid,NULL,0);
if(mem == (void*)-1)
{
perror("shmat:");
return 3;
}
//去关联
shmdt(mem);
//释放共享内存
shmctl(shmid,IPC_RMID,NULL);
return 0;
}
2.3 共享内存与管道的对比
同样是实现客户端client
与服务端server
的交互,共享内存明显会比管道通信快的多。
从上图观察我们就可以看出,管道通信将一个文件内容从服务端发送到客户端一共需要四次拷贝:
- 服务端将信息从输入文件复制到服务端的临时缓冲区中。
- 将服务端临时缓冲区的信息复制到管道中。
- 客户端将信息从管道复制到客户端的缓冲区中。
- 将客户端临时缓冲区的信息复制到输出文件中。
而如果是共享内存却只需要两次拷贝即可,大大提升效率。
- 从输入文件到共享内存。
- 从共享内存到输出文件。
但是共享内存也有明显的缺陷,那就是没有同步与互斥这样的保护机制。
3. system V消息队列
因为system V消息队列的实用性越来越低,所以这里并不重点介绍,如果想了解详细用法,可以查官方文件。
3.1 消息队列的基本原理
消息队列本质上是在系统中创建的一个队列。该队列的每个成员为一个数据块,且每个数据块由类型和信息两部分组成。两个相互通信的进程以某种方式访问同一个消息队列。当这两个进程向对方发送数据时,均在消息队列的队尾添加数据块;而当它们获取数据块时,则都在消息队列的队头取用数据块。
同样和共享内存一样,消息队列申请的资源生命周期随内核,所以我们需要特定函数或者指令释放资源。
3.2 消息队列的相关函数
- 消息队列的创建:int msgget(key_t key, int msgflg);
- 消息队列的发送:int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
- 消息队列的获取:ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
- 消息队列的销毁:int msgctl(int msqid, int cmd, struct msqid_ds *buf);
其中msgsnd
函数的第二个参数msgp
为一个结构体:
struct msgbuf{
long mtype; /* message type, must be > 0 */
char mtext[1]; /* message data */
};
该结构当中的第一个成员mtype
为发送数据的类型,第二个成员mtext
即为待发送的信息,当我们定义该结构时,mtext
的大小可以自己指定。
4. system V信号量
4.1 信号量的介绍
进程间通信通过共享资源来实现,这虽然解决了通信的问题,但是也引入了新的问题,那就是多个执行流共用同一个临界资源,若是不对该临界资源进行保护,就可能导致各个进程从临界资源获取的数据不一致等问题。为了解决这个问题,我们引入了信号量。
System V
信号量本质上是一个计数器,用于衡量临界资源中的资源数目。它对临界资源内部的资源数进行统计,同时操作系统为其提供了一种对临界资源的预定机制。所有进程在访问临界资源之前,必须先申请信号量。
比如说我们现在有100byte的临界资源,我们以10byte为单位划分,就能划分出十份临界资源,也就需要申请10个信号量。
但是信号量本质也是一个临界资源,为了防止其同时被多个执行流访问,我们可以将信号量设置为1,这种信号量我们称之为二元信号量,我们可以通过以下代码来具体说明为什么二元信号量能实现临界资源的互斥:
当进程 A 申请访问共享内存资源时,如果此时信号量
sem
的值为 1,那么进程 A 申请资源成功,此时需要将sem
的值减 1。之后进程 A 便可对共享内存进行一系列操作。然而,在进程 A 访问共享内存期间,若进程 B 申请访问该共享内存资源,此时sem
的值变为 0,进程 B 会被挂起。直到进程 A 访问共享内存结束后将sem
的值加 1,这时才会将进程 B 唤起,随后进程 B 再对该共享内存进行访问操作。在这种情况下,无论何时都只会有一个进程对同一份共享内存进行访问操作,从而解决了临界资源的互斥问题。
其中信号量sem
减减的操作我们称为P
操作,而信号量sem
加加的操作我们成为V
操作,PV
操作就是申请与释放信号量的过程,而且进程中访问临界资源的代码我们称之为临界区。
并且我们还需要保证PV
操作是原子的,即只有不做与做完两种状态,不存在正在做这种状态。
因为从汇编角度看,我们的
--
和++
操作其实是不安全的,他们转成汇编,一般会对应三条汇编指令:**从内存中读取数据到 CPU 中;CPU 内进行操作;CPU 将结果写回内存。**进程在运行的时候,随时可能被切换,这就导致在多进程共享信号量下,--
和++
操作可能会导致信号量的值发生错乱。为了防止这种情况,我们需要保证PV
操作的原子性,即只有一条汇编指令。
4.2 信号量相关函数
- 信号量的创建:int semget(key_t key, int nsems, int semflg)。
- 信号量的获取:int semop(int semid, struct sembuf *sops, unsigned nsops)。
- 信号量的销毁:int semctl(int semid, int semnum, int cmd, …)。
5. system V的数据结构
在我们操作系统中,肯定会同时存在大量进程进行通信,即肯定会存在大量的system V
,为了方便把这些申请的临界资源管理起来,操作系统本身肯定会维护一个关于它们的数据结构。
比如这是维护共享内存的数据结构:
struct shmid_ds
{
struct ipc_perm shm_perm; /* operation perms */
int shm_segsz; /* size of segment (bytes) */
__kernel_time_t shm_atime; /* last attach time */
__kernel_time_t shm_dtime; /* last detach time */
__kernel_time_t shm_ctime; /* last change time */
__kernel_ipc_pid_t shm_cpid; /* pid of creator */
__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
unsigned short shm_nattch; /* no. of current attaches */
unsigned short shm_unused; /* compatibility */
void *shm_unused2; /* ditto - used by DIPC */
void *shm_unused3; /* unused */
};
这是维护消息队列的数据结构:
struct msqid_ds
{
struct ipc_perm msg_perm;
struct msg *msg_first; /* first message on queue,unused */
struct msg *msg_last; /* last message in queue,unused */
__kernel_time_t msg_stime; /* last msgsnd time */
__kernel_time_t msg_rtime; /* last msgrcv time */
__kernel_time_t msg_ctime; /* last change time */
unsigned long msg_lcbytes; /* Reuse junk fields for 32 bit */
unsigned long msg_lqbytes; /* ditto */
unsigned short msg_cbytes; /* current number of bytes on queue */
unsigned short msg_qnum; /* number of messages in queue */
unsigned short msg_qbytes; /* max number of bytes on queue */
__kernel_ipc_pid_t msg_lspid; /* pid of last msgsnd */
__kernel_ipc_pid_t msg_lrpid; /* last receive pid */
};
这是维护信号量的数据结构:
struct semid_ds
{
struct ipc_perm sem_perm; /* permissions .. see ipc.h */
__kernel_time_t sem_otime; /* last semop time */
__kernel_time_t sem_ctime; /* last change time */
struct sem *sem_base; /* ptr to first semaphore in array */
struct sem_queue *sem_pending; /* pending operations to be processed */
struct sem_queue **sem_pending_last; /* last pending operation */
struct sem_undo *undo; /* undo requests on this array */
unsigned short sem_nsems; /* no. of semaphores in array */
};
其中我们发现无论时共享内存,消息队列,还是信号量数据结构的第一个成员都是一个ipc_perm
类型的结构体变量,定义如下:
struct ipc_perm
{
__kernel_key_t key;
__kernel_uid_t uid;
__kernel_gid_t gid;
__kernel_uid_t cuid;
__kernel_gid_t cgid;
__kernel_mode_t mode;
unsigned short seq;
};
其中就包含了我们关键的系统表示符key
。
其中msqid_ds
,msqid_ds
,semid_ds``与ipc_perm
结构体分别在/usr/include/linux/sem.h
和/usr/include/linux/ipc.h
中定义。
所以操作系统管理system V
的IPC
资源,本质就是对ipc_perm
的管理,在Linux
中,就是通过一个柔性数组所管理的。
我们如果想访问某个IPC资源,只需要通过数组下标找到对应资源,再进行强转即可。比如我们访问共享内存中的sh_ctime
成员,只需(struct shmid_ds* )array[下标]->sh_ctime
。而前面我们所使用的用户层标识符shmid
、msqid
、semid
本质上就是内核中柔性数组的下标。