本章介绍 Linux 2.4 内核中实现的信号量、共享内存和消息队列 IPC 机制。本章分为四个部分。前三部分分别介绍 信号量、消息队列 和 共享内存 的接口和支持函数。最后一部分介绍所有这三种机制共享的一组通用函数和数据结构。
本节介绍的函数实现了用户级信号量机制。请注意,此实现依赖于内核自旋锁和内核信号量的使用。为避免混淆,术语“内核信号量”将用于指代内核信号量。所有其他使用“信号量”一词的地方都将指代用户级信号量。
对 sys_semget() 的整个调用都受到全局 sem_ids.sem 内核信号量的保护。
如果必须创建新的信号量集,则调用 newary() 函数来创建和初始化新的信号量集。新集合的 ID 将返回给调用者。
如果为现有信号量集提供了键值,则调用 ipc_findkey() 来查找相应的信号量描述符数组索引。在返回信号量集 ID 之前,将验证调用者的参数和权限。
对于 IPC_INFO、SEM_INFO 和 SEM_STAT 命令,调用 semctl_nolock() 来执行必要的功能。
对于 GETALL、GETVAL、GETPID、GETNCNT、GETZCNT、IPC_STAT、SETVAL 和 SETALL 命令,调用 semctl_main() 来执行必要的功能。
对于 IPC_RMID 和 IPC_SET 命令,调用 semctl_down() 来执行必要的功能。在所有这些操作过程中,都持有全局 sem_ids.sem 内核信号量。
在验证调用参数后,信号量操作数据将从用户空间复制到临时缓冲区。如果小的临时缓冲区就足够了,则使用堆栈缓冲区。否则,将分配更大的缓冲区。在复制信号量操作数据后,全局信号量自旋锁被锁定,并且用户指定的信号量集 ID 被验证。还验证了信号量集的访问权限。
解析所有用户指定的信号量操作。在此过程中,维护一个计数,记录所有设置了 SEM_UNDO 标志的操作。如果任何操作从信号量值中减去,则设置 decrease
标志;如果任何信号量值被修改(即增加或减少),则设置 alter
标志。验证要修改的每个信号量的编号。
如果为任何信号量操作断言了 SEM_UNDO,则在当前任务的撤销列表中搜索与此信号量集关联的撤销结构。在此搜索期间,如果发现任何撤销结构的信号量集 ID 为 -1,则调用 freeundos() 以释放撤销结构并将其从列表中删除。如果未找到此信号量集的撤销结构,则调用 alloc_undo() 来分配和初始化一个撤销结构。
调用 try_atomic_semop() 函数,并将 do_undo
参数设置为 0,以便执行操作序列。返回值指示操作是通过、失败还是因为需要阻塞而未执行。以下进一步描述了每种情况
try_atomic_semop() 函数返回零,表示序列中的所有操作都成功。在这种情况下,调用 update_queue() 来遍历信号量集的挂起信号量操作队列,并唤醒任何不再需要阻塞的睡眠任务。这完成了此情况下的 sys_semop() 系统调用的执行。
如果 try_atomic_semop() 返回负值,则表示遇到了失败条件。在这种情况下,没有执行任何操作。当信号量操作会导致无效的信号量值,或者标记为 IPC_NOWAIT 的操作无法完成时,就会发生这种情况。然后将错误条件返回给 sys_semop() 的调用者。
在 sys_semop() 返回之前,会调用 update_queue() 来遍历信号量集的挂起信号量操作队列,并唤醒任何不再需要阻塞的睡眠任务。
try_atomic_semop() 函数返回 1,表示由于其中一个信号量会阻塞,因此未执行信号量操作序列。对于这种情况,初始化一个新的 sem_queue 元素,其中包含这些信号量操作。如果这些操作中的任何一个会改变信号量的状态,则将新的队列元素添加到队列的尾部。否则,将新的队列元素添加到队列的头部。
将当前任务的 semsleeping
元素设置为指示任务正在此 sem_queue 元素上睡眠。当前任务被标记为 TASK_INTERRUPTIBLE,并且 sem_queue 的 sleeper
元素设置为将此任务标识为睡眠者。然后解锁全局信号量自旋锁,并调用 schedule() 以使当前任务进入睡眠状态。
唤醒后,任务重新锁定全局信号量自旋锁,确定它为何被唤醒以及应如何响应。处理以下情况
status
元素设置为 1,则任务被唤醒以重试信号量操作。再次调用 try_atomic_semop() 以执行信号量操作序列。如果 try_atomic_sweep() 返回 1,则任务必须再次阻塞,如上所述。否则,返回 0 表示成功,或者在失败的情况下返回相应的错误代码。在 sys_semop() 返回之前,清除 current->semsleeping,并将 sem_queue 从队列中删除。如果任何指定的信号量操作都是更改操作(增加或减少),则调用 update_queue() 来遍历信号量集的挂起信号量操作队列,并唤醒任何不再需要阻塞的睡眠任务。status
元素未设置为 1,并且 sem_queue 元素未出队,则任务被中断唤醒。在这种情况下,系统调用失败,并返回 EINTR。在返回之前,清除 current->semsleeping,并将 sem_queue 从队列中删除。此外,如果任何操作是更改操作,则调用 update_queue()。status
元素未设置为 1,并且 sem_queue 元素已出队,则信号量操作已由 update_queue() 执行。队列 status
(成功时可能为 0,失败时可能为取反的错误代码)将成为系统调用的返回值。以下结构专门用于信号量支持
/* One sem_array data structure for each set of semaphores in the system. */
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 */
};
/* One semaphore structure for each semaphore in the system. */
struct sem {
int semval; /* current value */
int sempid; /* pid of last operation */
};
struct seminfo {
int semmap;
int semmni;
int semmns;
int semmnu;
int semmsl;
int semopm;
int semume;
int semusz;
int semvmx;
int semaem;
};
struct semid64_ds {
struct ipc64_perm sem_perm; /* permissions .. see
ipc.h */
__kernel_time_t sem_otime; /* last semop time */
unsigned long __unused1;
__kernel_time_t sem_ctime; /* last change time */
unsigned long __unused2;
unsigned long sem_nsems; /* no. of semaphores in
array */
unsigned long __unused3;
unsigned long __unused4;
};
/* One queue for each sleeping process in the system. */
struct sem_queue {
struct sem_queue * next; /* next entry in the queue */
struct sem_queue ** prev; /* previous entry in the queue, *(q->pr
ev) == q */
struct task_struct* sleeper; /* this process */
struct sem_undo * undo; /* undo structure */
int pid; /* process id of requesting process */
int status; /* completion status of operation */
struct sem_array * sma; /* semaphore array for operations */
int id; /* internal sem id */
struct sembuf * sops; /* array of pending operations */
int nsops; /* number of operations */
int alter; /* operation will alter semaphore */
};
/* semop system calls takes an array of these. */
struct sembuf {
unsigned short sem_num; /* semaphore index in array */
short sem_op; /* semaphore operation */
short sem_flg; /* operation flags */
};
/* Each task has a list of undo requests. They are executed automatically
* when the process exits.
*/
struct sem_undo {
struct sem_undo * proc_next; /* next entry on this process */
struct sem_undo * id_next; /* next entry on this semaphore set */
int semid; /* semaphore set identifier */
short * semadj; /* array of adjustments, one per
semaphore */
};
以下函数专门用于支持信号量
newary() 依赖于 ipc_alloc() 函数来分配新信号量集所需的内存。它为信号量集描述符和集合中的每个信号量分配足够的内存。分配的内存被清除,信号量集描述符的第一个元素的地址被传递给 ipc_addid()。ipc_addid() 为新的信号量集描述符保留一个数组条目,并初始化集合的 (struct kern_ipc_perm) 数据。全局 used_sems
变量按新集合中的信号量数量更新,并完成新集合的 (struct kern_ipc_perm) 数据的初始化。为此集合执行的其他初始化列在下面
sem_base
元素被初始化为紧跟在 (struct sem_array) 部分之后的新分配数据的地址。这对应于集合中第一个信号量的位置。sem_pending
队列被初始化为空。在调用 ipc_addid() 之后执行的所有操作都在持有全局信号量自旋锁时执行。在解锁全局信号量自旋锁之后,newary() 调用 ipc_buildid()(通过 sem_buildid())。此函数使用信号量集描述符的索引来创建唯一的 ID,然后将其返回给 newary() 的调用者。
freeary() 由 semctl_down() 调用,以执行下面列出的功能。它在全局信号量自旋锁锁定的情况下被调用,并在解锁自旋锁的情况下返回
semctl_down() 提供了 semctl() 系统调用的 IPC_RMID 和 IPC_SET 操作。在执行这两个操作中的任何一个之前,都会验证信号量集 ID 和访问权限,并且在这两种情况下,全局信号量自旋锁在整个操作过程中都处于持有状态。
IPC_RMID 操作调用 freeary() 以删除信号量集。
IPC_SET 操作更新信号量集的 uid
、gid
、mode
和 ctime
元素。
semctl_nolock() 由 sys_semctl() 调用,以执行 IPC_INFO、SEM_INFO 和 SEM_STAT 功能。
IPC_INFO 和 SEM_INFO 导致临时 seminfo 缓冲区被初始化并加载不变的信号量统计数据。然后,在持有全局 sem_ids.sem
内核信号量的同时,根据给定的命令(IPC_INFO 或 SEM_INFO)更新 seminfo 结构的 semusz
和 semaem
元素。系统调用的返回值设置为最大信号量集 ID。
SEM_STAT 导致临时 semid64_ds 缓冲区被初始化。然后,在持有全局信号量自旋锁的同时,将 sem_otime
、sem_ctime
和 sem_nsems
值复制到缓冲区中。然后将此数据复制到用户空间。
semctl_main() 由 sys_semctl() 调用,以执行许多支持的功能,如下面的小节中所述。在执行以下任何操作之前,semctl_main() 会锁定全局信号量自旋锁并验证信号量集 ID 和权限。自旋锁在返回之前释放。
GETALL 操作将当前信号量值加载到临时内核缓冲区中,并将其复制到用户空间。如果信号量集很小,则使用小的堆栈缓冲区。否则,将临时释放自旋锁以分配更大的缓冲区。在将信号量值复制到临时缓冲区时,持有自旋锁。
SETALL 操作将信号量值从用户空间复制到临时缓冲区,然后再复制到信号量集。在将值从用户空间复制到临时缓冲区以及验证合理值时,会释放自旋锁。如果信号量集很小,则使用堆栈缓冲区,否则分配更大的缓冲区。重新获得并持有自旋锁,同时对信号量集执行以下操作
sem_ctime
值。在 IPC_STAT 操作中,sem_otime
、sem_ctime
和 sem_nsems
值被复制到堆栈缓冲区中。在释放自旋锁后,数据然后被复制到用户空间。
对于非错误情况下的 GETVAL,系统调用的返回值设置为指定信号量的值。
对于非错误情况下的 GETPID,系统调用的返回值设置为与信号量的上次操作关联的 pid
。
对于非错误情况下的 GETNCNT,系统调用的返回值设置为等待信号量小于零的进程数。此数字由 count_semncnt() 函数计算。
对于非错误情况下的 GETZCNT,系统调用的返回值设置为等待信号量设置为零的进程数。此数字由 count_semzcnt() 函数计算。
在验证新的信号量值后,执行以下功能
sem_ctime
值。count_semncnt() 计算等待信号量值小于零的任务数。
count_semzcnt() 计算等待信号量值为零的任务数。
update_queue() 遍历信号量集的挂起 semop 队列,并调用 try_atomic_semop() 以确定哪些信号量操作序列会成功。如果队列元素的状态指示阻塞任务已被唤醒,则跳过该队列元素。对于队列的其他元素,q-alter
标志作为撤销参数传递给 try_atomic_semop(),指示任何更改操作都应在返回之前撤销。
如果操作序列会阻塞,则 update_queue() 返回而不进行任何更改。
如果其中一个信号量操作会导致无效的信号量值,或者标记为 IPC_NOWAIT 的操作无法完成,则操作序列可能会失败。在这种情况下,阻塞在信号量操作序列上的任务将被唤醒,并且队列状态将设置为相应的错误代码。队列元素也会出队。
如果操作序列是非更改操作,则它们会将零值作为撤销参数传递给 try_atomic_semop()。如果这些操作成功,则认为它们已完成并从队列中删除。阻塞的任务被唤醒,队列元素 status
设置为指示成功。
如果操作序列会更改信号量值,但可以成功,则会唤醒不再需要阻塞的睡眠任务。队列状态设置为 1,以指示阻塞的任务已被唤醒。操作尚未执行,因此队列元素不会从队列中删除。信号量操作将由唤醒的任务执行。
try_atomic_semop() 由 sys_semop() 和 update_queue() 调用,以确定信号量操作序列是否都会成功。它通过尝试执行每个操作来确定这一点。
如果遇到阻塞操作,则进程中止,并且所有操作都将反转。如果设置了 IPC_NOWAIT,则返回 -EAGAIN。否则,返回 1 以指示信号量操作序列被阻塞。
如果信号量值调整超出系统限制,则所有操作都将反转,并返回 -ERANGE。
如果序列中的所有操作都成功,并且 do_undo
参数为非零值,则所有操作都将反转,并返回 0。如果 do_undo
参数为零,则所有操作都成功并保持有效,并且更新信号量集的 sem_otime
字段。
当全局信号量自旋锁被临时释放并且需要再次锁定时,调用 sem_revalidate()。它由 semctl_main() 和 alloc_undo() 调用。它验证信号量 ID 和权限,并在成功后,返回时全局信号量自旋锁处于锁定状态。
freeundos() 遍历进程撤销列表以搜索所需的撤销结构。如果找到,则从列表中删除撤销结构并释放。返回指向进程列表中的下一个撤销结构的指针。
alloc_undo() 期望在全局信号量自旋锁锁定的情况下被调用。如果发生错误,则在解锁它的情况下返回。
全局信号量自旋锁被解锁,并调用 kmalloc() 以为 sem_undo 结构以及集合中每个信号量的调整值数组分配足够的内存。成功后,通过调用 sem_revalidate() 重新获得全局自旋锁。
然后初始化新的 semundo 结构,并将此结构的地址放置在调用者提供的地址处。然后将新的撤销结构放置在当前任务的撤销列表的头部。
sem_exit() 由 do_exit() 调用,负责执行退出任务的所有撤销调整。
如果当前进程被阻塞在信号量上,则在持有全局信号量自旋锁的同时,将其从 sem_queue 列表中删除。
然后遍历当前任务的撤销列表,并在围绕列表的每个元素的处理持有和释放全局信号量自旋锁时执行以下操作。为每个撤销元素执行以下操作
sem_otime
参数。当列表的处理完成时,清除 current->semundo 值。
对 sys_msgget() 的整个调用都受到全局消息队列信号量 (msg_ids.sem) 的保护。
如果必须创建新的消息队列,则调用 newque() 函数来创建和初始化新的消息队列,并将新的队列 ID 返回给调用者。
如果为现有消息队列提供了键值,则调用 ipc_findkey() 来查找全局消息队列描述符数组 (msg_ids.entries) 中的相应索引。在返回消息队列 ID 之前,将验证调用者的参数和权限。查找操作和验证在持有全局消息队列自旋锁 (msg_ids.ary) 时执行。
传递给 sys_msgctl() 的参数是:消息队列 ID (msqid
)、操作 (cmd
) 以及指向 msgid_ds 类型用户空间缓冲区 (buf
) 的指针。此函数中提供了六个操作:IPC_INFO、MSG_INFO、IPC_STAT、MSG_STAT、IPC_SET 和 IPC_RMID。验证消息队列 ID 和操作参数;然后,按如下方式执行操作 (cmd)
全局消息队列信息被复制到用户空间。
初始化 struct msqid64_ds 类型的临时缓冲区,并锁定全局消息队列自旋锁。在验证调用进程的访问权限后,与消息队列 ID 关联的消息队列信息被加载到临时缓冲区中,全局消息队列自旋锁被解锁,临时缓冲区的内容通过 copy_msqid_to_user() 复制到用户空间。
用户数据通过 copy_msqid_to_user() 复制进来。全局消息队列信号量和自旋锁在最后获取和释放。在验证消息队列 ID 和当前进程访问权限后,消息队列信息将使用用户提供的数据进行更新。稍后,调用 expunge_all() 和 ss_wakeup() 以唤醒消息队列的接收者和发送者等待队列上所有睡眠的进程。这是因为某些接收者现在可能会因更严格的访问权限而被排除,而某些发送者现在可能能够由于队列大小增加而发送消息。
获取全局消息队列信号量,并锁定全局消息队列自旋锁。在验证消息队列 ID 和当前任务访问权限后,调用 freeque() 以释放与消息队列 ID 相关的资源。全局消息队列信号量和自旋锁被释放。
sys_msgsnd() 接收消息队列 ID (msqid
)、指向 struct msg_msg 类型缓冲区 (msgp
) 的指针、要发送的消息大小 (msgsz
) 以及指示等待与不等待的标志 (msgflg
) 作为参数。消息队列 ID 有两个任务等待队列和一个消息等待队列与之关联。如果接收者等待队列中有一个任务正在等待此消息,则该消息将直接传递给接收者,并且接收者将被唤醒。否则,如果消息等待队列中有足够的可用空间,则该消息将保存在此队列中。作为最后的手段,发送任务将其自身排队到发送者等待队列中。以下是对 sys_msgsnd() 执行的操作的更深入讨论
msg
中。还初始化 msg
的消息类型和消息大小字段。msgflg
中指定了 IPC_NOWAIT,则解锁全局消息队列自旋锁,释放消息的内存资源,并返回 EAGAIN。msg
排队到消息等待队列 (msq->q_messages) 中。更新消息队列描述符的 q_cbytes
和 q_qnum
字段,以及指示消息使用的总字节数和系统范围内的消息总数的全局变量 msg_bytes
和 msg_hdrs
。q_lspid
和 q_stime
字段,并释放全局消息队列自旋锁。sys_msgrcv() 函数接收消息队列 ID (msqid
)、指向 msg_msg (msgp
) 类型缓冲区的指针、所需的消息大小 (msgsz
)、消息类型 (msgtyp
) 和标志 (msgflg
) 作为参数。它搜索与消息队列 ID 关联的消息等待队列,查找队列中与请求类型匹配的第一个消息,并将其复制到给定的用户缓冲区中。如果在消息等待队列中未找到此类消息,则请求任务将排队到接收者等待队列中,直到所需的消息可用为止。以下是对 sys_msgrcv() 执行的操作的更深入讨论
msgtyp
派生搜索模式。然后,sys_msgrcv() 锁定全局消息队列自旋锁,并获取与消息队列 ID 关联的消息队列描述符。如果不存在此类消息队列,则返回 EINVAL。msgtyp
的第一个消息。msgflg
指示不允许错误,则解锁全局消息队列自旋锁并返回 E2BIG。msgflg
。如果设置了 IPC_NOWAIT,则解锁全局消息队列自旋锁并返回 ENOMSG。否则,接收者将按如下方式排队到接收者等待队列中msr
,并将其添加到等待队列的头部。msr
的 r_tsk
字段设置为当前任务。r_msgtype
和 r_mode
字段分别使用所需的消息类型和模式进行初始化。msgflg
指示 MSG_NOERROR,则 msr
的 r_maxsize 字段设置为 msgsz
的值,否则设置为 INT_MAX。r_msg
字段被初始化为指示尚未收到消息。msr
的 r_msg
字段。此字段用于存储管道消息,或者在发生错误时,存储错误状态。如果 r_msg
字段填充了期望的消息,则跳转到 最后一步。否则,全局消息队列自旋锁将再次被锁定。r_msg
字段,以查看在等待自旋锁期间是否已收到消息。如果消息已被接收,则执行 最后一步。r_msg
字段保持不变,则任务被唤醒是为了重试。在这种情况下,msr
将被出队。如果任务有信号待处理,则全局消息队列自旋锁被解锁,并返回 EINTR。否则,函数需要 返回 并重试。r_msg
字段显示在睡眠期间发生错误,则全局消息队列自旋锁被解锁,并返回错误。msp
的地址有效后,消息类型被加载到 msp
的 mtype
字段中,并调用 store_msg() 将消息内容复制到 msp
的 mtext
字段。最后,消息的内存由函数 free_msg() 释放。消息队列的数据结构在 msg.c 中定义。
/* one msq_queue structure for each present queue on the system */
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;
};
/* one msg_msg structure for each message */
struct msg_msg {
struct list_head m_list;
long m_type;
int m_ts; /* message text size */
struct msg_msgseg* next;
/* the actual message follows immediately */
};
/* message segment for each message */
struct msg_msgseg {
struct msg_msgseg* next;
/* the next part of the message follows immediately */
};
/* one msg_sender for each sleeping sender */
struct msg_sender {
struct list_head list;
struct task_struct* tsk;
};
/* one msg_receiver structure for each sleeping receiver */
struct msg_receiver {
struct list_head r_list;
struct task_struct* r_tsk;
int r_mode;
long r_msgtype;
long r_maxsize;
struct msg_msg* volatile r_msg;
};
struct msqid64_ds {
struct ipc64_perm msg_perm;
__kernel_time_t msg_stime; /* last msgsnd time */
unsigned long __unused1;
__kernel_time_t msg_rtime; /* last msgrcv time */
unsigned long __unused2;
__kernel_time_t msg_ctime; /* last change time */
unsigned long __unused3;
unsigned long msg_cbytes; /* current number of bytes on queue */
unsigned long msg_qnum; /* number of messages in queue */
unsigned long msg_qbytes; /* max number of bytes on queue */
__kernel_pid_t msg_lspid; /* pid of last msgsnd */
__kernel_pid_t msg_lrpid; /* last receive pid */
unsigned long __unused4;
unsigned long __unused5;
};
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 msq_setbuf {
unsigned long qbytes;
uid_t uid;
gid_t gid;
mode_t mode;
};
newque() 为新的消息队列描述符(struct msg_queue)分配内存,然后调用 ipc_addid(),该函数为新的消息队列描述符保留一个消息队列数组条目。消息队列描述符的初始化如下:
q_stime
和 q_rtime
字段被初始化为 0。q_ctime
字段被设置为 CURRENT_TIME。q_qbytes
) 被设置为 MSGMNB,队列当前使用的字节数 (q_cbytes
) 被初始化为 0。q_messages
)、接收器等待队列 (q_receivers
) 和发送器等待队列 (q_senders
) 都被初始化为空。在调用 ipc_addid() 之后的所有操作都在持有全局消息队列自旋锁的情况下执行。解锁自旋锁后,newque() 调用 msg_buildid(),它直接映射到 ipc_buildid()。ipc_buildid() 使用消息队列描述符的索引来创建一个唯一的消息队列 ID,然后该 ID 返回给 newque() 的调用者。
当要删除消息队列时,将调用 freeque() 函数。此函数假定调用函数已锁定全局消息队列自旋锁。它释放与该消息队列关联的所有内核资源。首先,它调用 ipc_rmid()(通过 msg_rmid())从全局消息队列描述符数组中删除消息队列描述符。然后,它调用 expunge_all 来唤醒所有接收器,并调用 ss_wakeup() 来唤醒所有在此消息队列上睡眠的发送器。稍后,全局消息队列自旋锁被释放。存储在此消息队列中的所有消息都被释放,并且消息队列描述符的内存被释放。
ss_wakeup() 唤醒给定消息发送器等待队列中所有等待的任务。如果此函数由 freeque() 调用,则队列中的所有发送器都将被出队。
ss_add() 接收消息队列描述符和消息发送器数据结构作为参数。它用当前进程填充消息发送器数据结构的 tsk
字段,将当前进程的状态更改为 TASK_INTERRUPTIBLE,然后将消息发送器数据结构插入到给定消息队列的发送器等待队列的头部。
如果给定的消息发送器数据结构 (mss
) 仍在关联的发送器等待队列中,则 ss_del() 从队列中删除 mss
。
expunge_all() 接收消息队列描述符 (msq
) 和一个整数值 (res
) 作为参数,指示唤醒接收器的原因。对于与 msq
关联的每个睡眠接收器,r_msg
字段被设置为指示的唤醒原因 (res
),并且关联的接收任务被唤醒。当消息队列被删除或执行消息控制操作时,将调用此函数。
当进程发送消息时,sys_msgsnd() 函数首先调用 load_msg() 函数将消息从用户空间加载到内核空间。消息在内核内存中表示为数据块的链表。与第一个数据块关联的是一个 msg_msg 结构,它描述了整个消息。与 msg_msg 结构关联的数据块大小限制为 DATA_MSG_LEN。数据块和结构在一个连续的内存块中分配,该内存块可能高达内存中的一页。如果完整消息无法放入第一个数据块,则会分配其他数据块,并将其组织成链表。这些额外的数据块大小限制为 DATA_SEG_LEN,并且每个数据块都包含一个关联的 msg_msgseg) 结构。msg_msgseg 结构和关联的数据块在一个连续的内存块中分配,该内存块可能高达内存中的一页。此函数成功后返回新的 msg_msg 结构的地址。
store_msg() 函数由 sys_msgrcv() 调用,以将接收到的消息重新组装到调用者提供的用户空间缓冲区中。由 msg_msg 结构和任何 msg_msgseg 结构描述的数据按顺序复制到用户空间缓冲区。
free_msg() 函数释放消息数据结构 msg_msg 和消息段的内存。
convert_mode() 由 sys_msgrcv() 调用。它接收指定的消息类型地址 (msgtyp
) 和标志 (msgflg
) 作为参数。它根据 msgtyp
和 msgflg
的值向调用者返回搜索模式。如果 msgtyp
为空,则返回 SEARCH_ANY。如果 msgtyp
小于 0,则 msgtyp
设置为其绝对值,并返回 SEARCH_LESSEQUAL。如果在 msgflg
中指定了 MSG_EXCEPT,则返回 SEARCH_NOTEQUAL。否则,返回 SEARCH_EQUAL。
testmsg() 函数检查消息是否满足接收器指定的条件。如果以下条件之一为真,则返回 1:
pipelined_send() 允许进程直接向等待的接收器发送消息,而不是将消息存放在关联的消息等待队列中。调用 testmsg() 函数以查找正在等待给定消息的第一个接收器。如果找到,则将等待的接收器从接收器等待队列中删除,并唤醒关联的接收任务。消息存储在接收器的 r_msg
字段中,并返回 1。如果找不到等待消息的接收器,则返回 0。
在搜索接收器的过程中,可能会找到请求大小小于给定消息大小的潜在接收器。这些接收器将从队列中删除,并以 E2BIG 的错误状态唤醒,该错误状态存储在 r_msg
字段中。然后继续搜索,直到找到有效的接收器或队列耗尽。
copy_msqid_to_user() 将内核缓冲区的内容复制到用户缓冲区。它接收用户缓冲区、msqid64_ds 类型的内核缓冲区和一个版本标志作为参数,该版本标志指示新的 IPC 版本与旧的 IPC 版本。如果版本标志等于 IPC_64,则调用 copy_to_user() 直接从内核缓冲区复制到用户缓冲区。否则,初始化 struct msqid_ds 类型的临时缓冲区,并将内核数据转换为此临时缓冲区。稍后,调用 copy_to_user() 将临时缓冲区的内容复制到用户缓冲区。
函数 copy_msqid_from_user() 接收 struct msq_setbuf 类型的内核消息缓冲区、用户缓冲区和一个版本标志作为参数,该版本标志指示新的 IPC 版本与旧的 IPC 版本。在新 IPC 版本的情况下,调用 copy_from_user() 将用户缓冲区的内容复制到 msqid64_ds 类型的临时缓冲区。然后,内核缓冲区的 qbytes
、uid
、gid
和 mode
字段将填充来自临时缓冲区的相应字段的值。在旧 IPC 版本的情况下,将使用 struct msqid_ds 类型的临时缓冲区。
对 sys_shmget() 的整个调用都受到全局共享内存信号量的保护。
在必须创建新的共享内存段的情况下,将调用 newseg() 函数来创建和初始化新的共享内存段。新段的 ID 将返回给调用者。
在为现有共享内存段提供键值的情况下,将在共享内存描述符数组中查找相应的索引,并在返回共享内存段 ID 之前验证调用者的参数和权限。查找操作和验证在持有全局共享内存自旋锁时执行。
临时的 shminfo64 缓冲区加载了系统范围的共享内存参数,并复制到用户空间,供调用应用程序访问。
在收集系统范围的共享内存统计信息时,将持有全局共享内存信号量和全局共享内存自旋锁。调用 shm_get_stat() 函数来计算驻留在内存中的共享内存页数和换出的共享内存页数。其他统计信息包括共享内存页总数和正在使用的共享内存段数。swap_attempts
和 swap_successes
的计数被硬编码为零。这些统计信息存储在临时的 shm_info 缓冲区中,并复制到用户空间,供调用应用程序使用。
对于 SHM_STAT 和 IPC_STAT,将初始化 struct shmid64_ds 类型的临时缓冲区,并锁定全局共享内存自旋锁。
对于 SHM_STAT 情况,共享内存段 ID 参数应为直接索引(即 0 到 n,其中 n 是系统中共享内存 ID 的数量)。在验证索引后,调用 ipc_buildid()(通过 shm_buildid())将索引转换为共享内存 ID。在 SHM_STAT 的通过情况下,共享内存 ID 将是返回值。请注意,这是一个未记录的功能,但为了 ipcs(8) 程序而维护。
对于 IPC_STAT 情况,共享内存段 ID 参数应为通过调用 shmget() 生成的 ID。在继续之前,将验证 ID。在 IPC_STAT 的通过情况下,返回值将为 0。
对于 SHM_STAT 和 IPC_STAT,都将验证调用者的访问权限。所需的统计信息将加载到临时缓冲区中,然后复制到调用应用程序。
在验证访问权限后,将锁定全局共享内存自旋锁,并验证共享内存段 ID。对于 SHM_LOCK 和 SHM_UNLOCK,都将调用 shmem_lock() 来执行该功能。shmem_lock() 的参数标识要执行的功能。
在 IPC_RMID 期间,全局共享内存信号量和全局共享内存自旋锁在整个函数中都被持有。共享内存 ID 被验证,然后,如果没有当前连接,则调用 shm_destroy() 来销毁共享内存段。否则,设置 SHM_DEST 标志以标记其进行销毁,并设置 IPC_PRIVATE 标志以防止其他进程能够引用共享内存 ID。
在验证共享内存段 ID 和用户访问权限后,共享内存段的 uid
、gid
和 mode
标志将使用用户数据进行更新。shm_ctime
字段也会更新。这些更改在持有全局共享内存信号量和全局共享内存自旋锁时进行。
sys_shmat() 接收共享内存段 ID、应在其中附加共享内存段的地址 (shmaddr
) 和将在下面描述的标志作为参数。
如果 shmaddr
为非零,并且指定了 SHM_RND 标志,则 shmaddr
将向下舍入到 SHMLBA 的倍数。如果 shmaddr
不是 SHMLBA 的倍数,并且未指定 SHM_RND,则返回 EINVAL。
调用者的访问权限将被验证,并且共享内存段的 shm_nattch
字段将递增。请注意,此递增保证了连接计数为非零,并防止共享内存段在附加到该段的过程中被销毁。这些操作在持有全局共享内存自旋锁时执行。
调用 do_mmap() 函数来创建到共享内存段页面的虚拟内存映射。这是在持有当前任务的 mmap_sem
信号量时完成的。MAP_SHARED 标志传递给 do_mmap()。如果调用者提供了地址,则 MAP_FIXED 标志也会传递给 do_mmap()。否则,do_mmap() 将选择映射共享内存段的虚拟地址。
注意:shm_inc() 将在 do_mmap() 函数调用中通过 shm_file_operations
结构调用。调用此函数以设置 PID、设置当前时间并递增此共享内存段的连接数。
在调用 do_mmap() 后,将同时获取全局共享内存信号量和全局共享内存自旋锁。然后递减连接计数。由于调用 shm_inc(),shmat() 调用的连接计数净变化为 1。如果在递减连接计数后,发现结果计数为零,并且该段被标记为销毁 (SHM_DEST),则调用 shm_destroy() 以释放共享内存段资源。
最后,共享内存映射到的虚拟地址将返回给用户指定地址的调用者。如果 do_mmap() 返回了错误代码,则此失败代码将作为系统调用的返回值传递。
在执行 sys_shmdt() 时,将持有全局共享内存信号量。在当前进程的 mm_struct
中搜索与共享内存地址关联的 vm_area_struct
。找到后,将调用 do_munmap() 以撤消共享内存段的虚拟地址映射。
另请注意,do_munmap() 执行对 shm_close() 的回调,后者执行共享内存记账功能,并在没有其他连接时释放共享内存段资源。
sys_shmdt() 无条件返回 0。
struct shminfo64 {
unsigned long shmmax;
unsigned long shmmin;
unsigned long shmmni;
unsigned long shmseg;
unsigned long shmall;
unsigned long __unused1;
unsigned long __unused2;
unsigned long __unused3;
unsigned long __unused4;
};
struct shm_info {
int used_ids;
unsigned long shm_tot; /* total allocated shm */
unsigned long shm_rss; /* total resident shm */
unsigned long shm_swp; /* total swapped shm */
unsigned long swap_attempts;
unsigned long swap_successes;
};
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 shmid64_ds {
struct ipc64_perm shm_perm; /* operation perms */
size_t shm_segsz; /* size of segment (bytes) */
__kernel_time_t shm_atime; /* last attach time */
unsigned long __unused1;
__kernel_time_t shm_dtime; /* last detach time */
unsigned long __unused2;
__kernel_time_t shm_ctime; /* last change time */
unsigned long __unused3;
__kernel_pid_t shm_cpid; /* pid of creator */
__kernel_pid_t shm_lpid; /* pid of last operator */
unsigned long shm_nattch; /* no. of current attaches */
unsigned long __unused4;
unsigned long __unused5;
};
struct shmem_inode_info {
spinlock_t lock;
unsigned long max_index;
swp_entry_t i_direct[SHMEM_NR_DIRECT]; /* for the first blocks */
swp_entry_t **i_indirect; /* doubly indirect blocks */
unsigned long swapped;
int locked; /* into memory */
struct list_head list;
};
当需要创建新的共享内存段时,将调用 newseg() 函数。它作用于新段的三个参数:键、标志和大小。在验证要创建的共享内存段的大小在 SHMMIN 和 SHMMAX 之间,并且共享内存段的总数不超过 SHMALL 之后,它会分配一个新的共享内存段描述符。稍后将调用 shmem_file_setup() 函数来创建 tmpfs 类型的未链接文件。返回的文件指针保存在关联的共享内存段描述符的 shm_file
字段中。文件大小设置为与段大小相同。新的共享内存段描述符被初始化并插入到全局 IPC 共享内存描述符数组中。共享内存段 ID 由 shm_buildid()(通过 ipc_buildid())创建。此段 ID 保存在共享内存段描述符的 id
字段以及关联 inode 的 i_ino
字段中。此外,结构 shm_file_operation
中定义的共享内存操作的地址存储在关联的文件中。全局变量 shm_tot
的值(指示系统范围内的共享内存段总数)也会增加以反映此更改。成功后,段 ID 将返回给调用者应用程序。
shm_get_stat() 循环遍历所有共享内存结构,并计算共享内存使用的内存页总数和换出的共享内存页总数。每个共享内存段都有一个文件结构和一个 inode 结构。由于所需数据是通过 inode 获取的,因此将按顺序锁定和解锁访问的每个 inode 结构的自旋锁。
shmem_lock() 接收指向共享内存段描述符的指针和一个标志(指示锁定与解锁)作为参数。共享内存段的锁定状态存储在关联的 inode 中。此状态与所需的锁定状态进行比较;如果它们匹配,shmem_lock() 只会返回。
在持有关联 inode 的信号量时,将设置 inode 的锁定状态。以下项目列表发生在共享内存段中的每个页面中:
在 shm_destroy() 期间,将调整共享内存页面的总数以计入共享内存段的删除。调用 ipc_rmid()(通过 shm_rmid())来删除共享内存 ID。调用 shmem_lock 来解锁共享内存页面,有效地将每个页面的引用计数递减为零。调用 fput() 来递减关联文件对象的使用计数器 f_count
,并在必要时释放文件对象资源。调用 kfree() 来释放共享内存段描述符。
shm_inc() 设置 PID、设置当前时间并递增给定共享内存段的连接数。这些操作在持有全局共享内存自旋锁时执行。
shm_close() 更新 shm_lprid
和 shm_dtim
字段,并递减连接的共享内存段数。如果共享内存段没有其他连接,则调用 shm_destroy() 来释放共享内存段资源。这些操作都在持有全局共享内存信号量和全局共享内存自旋锁时执行。
shmem_file_setup() 函数使用给定的名称和大小在 tmpfs 文件系统中设置一个未链接的文件。如果系统内存资源足以支持此文件,它将在 tmpfs 的挂载根目录下创建一个新的目录项,并分配一个新的文件描述符和一个新的 tmpfs 类型 inode 对象。然后,它通过调用 d_instantiate() 将新的目录项对象与新的 inode 对象关联,并将目录项对象的地址保存在文件描述符中。inode 对象的 i_size
字段设置为文件大小,i_nlink
字段设置为 0,以将 inode 标记为未链接。此外,shmem_file_setup() 将 shmem_file_operations
结构的地址存储在 f_op
字段中,并正确初始化文件描述符的 f_mode
和 f_vfsmnt
字段。调用 shmem_truncate() 函数来完成 inode 对象的初始化。成功后,shmem_file_setup() 返回新的文件描述符。
Linux 的信号量、消息和共享内存机制构建在一组通用原语之上。这些原语在以下章节中描述。
如果内存分配大于 PAGE_SIZE,则使用 vmalloc() 分配内存。否则,调用 kmalloc() 并使用 GFP_KERNEL 来分配内存。
当添加新的信号量集、消息队列或共享内存段时,ipc_addid() 首先调用 grow_ary() 以确保相应的描述符数组的大小对于系统最大值来说足够大。搜索描述符数组以查找第一个未使用的元素。如果找到未使用的元素,则递增正在使用的描述符计数。kern_ipc_perm 结构用于初始化新的资源描述符,并返回新描述符的数组索引。当 ipc_addid() 成功时,它会在锁定给定 IPC 类型的全局自旋锁的情况下返回。
ipc_rmid() 从 IPC 类型的全局描述符数组中删除 IPC 描述符,更新正在使用的 ID 计数,并在必要时调整相应描述符数组中的最大 ID。返回指向与给定 IPC ID 关联的 IPC 描述符的指针。
ipc_buildid() 创建一个唯一的 ID,用于与给定 IPC 类型中的每个描述符关联。此 ID 在添加新的 IPC 元素(例如,新的共享内存段或新的信号量集)时创建。IPC ID 可以轻松转换为相应的描述符数组索引。每种 IPC 类型都维护一个序列号,每次添加描述符时,该序列号都会递增。通过将序列号与 SEQ_MULTIPLIER 相乘,并将乘积添加到描述符数组索引来创建 ID。用于创建特定 IPC ID 的序列号随后存储在相应的描述符中。序列号的存在使得可以检测到陈旧 IPC ID 的使用。
ipc_checkid() 将给定的 IPC ID 除以 SEQ_MULTIPLIER,并将商与保存的对应描述符的 seq 值进行比较。如果它们相等,则认为 IPC ID 有效并返回 1。否则,返回 0。
grow_ary() 处理给定 IPC 类型的最大(可调整)ID 数量可以动态更改的可能性。它强制执行当前最大限制,使其不大于永久系统限制 (IPCMNI),并在必要时将其向下调整。它还确保现有描述符数组足够大。如果现有数组大小足够大,则返回当前最大限制。否则,将分配一个新的更大的数组,旧数组将复制到新数组中,并且旧数组将被释放。在更新给定 IPC 类型的描述符数组时,将持有相应的全局自旋锁。
ipc_findkey() 搜索指定的 ipc_ids 对象的描述符数组,并搜索指定的键。一旦找到,将返回相应描述符的索引。如果未找到键,则返回 -1。
ipcperms() 检查用户、组和其他权限以访问 IPC 资源。如果授予权限,则返回 0,否则返回 -1。
ipc_lock() 将 IPC ID 作为其参数之一。它锁定给定 IPC 类型的全局自旋锁,并返回指向与指定 IPC ID 对应的描述符的指针。
ipc_unlock() 释放指示的 IPC 类型的全局自旋锁。
ipc_lockall() 锁定给定 IPC 机制(即共享内存、信号量和消息传递)的全局自旋锁。
ipc_unlockall() 解锁给定 IPC 机制(即共享内存、信号量和消息传递)的全局自旋锁。
ipc_get() 接收指向特定 IPC 类型(即共享内存、信号量或消息队列)和描述符 ID 的指针,并返回指向相应 IPC 描述符的指针。请注意,尽管每种 IPC 类型的描述符都是不同的数据类型,但通用的 kern_ipc_perm 结构类型作为第一个实体嵌入在每种情况下。ipc_get() 函数返回此通用数据类型。预期的模型是 ipc_get() 通过包装函数(例如 shm_get())调用,该包装函数将数据类型强制转换为正确的描述符数据类型。
ipc_parse_version() 从命令中删除 IPC_64 标志(如果存在),并返回 IPC_64 或 IPC_OLD。
信号量、消息和共享内存机制都使用以下通用结构:
每个 IPC 描述符都将此类型的数据对象作为首个元素。这使得可以使用此数据类型的指针,从任何通用 IPC 函数访问任何描述符。
/* used by in-kernel data structures */
struct kern_ipc_perm {
key_t key;
uid_t uid;
gid_t gid;
uid_t cuid;
gid_t cgid;
mode_t mode;
unsigned long seq;
};
ipc_ids 结构体描述了信号量、消息队列和共享内存的通用数据。此数据结构有三个全局实例:semid_ds
、msgid_ds
和 shmid_ds
,分别用于信号量、消息和共享内存。 在每个实例中,sem
信号量用于保护对结构的访问。entries
字段指向一个 IPC 描述符数组,而 ary
自旋锁保护对此数组的访问。seq
字段是一个全局序列号,创建新的 IPC 资源时,此字段会递增。
struct ipc_ids {
int size;
int in_use;
int max_id;
unsigned short seq;
unsigned short seq_max;
struct semaphore sem;
spinlock_t ary;
struct ipc_id* entries;
};
在 ipc_ids 结构的每个实例中,都存在一个 `struct ipc_id` 的数组。该数组是动态分配的,并且可以根据需要通过 grow_ary() 替换为更大的数组。 该数组有时被称为描述符数组,因为 IPC 通用函数将 kern_ipc_perm 数据类型用作通用描述符数据类型。
struct ipc_id {
struct kern_ipc_perm* p;
};