目录
1.共享内存
1.1.共享内存的概念
为什么共享内存可以同管道一样作用与进程间的通信?本质上都是让进程可以看到同一份资源,可以是内核级的内存也可以是命名管道的文件。
System V 共享内存:
System V 共享内存允许不同的进程访问同一块物理内存区域,进程可以直接在这块内存中读写数据,而不需要进行繁琐的数据复制操作,这使得数据的传输速度非常快。
操作系统为了对共享内存进行管理,会先描述为 struct shmid_ds 结构体,再用数据结构组织起来,其中会存在引用计数(nattach)来代表共享内存被多少进程使用,后续会进一步讲解 struct shmid_ds 结构体。
共享内存数据结构:
struct shmid_ds {
struct ipc_perm shm_perm; /* Ownership and permissions */
size_t shm_segsz; /* Size of segment (bytes) */
time_t shm_atime; /* Last attach time */
time_t shm_dtime; /* Last detach time */
time_t shm_ctime; /* Last change time */
pid_t shm_cpid; /* PID of creator */
pid_t shm_lpid; /* PID of last shmat(2)/shmdt(2) */
shmatt_t shm_nattch; /* No. of current attaches */
...
};
struct ipc_perm {
key_t __key; /* Key supplied to shmget(2) */
uid_t uid; /* Effective UID of owner */
gid_t gid; /* Effective GID of owner */
uid_t cuid; /* Effective UID of creator */
gid_t cgid; /* Effective GID of creator */
unsigned short mode; /* Permissions + SHM_DEST and
SHM_LOCKED flags */
unsigned short __seq; /* Sequence number */
};
为什么需要共享内存:
进程间通过共享内存通信时,先由操作系统分配一块可共享的物理内存区域,映射到各进程的虚拟地址空间。进程直接读写该区域,无需数据拷贝,配合互斥锁等同步机制避免冲突,实现高效数据交换,尤其适合大容量、高频通信场景。
1.2共享内存使用指令
查看共享内存:
ipcs -m + shmid(共享内存编号)
删除共享内存:
ipcrm -q + shmid
1.3共享内存相关函数
shmget:
获取一个共享内存:shmget() 函数返回与参数 key 关联的 System V 共享内存段的标识符,而key是一个数字,key的值不重要,它在内核中具有唯一性,能够让不同的进程进行唯一性标识;
第一个进程可以通过key创建共享内存,第二个之后的进程只需要拿着同一个key就可以和第一个进程看到同一个共享内存;对于一个已经创建好的共享内存,key储存在共享内存的structXXX结构体中;
而size是你要创建共享内存的大小,单位字节,shmflg是创建模式:
IPC_CREAT:不存在就创建,存在就返回,若未指定该标志,shmget() 仅查找与 key 关联的段,并检查用户是否有访问权限。
IPC_CREAT | IPC_EXCL:不存在就创建,存在就出错返回,需与 IPC_CREAT 结合使用的目的:确保当段已存在时操作失败,即确保此共享内存始终只有一份!
IPC_CREAT | IPC_EXCL | MODE:还可以设置创建文件权限,一般设为0666。
如何来创建key?
ftok:
获取一个key:这里的 pathname 和 proj_id 可以是任意设定,如果创建key失败,那么更改一下即可,实质上是一套算法,对pathname和proj_id进行了数值计算
shmat&shmdt:
shmat链接共享内存:
shmid:共享内存段的标识符(由 shmget() 返回)。
shmaddr:指定附加地址(通常设为 NULL,由系统自动分配)。
shmflg:标志位,常用值:SHM_RDONLY:以只读方式附加;或 0:以读写方式附加。
返回值:成功时,shmat () 函数返回附加的共享内存段的地址;失败时,返回 (void *) -1,并设置 errno 以指示错误原因。
shmdt用来去关联共享内存:直接传入shmat返回的地址即可与共享内存去关联。
shmctl:
用来控制共享内存:
shmid:共享内存段的标识符。
cmd:命令类型,常用值:
- IPC_STAT:获取共享内存段的状态信息,存入 buf。
- IPC_SET:设置共享内存段的属性(如权限)。
- IPC_RMID:标记共享内存段为待删除状态(实际删除需所有进程分离后)。
buf:指向 struct shmid_ds 的指针,用于存储或设置共享内存段的信息。(一般设为nullptr)
1.4共享内存优缺点
优点:
速度快:避免了数据的多次复制,数据传输效率高。
使用方便:进程可以像操作普通内存一样操作共享内存。
缺点:
同步问题:由于多个进程可以同时访问共享内存,可能会出现数据竞争的问题。比如,一个进程正在写入数据,另一个进程同时读取数据,可能会导致数据不一致。这就需要使用其他的同步机制,如信号量,来保证数据的一致性。
管理复杂:需要手动管理共享内存的创建、连接、分离和删除,容易出现内存泄漏等问题。
1.5共享内存使用注意
①共享内存属于内核,进程终止它仍旧存在,这就是说 共享内存内部的数据,由用户自己维护!需要手动创建、链接、去关联、控制及释放;
②shmid是用户级的,key是内核级的,所以用户只需对shmid进行相关函数操作,无需考虑内核中的key;
③如果是进程异常退出导致的共享内存没有删除,共享内存的状态仍会设为dest,(待删除状态)【表示这块内存不能再被使用了】。
2.消息队列
2.1消息队列的概念
消息队列通过内核维护一个有序的数据缓冲区(队列),实现进程间的异步通信。即在内核中创建一个队列,发送方将消息放入队列,接收方从队列中取出消息处理,所有的消息队列先描述为 msqid_ds 结构体再组织起来。
进程可向队列中添加消息(如结构化数据),也可从队列中读取消息,消息按发送顺序存储和处理。无需接收方实时等待,发送方发送后即可继续执行,适合非实时、松耦合的通信场景,常见于分布式系统、异步任务处理等。
消息队列数据结构:
struct msqid_ds {
struct ipc_perm msg_perm; /* Ownership and permissions */
time_t msg_stime; /* Time of last msgsnd(2) */
time_t msg_rtime; /* Time of last msgrcv(2) */
time_t msg_ctime; /* Time of last change */
unsigned long __msg_cbytes; /* Current number of bytes in
queue (nonstandard) */
msgqnum_t msg_qnum; /* Current number of messages
in queue */
msglen_t msg_qbytes; /* Maximum number of bytes
allowed in queue */
pid_t msg_lspid; /* PID of last msgsnd(2) */
pid_t msg_lrpid; /* PID of last msgrcv(2) */
};
struct ipc_perm {
key_t __key; /* Key supplied to msgget(2) */
uid_t uid; /* Effective UID of owner */
gid_t gid; /* Effective GID of owner */
uid_t cuid; /* Effective UID of creator */
gid_t cgid; /* Effective GID of creator */
unsigned short mode; /* Permissions */
unsigned short __seq; /* Sequence number */
};
2.2消息队列使用指令
查看消息队列:
ipcs -q + msqid(共享内存编号)
删除消息队列:
ipcrm -q + msqid
2.3消息队列相关函数
msgget:
创建消息队列:key与共享内存一样,由ftok生成(具体方式参考共享内存中ftok的使用)
msgflg:使用与共享内存高度相似:
msgflg取值 | 描述 |
---|---|
IPC_CRAET | 创建共享内存时,如果共享内存已经已经存在,获取已经存在的共享内存;不存在则创建并返回 |
IPC_EXCL | 需要与IPC_CREAT组合使用,单独使用没有意义。如果带创建的共享内存存在,则出错返回;如果不存在,则创建并返回对应的共享内存 |
msgsnd&msgrcv: 
msgsnd发送消息:其中,第二个参数需要传入一个形式如下所示的结构体,这个结构体需要用户自己定义:
The msgp argument is a pointer to caller-defined structure of the following general form:
struct msgbuf {
long mtype; /* message type, must be > 0 */
char mtext[1]; /* message data */
};
上述中的mtext表示要发送的数据,因而mtext的数组大小由用户自己指定;而mtype用于标识数据,例如:A进程只读取mtype为1的数据,B进程只读取mtype为2的数据,即mtype标识这个数据要发送给哪些进程。
msqid:消息队列的标识符(由 msgget() 返回)。
msgflg:标志位,常用值:IPC_NOWAIT:如果队列已满,不会阻塞等待,而是会返回错误码EAGAIN(设置在errno中);或 0:默认行为,阻塞等待消息队列,直到消息队列中有空间。
msgrcv读取消息:与上面一样,同样需要自定义一下 struct msgbuf 结构体;
msqid:消息队列的标识符(由 msgget() 返回)。
msgsz:用于接收数据的msgbuf中的mtext的大小,即接收缓冲区的大小。
msgflg:
msgflg 取值 | 含义 |
---|---|
0 | 默认行为,阻塞等待消息队列,直到消息队列中有满足需求的数据 |
IPC_NOWAIT | 非阻塞等待消息队列,如果队列中没有对应需求的数据,则 errno 设置为 ENOMSG,并返回 |
MSG_EXCEPT | 接收队列中 msgbuf 中的 mtype 不为 msgtyp 的数据 |
MSG_NOERROR | 如果数据的长度长于接收方的 msgbuf 的 mtext 大小,则会发生截断,余下数据被丢弃 |
msgctl:
msqid:消息队列的标识符。
cmd:命令类型,常用值:
cmd取值 | 含义 |
---|---|
IPC_STAT | 获取消息队列的状态 |
IPC_SET | 设置消息队列的属性 |
IPC_RMID | 从系统中移除消息队列 |
buf:指向 struct msqid_ds 的指针,用于存储或设置共享内存段的信息。(一般设为nullptr)
3.信号量
本篇信号量不是重点,简单了解即可,先引入问题:
对于共享内存来说,一个进程在写,一个进程在读,会引发数据不一致的问题,而管道因为原子写入是安全的,这时候需要加保护,也就是加锁——互斥访问,任何时候,只允许一个执行流访问共享资源,具有锁的资源——临界资源,而访问临界资源的代码——临界区。
信号量就类似于一个计数器,当进程要访问临界资源需要申请计数器,tips:
1)申请计数器成功,就代表该进程具有访问资源的权限了;计数器减一;
2)申请了计数器资源,并不代表正在访问,是对临界资源的一种预订机制;
3)计数器可以有效保证进入共享资源的执行流的数量;
4)每一个执行流像访问共享资源中的一部分的时候,不是直接访问,而是先申请计数器资源,如果申请成功,才能进行访问;
5)将1/0两态的计数器称为——锁,其实就是将临界资源当作整体看待,整体申请,整体释放;
6)信号量计数器也是共享资源,直接减一并不安全,自己的安全如何保证?通过原子性操作(使用CAS技术),完成申请——P操作,释放——V操作;
7)信号量与其他执行流互相协同,虽然不直接传输数据,但是属于进程间通信
简单介绍一下接口:
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, ...);
补充: IPC在内核中数据结构的设计:
不管是共享内存/消息队列/信号量,都是由XXXid_ds描述的,并且第一个成员都是 struct ipc_perm 类型,事实上,操作系统将 struct ipc_perm 用数组 struct ipc_perm* array[] 组织起来,长度固定,循环访问,下标对应 XXXid(shmid/msqid/semid),因为 struct ipc_perm 中保存着key,并且标志着 struct ipc_perm 属于哪种类型,以方便对 XXXid_ds 的成员进行访问。