首先观察一下他们的结构体:共享内存(shmid_ds)、消息队列(msqid_ds)、信号量(semid_ds) 他们都有一个相同的成员结构体。ipc_perm 这个ipc_perm
结构体用于存储权限和相关信息,如所有者、用户组、权限模式等。
struct ipc_perm
{
key_t __key;/*Key*/
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 */
};
这里的关键字段包括:
__key
:这是在调用shmget、msgget、semget
时提供的键值,用于标识共享内存对象。
uid
、gid
、cuid
、cgid
:这些字段分别表示创建者和所有者的用户ID和组ID。
mode
:这是IPC对象的权限,包括读、写、执行等权限,以及共享内存的标记(如SHM_DEST
和SHM_LOCKED
)。
__seq
:这是一个序列号,用于跟踪IPC对象的唯一性。
这个结构体通常用于描述共享内存段、信号量集等IPC对象的相关信息。在Unix-like系统中,这些对象通常通过系统调用进行创建和管理
struct shmid_ds {
uid_t shm_perm.uid; // 所有者ID
gid_t shm_perm.gid; // 组ID
mode_t shm_perm.mode; // 访问权限
int shm_perm.__key; // IPC键值
struct ipc_pid shm_perm.cuid;
struct ipc_pid shm_perm.uid;
unsigned short shm_perm.mode;
unsigned short shm_perm._seq;
time_t shm_ctime; //创建时间
time_t shm_atime; //最后连接时间
time_t shm_dtime; //最后断开连接时间
size_t shm_segsz; //内存段大小
pid_t shm_cpid; //创建进程号
pid_t shm_lpid; //最后连接进程号
short shm_nattch; //当前挂接进程数
... // 其他成员
}
//我的系统中:
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) */
shmatt_t shm_nattch;/* No. of current attaches */
...
};
struct msqid_ds {
int msqid; /* 消息队列ID */
pid_t pid /* 拥有者进程ID */
uid_t uid; /* 拥有者用户ID */
gid_t gid; /* 拥有者组ID */
mode_t perms; /* 队列权限 */
struct timespec msg_stime; /* 最近一次发送消息的时间 */
struct timespec msg_rtime; /* 最近一次接收消息的时间 */
struct timespec msg_ctime; /* 最近一次更改消息队列属性时间 */
unsigned long long msg_qnum;/* 消息队列中的消息数量 */
unsigned long long msg_qbytes;/* 消息队列的最大消息大小 */
key_t key; /* 消息的键 */
bool msg_flg; /* 消息队列的标志位 */
};
//我的系统中:
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 semid_ds
{
struct ipc_perm sem_perm; /* Ownership and permissions */
time_t sem_otime; /* Last semop time */
time_t sem_ctime; /* Last change time */
unsigned long sem_nsems; /* No. of semaphores in set */
};
都包含一个ipc_perm
结构体作为其成员。这个ipc_perm
结构体用于存储他们的的权限和相关信息,如所有者、用户组、权限模式等。
在操作系统中存在一个结构体来管理所有的IPC结构,叫做 ipc_ids 里边有这么些字段 in_use,max_id ,seq,seq_max,sem,*entries
struct ipc_ids {
int in_use; // 当前使用资源的进程数量
int max_id; //当前系统中可用的最大资源ID
int seq; // 跟踪资源的序列号
int seq_max; // 最大序列号
int sem; // 信号量的标识符
struct ipc_id_ary *entries;
// 指向ipc_id_ary结构的指针
};
ipc_id_ary
结构体的可能结构如下:
struct ipc_id_ary {
int size; // 数组的大小
struct ipc_perm *p[]; // ipc_perm结构的柔性数组
};
这里,ipc_id_ary
结构体包含了一个size
字段,用于表示柔性数组的大小。柔性数组是C语言中一种特殊的数组,其大小可以动态设定。
而*p
是指向ipc_perm
结构体的指针数组,它是柔性数组。
在每个IPC的结构体中的第一个成员是一个结构体,而非一个指针,这个第一个成员的地址(指针)被统一的存储了起来在ipc_id_ary中。
所以要管理操作系统中所有的IPC资源,转化成了对于这个数组的增删查改。通过ipc_perm中的mode 字段知道是什么类型的资源,权限等信息,通过__key字段知道他的键值,通过 __seq 字段(序列号,是IPC的唯一标识符),确定其在操作系统中的唯一性。
我们想要访问IPC的其他成员时候,会进行一个类似多态的过程。遍历数组中的指针指向的ipc_perm成员,首先确认要访问IPC的类型。接着将这个指针转化。(第一个成员的指针就是原本结构体的指针)
所以假如访问的是一个shimd_ds,他在ipc_id_ary中的数组的[3]下标位置 访问这个shimd_ds 的 shm_atime成员 可以是:
((struct shimd_ds*)ipc_id_ary.p[3])->shm_atime
这是因为ipc_id_ary
数组中的下标实际上与操作系统中的IPC资源ID有关。
在Unix-like系统中,每个IPC资源都有一个唯一的标识符,称为序列号(sequence number)。操作系统内核为每个IPC资源分配一个唯一的序列号,并将其存储在ipc_perm
结构体的__seq
字段中。序列号用于区分不同的IPC资源,并确保每个资源在整个系统中具有唯一性。
ipc_id_ary
数组中的下标实际上就是IPC资源的序列号。由于操作系统需要能够快速查找和访问每个IPC资源,因此将序列号作为ipc_id_ary
数组的下标可以使得查找和访问更加高效。同时,由于序列号通常是一个较大的整数,因此ipc_id_ary
数组的下标也可能非常大。
需要注意的是,虽然ipc_id_ary
数组中的下标与IPC资源的序列号有关,但具体的下标值通常是由操作系统内核管理的,应用程序不需要关心下标值的细节。应用程序只需要使用ipc_perm
结构体中的__seq
字段来确定相应的IPC资源即可。
所以应用程序只需要记住序列号就能准确定位IPC成员。
所以访问这个shimd_ds 的 shm_atime成员 实际上是:
((struct shimd_ds*)ipc_id_ary.p[__seq])->shm_atime
其中__seq是通过一些过程得到的
其中有两个指针msg_msg* next 与 msg_msg *prev,这些指针用于将消息队列中的消息串联起来形成链表结构。
指向的结构体msg_msg可能的结构是:
struct msg_msg {
struct msg_msg *next; // 下一个消息的指针
struct msg_msg *prev; // 上一个消息的指针
long mtype; // 消息类型
size_t mtext_len; // 消息文本的长度
// 其他用于存储消息内容的成员
};
这里的msg_msg
结构体包含了链表操作所需的next
和prev
指针,用于连接消息队列中的不同消息。另外,mtype
成员用于表示消息的类型,mtext_len
成员用于存储消息文本的长度。
这里其他用于存储消息内容的成员是指的消息队列中一个消息的正文。
实际上,在消息队列的内核结构体msg_queue
中,msg_msg
并不包含对消息正文的直接引用。相反,msg_msg
结构体中保存了消息正文的长度(mtext_len
),而不是结构体本身。
消息正文实际上是相对于msg_msg
结构体来说的一个灵活数组,跟随在msg_msg
结构体的后面。在内核中,消息正文是紧密地存储在消息队列中,不需要使用额外的指针来引用它。msg_msg
结构体的定义只包含了在消息队列中管理消息所需的元数据,而不直接包含消息正文的结构体。(通过指针加类型和长度可以直接访问正文)
其中有一些字段 sem_base , sem_pending, sem_pending_last,undo
在共享内存的内核结构体sem_array
中,存在以下字段:*sem_base
、**sem_pending
、**sem_pending_last
和*undo
。还有下边三个字段
unsigned short sem_nsems; // 信号量数组中的信号量数量
time_t sem_otime; // 上次操作时间
time_t sem_ctime; // 最后改变时间
*sem_base
:这个字段是一个指针,指向共享内存中的信号量数组的起始位置。它用于访问共享内存中存储信号量的数据结构。
**sem_pending
:这个字段是一个指向指针的指针,指向一个挂起操作的信号量链表。每当有进程等待某个信号量时,该信号量可能会被放入挂起链表中,直到资源可用后再唤醒等待的进程。
**sem_pending_last
:类似于**sem_pending
,这个字段也是一个指向指针的指针,指向挂起操作的信号量链表的最后一个节点。它用于方便地向链表添加新的挂起信号量节点。
*undo
:这个字段是一个指向共享内存中的撤销信息的结构体的指针。撤销信息用于在进程因为某种原因终止后,对共享内存中的修改进行回滚,以保持数据的一致性。
unsigned short sem_nsems
:表示信号量的数量,即信号量数组的长度。
time_t sem_otime
:记录上次操作的时间。
time_t sem_ctime
:记录最后一次改变的时间。
ipc_perm, shmid_ds, msqid_ds, semid_ds 在内核中都有对应的结构体 core_ipc_perm, shmid_kernel, sg_msgseg, sem_array.这些内核结构体中或多或少有前边结构体的影子。或包含或含有指针。
可能在操作系统原理中我们能更上一层楼。这里是为了让我们更能理解这三个的共同点,理解在操作系统中他们是怎么组织的