目录
⛳️推荐
前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站【Linux修行路】动静态库详解点击跳转到网站
一、消息队列
1.1 实现原理
由操作系统在内核中创建一个队列,A、B两个进程以数据块的形式将需要发送的数据链接到队列中,为了区分消息队列中的数据块是谁发送的,因此数据块本质上是一个结构体,它里面有一个类型字段用来该标识数据块是由哪个进程发送的。
1.2 消息队列接口
1.2.1 msgget——创建、获取一个消息队列
其中第一个参数 key
和共享内存中的那个 key
一样,也是通过 ftok
函数来获取。
1.2.2 msgctl——释放消息队列、获取消息队列属性
该函数用来控制一个消息队列,可以用它删除一个消息队列、获取消息队列的属性。
用户层描述消息队列的结构体 struct msqid_ds
:
1.2.3 msgsnd——发送数据
消息队列不需要挂接到进程的地址空间,直接通过系统调用向消息队列中写入数据,或者从消息队列中读取数据。
msqid
:用户层唯一标识一个消息队列的 ID,就是msgget
函数的返回值。msgp
:发送的数据块,是一个struct msgbuf
结构体类型,需要用户自己定义。msgsz
:数据块的大小。msgflg
:一般设置为0,表示消息队列如果满了,就阻塞式等待;IPC_N
OWAIT
表示消息队列如果为满,不阻塞,立即返回-1。
数据块的类型:
mtype
:标识数据块的类型。mtext
:用发送的信息。
1.2.4 msgrcv——从消息队列中检索数据块
msgtyp
:表示要检索的数据块类型。msgflag
:用来设置当消息队列为空的时候,阻塞还是非阻塞,选项和上面的一样。
1.3 消息队列的指令操作
ipcs -q
:查看当前操作系统中所有的消息队列。ipcrm -q msqid
:释放一个消息队列。
二、信号量
2.1 数据不一致问题、互斥、临界资源、临界区
共享内存没有同步与互斥机制,可能会出现 A 进程正在向共享内存中写入,还没有写完,B 进程就来读取,导致发方和收方的数据不完整,这就是数据不一致问题。
- A B看到的同一份资源,共享资源,如果不加保护,会导致数据不一致问题。
- 可以通过加锁来实现互斥访问,在任何时候,只允许一个执行流访问共享资源。
- 把共享的,任何时刻只允许一个执行流访问(执行访问代码)的资源称为——临界资源。
- 临界资源一般是操作系统或者用户维护的一段内存空间。
- 把访问临界资源的代码叫做临界区。
2.2 理解信号量
信号量是一种用于控制多个进程或线程对共享资源的访问的同步机制,它的本质是一把计数器,用于记录资源的可用数量。
在临界资源充足的情况下,如果出现多个执行流访问同一个临界资源,那属于编码 Bug。信号量保证的是,假设只有 n 个临界资源,不会出现 n+1 个执行流来访问临界资源,如果出现就出导致数据不一致问题。
- 申请计数器成功,就表示当前执行流具有访问临界资源的权限了。
- 申请到了计数器,并没有去访问临界资源,申请计数器是对资源的一种预定机制。
- 计数器可以有效的保证访问临界资源的执行流的数量。
- 所以,每个执行流想要访问临界资源的时候,不是直接访问,而是先申请计数器资源(信号量)。
2.3 二元信号量
如果临界资源只有一份,那么这个计数器的值只能是 1 或者 0,并且,在任何时候都只允许一个执行流访问共享资源。我们把这种只能为 1、0 两态的计数器就叫做——二元信号量。二元信号量本质是一把锁。计数器最大为 1,本质上是资源只有一份,也就是不要将临界资源分成很多块,而是当做一个整体,整体申请,整体释放,这样就能实现互斥。
总结:二元信号量主要用于实现对临界资源的互斥访问。
2.4 信号量也是共享资源
所有执行流想要使用临界资源,必须先来申请信号量,所以信号量也是一种共享资源。可能出现多个执行流同时来申请同一个信号量,信号量是用来保护临界资源的,前提是信号量得保证自身的安全。而我们的 --
和 ++
操作是不安全的,他们转成汇编,一般会对应三条汇编指令:从内存中读取数据到 CPU 中;CPU 内进行操作;CPU 将结果写回内存。进程在运行的时候,随时可能被切换,这就导致在多进程共享信号量的前提下, --
和 ++
操作可能会导致信号量的值发生错乱。
2.5 PV操作
PV操作:申请信号量,本质是对计数器 --
,称为 P
操作;释放共享资源,本质是对计数器 ++
,称为 V
操作。PV
操作一定是原子的。所谓原子性,就是一件事情,要么不做,要做就做完,只有两态,没有正在做这样的概念。站在技术角度来理解原子性就是:该操作只对应一条汇编指令,那么该操作就是原子的。
2.6 总结
2.7 信号量的接口
创建、获取一个信号量(集)——semget
nsems
:表示创建几个信号量。
控制信号量——semctl
semnum
:信号量的编号。
可以通过将 cmd
设置为 SETVAL
,再传递 union semun
来设置信号量的初始值。
申请释放信号量(PV操作)——semop
sem_num
:信号量的编号。sem_op
:1 表示V
操作;-1
表示P
操作。
2.8 信号量凭什么是进程通信的一种?
- 通信不仅仅是数据传输,相互协同也是,告诉某个执行流接下来可以干什么了,或者不可以干什么。
- 要协同,本质也是通信,信号量首先要被所有的通信进程看到。
三、内核中对 IPC 资源的管理
共享内存、消息队列、信号量,统称为操作系统中的 IPC 资源。为了管理这些资源,操作系统创建了三个结构体,分别用来描述这三种 IPC 资源。
3.1 三种描述 IPC 资源的结构体
struct shmid_kernel:
struct shmid_kernel /* private to the kernel */
{
struct kern_ipc_perm shm_perm;
struct file * shm_file;
int id;
unsigned long shm_nattch;
unsigned long shm_segsz;
time_t shm_atim;
time_t shm_dtim;
time_t shm_ctim;
pid_t shm_cprid;
pid_t shm_lprid;
struct user_struct *mlock_user;
};
struct msg_queue:
struct msg_queue {
struct kern_ipc_perm q_perm;
time_t q_stime; /* last msgsnd time */
time_t q_rtime; /* last msgrcv time */
time_t q_ctime; /* last change time */
unsigned long q_cbytes; /* current number of bytes on queue */
unsigned long q_qnum; /* number of messages in queue */
unsigned long q_qbytes; /* max number of bytes on queue */
pid_t q_lspid; /* pid of last msgsnd */
pid_t q_lrpid; /* last receive pid */
struct list_head q_messages;
struct list_head q_receivers;
struct list_head q_senders;
};
struct sem_array:
struct sem_array {
struct kern_ipc_perm sem_perm; /* permissions .. see ipc.h */
time_t sem_otime; /* last semop time */
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 long sem_nsems; /* no. of semaphores in array */
};
这三个结构体都有一个共性,结构体中的第一个数据类型都是 struct kern_ipc_perm
类型。
struct kern_ipc_perm:
struct kern_ipc_perm
{
spinlock_t lock;
int deleted;
key_t key;
uid_t uid;
gid_t gid;
uid_t cuid;
gid_t cgid;
mode_t mode;
unsigned long seq;
void *security;
};
3.2 操作系统对 IPC 资源的管理
无论是什么类型的 IPC 资源,一定都有一个属于自己的 struct kern_ipc_perm
结构。所以,操作系统就通过一个 structkern_ipc_perm*
类型的数组将所有的 IPC 资源管理了起来。
struct ipc_ids:
struct ipc_ids {
int in_use;
int max_id;
unsigned short seq;
unsigned short seq_max;
struct semaphore sem;
struct ipc_id_ary nullentry;
struct ipc_id_ary* entries;
};
struct ipc_id_arry:
struct ipc_id_ary {
int size;
struct kern_ipc_perm *p[0];
};
其中 p
就是维护当前操作系统中所有 IPC 资源的一个柔性数组。如何通过这个数组里存的 struct ipc_id_ary*
找到某一个具体的 IPC 对象呢(如何找到 struct shmid_kernel
、struct msg_queue
、struct sem_array
)?答案是通过强制类型转换,因为 kern_ipc_perm
是这三个结构体中的第一个成员,我们只要知道了一个 kern_ipc_perm
的地址,就相当于知道了某个具体 IPC 对象的起始地址,然后通过强制类型转换就可以访问到该 IPC 对象中的所有成员属性,这样就实现了对一个具体 IPC 对象的访问。例如:(struct shmid_kernel*)p[0]->q_stime
。那操作系统是如何知道要将其强制转化成什么类型呢?答案是,在 kern_ipc_perm
中一定有字段来标识该 kern_ipc_perm
是属于那种 IPC 资源的,这就是多态的雏形。我们在用户层面上使用的:shmid、msqid、semid本质上就是内核中 p
数组的下标。
ipc_id_arry 这个数组隶属于操作系统,不属于任何进程,数组下标是一直线性递增的,不会因为 IPC 资源的释放而改变它的递增属性,即当前操作系统中最后一个 IPC 资源的下标是 12,释放掉这个 IPC 资源,下一次再创建 IPC 资源,因为有递增属性,所以它的下标是13,而不是12,当递增到一定值的时候,会回绕到0。
🎁结语:
今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,您的支持就是我前进的动力!