一、管道
在Linux 中,管道是一种使用非常频繁的通信机制。从本质上说,管道也是一种文件,但它又和一般的文件有所不同,管道可以克服使用文件进行通信的两个问题,具体表现如下所述。
• 限制管道的大小。实际上,管道是一个固定大小的缓冲区。在Linux 中,该缓冲区的大小为1 页,即4KB,使得它的大小不像文件那样不加检验地增长。使 用单个固定缓冲区也会带来问题,比如在写管道时可能变满,当这种情况发生时,随后对管道的write()调用将默认地被阻塞,等待某些数据被读取,以便腾出足够的空间供write()调用写。
• 读取进程也可能工作得比写进程快。当所有当前进程数据已被读取时,管道变空。当这种情况发生时,一个随后的read()调用将默认地被阻塞,等待某些数据被写入,这解决了read()调用返回文件结束的问题。
注意,从管道读数据是一次性操作,数据一旦被读,它就从管道中被抛弃,释放空间以便写更多的数据。
(一)、管道的结构
在Linux 中,管道的实现并没有使用专门的数据结构,而是借助了文件系统的file 结构和VFS 的索引节点inode。通过将两个 file 结构指向同一个临时的 VFS 索引节点,而这个 VFS 索引节点又指向一个物理页面而实现的。如图
中有两个 file 数据结构,但它们定义文件操作例程地址是不同的,其中一个是向管道中写入数据的例程地址,而另一个是从管道中读出数据的例程地址。这样,用户程序的系统调用仍然是通常的文件操作,而内核却利用这种抽象机制实现了管道这一特殊操作。
一个普通的管道仅可供具有共同祖先的两个进程之间共享,并且这个祖先必须已经建立了供它们使用的管道。
注意,在管道中的数据始终以和写数据相同的次序来进行读,这表示lseek()系统调用对管道不起作用。
二、信号
(一)、信号在内核中的表示
中断的响应和处理都发生在内核空间,而信号的响应发生在内核空间,信号处理程序的执行却发生在用户空间。
那么,什么时候检测和响应信号呢?通常发生在以下两种情况下:
(1)当前进程由于系统调用、中断或异常而进入内核空间以后,从内核空间返回到用户空间前夕;
(2)当前进程在内核中进入睡眠以后刚被唤醒的时候,由于检测到信号的存在而提前返回到用户空间。
当有信号要响应时,处理器执行路线的示意图如图
当用户进程通过系统调用刚进入内核的时候,CPU会自动在该进程的内核栈上压入下图所示的内容:
在处理完系统调用以后,就要调用do_signal()函数进行设置frame等工作。这时内核堆栈的状态应该跟下图左半部分类似(系统调用将一些信息压入栈了):
在找到了信号处理函数之后,do_signal() 函数首先把内核堆栈中存放返回执行点的eip保存为old_eip,然后将eip替换为信号处理函数的地址,然后将内核中保存的“原ESP”(即用户态栈地址)减去一定的值,目的是扩大用户态的栈,然后将内核栈上的内容保存到用户栈上,这个过程就是设置frame.值得注意的是下面两点:
1、之所以把EIP的值设置成信号处理函数的地址,是因为一旦进程返回用户态,就要去执行信号处理程序,所以EIP要指向信号处理程序而不是原来应该执行的地址。
2、之所以要把frame从内核栈拷贝到用户栈,是因为进程从内核态返回用户态会清理这次调用所用到的内核栈(类似函数调用),内核栈又太小,不能单纯的在栈上保存另一个frame(想象一下嵌套信号处理),而我们需要EAX(系统调用返回值)、EIP这些信息以便执行完信号处理函数后能继续执行程序,所以把它们拷贝到用户态栈以保存起来。
这时进程返回用户空间,就会根据内核栈中的EIP值执行信号处理函数。那么,信号处理程序执行完后,怎么返回程序继续执行呢?
信号处理程序执行完毕之后,进程会主动调用sigreturn()系统调用再次回到内核,查看有没有其他信号需要处理,如果没有,这时内核就会做一些善后工作,将之前保存的frame恢复到内核栈,恢复eip的值为old_eip,然后返回用户空间,程序就能够继续执行。至此,内核遍完成了一次(或几次)信号处理工作。
三、System V 的IPC 机制
为了提供与其他系统的兼容性,Linux 也支持3 种system Ⅴ的进程间通信机制:消息、信号量(semaphores)和共享内存,Linux 对这些机制的实施大同小异。我们把信号量、消息和共享内存统称System V IPC 的对象,每一个对象都具有同样类型的接口,即系统调用。就像每个文件都有一个打开文件号一样,每个对象也都有唯一的识别号,进程可以通过系统调用传递的识别号来存取这些对象,与文件的存取一样,对这些对象的存取也要验证存取权限,System V IPC 可以通过系统调用对对象的创建者设置这些对象的存取权限。在Linux 内核中,System V IPC 的所有对象有一个公共的数据结构pc_perm 结构,它是IPC 对象的权限描述,在linux/ipc.h 中定义如下:
struct ipc_perm
{
key_t key; /* 键 */
ushort uid; /* 对象拥有者对应进程的有效用户识别号和有效组识别号 */
ushort gid;
ushort cuid; /* 对象创建者对应进程的有效用户识别号和有效组识别号 */
ushort cgid;
ushort mode; /* 存取模式 */
ushort seq; /* 序列号 */
};
(1)系统中每个信号量的数据结构(sem)
struct sem
{
int semval; /* 信号量的当前值 */
unsigned short semzcnt; /* # waiting for zero */
unsigned short semncnt; /* # waiting for increase */
int sempid; /*在信号量上最后一次操作的进程识别号*/
};
(2)系统中表示信号量集合(set)的数据结构(semid_ds)
struct semid_ds
{
struct ipc_perm sem_perm; /* IPC 权限 */
long sem_otime; /* 最后一次对信号量操作(semop)的时间 */
long sem_ctime; /* 对这个结构最后一次修改的时间 */
struct sem *sem_base; /* 在信号量数组中指向第一个信号量的指针 */
struct sem_queue *sem_pending; /* 待处理的挂起操作*/
struct sem_queue **sem_pending_last; /* 最后一个挂起操作 */
struct sem_undo *undo; /* 在这个数组上的undo 请求 */
ushort sem_nsems; /* 在信号量数组上的信号量号 */
};
(3)系统中每一信号量集合的队列结构(sem_queue)
struct sem_queue
{
struct sem_queue *next; /* 队列中下一个节点 */
struct sem_queue **prev; /* 队列中前一个节点, *(q->prev) == q */
struct wait_queue *sleeper; /* 正在睡眠的进程 */
struct sem_undo *undo; /* undo 结构*/
int pid; /* 请求进程的进程识别号 */
int status; /* 操作的完成状态 */
struct semid_ds *sma; /*有操作的信号量集合数组 */
struct sembuf *sops; /* 挂起操作的数组 */
int nsops; /* 操作的个数 */
};
信号量集的功能分成了两部分:标志组和等待任务链表。标志组中存放了信号量集的所有信号,而等待任务链表中的每个结点都对应着一个叫做FLAG_NODE的结构。FLAG_NODE结构实质上就是等待任务控制块,其中当sem_flg为SEM_UNDO时标志着等待任务控制块,其结构体相当于链表中的节点相连
struct sembuf
{
ushort sem_num; /* 在数组中信号量的索引值 */
short sem_op; /* 信号量操作值(正数、负数或0) */
short sem_flg; /* 操作标志,为IPC_NOWAIT 或SEM_UNDO
如果进程被挂起,Linux 必须保存信号量的操作状态并将当前进程放入等待队列。为此,Linux 内核在堆栈中建立一个 sem_queue 结构并填充该结构。新的 sem_queue 结构添加到集合的等待队列中(利用 sem_pending 和 sem_pending_last 指针)。当前进程放入sem_queue 结构的等待队列中(sleeper)后调用调度程序选择其他的进程运行。
当某个进程修改了信号量而进入临界区之后,却因为崩溃或被“杀死(kill)”而没有退出临界区,这时,其他被挂起在信号量上的进程永远得不到运行机会,这就是所谓的死锁。Linux 通过维护一个信号量数组的调整列表(semadj)来避免这一问题。其基本思想是,当应用这些“调整”时,让信号量的状态退回到操作实施前的状态。
(二)、消息队列
Linux 中的消息可以被描述成在内核地址空间的一个内部链表,每一个消息队列由一个IPC 的标识号唯一地标识。Linux 为系统中所有的消息队列维护一个 msgque 链表,该链表中的每个指针指向一个 msgid_ds 结构,该结构完整描述一个消息队列。
(1)消息缓冲区(msgbuf)
/* msgsnd 和msgrcv 系统调用使用的消息缓冲区*/
struct msgbuf
{
long mtype; /* 消息的类型,必须为正数 */
char mtext[1]; /* 消息正文 */
};
(2)消息结构(msg)
struct msg
{
struct msg *msg_next; /* 队列上的下一条消息 */
long msg_type; /*消息类型*/
char *msg_spot; /* 消息正文的地址 */
short msg_ts; /* 消息正文的大小 */
};
(3)消息队列结构(msgid_ds)
/* 在系统中的每一个消息队列对应一个msgid_ds 结构 */
struct msgid_ds
{
struct ipc_perm msg_perm;
struct msg *msg_first; /* 队列上第一条消息,即链表头*/
struct msg *msg_last; /* 队列中的最后一条消息,即链表尾 */
time_t msg_stime; /* 发送给队列的最后一条消息的时间 */
time_t msg_rtime; /* 从消息队列接收到的最后一条消息的时间 */
time_t msg_ctime; /* 最后修改队列的时间*/
ushort msg_cbytes; /*队列上所有消息总的字节数 */
ushort msg_qnum; /*在当前队列上消息的个数 */
ushort msg_qbytes; /* 队列最大的字节数 */
ushort msg_lspid; /* 发送最后一条消息的进程的pid */
ushort msg_lrpid; /* 接收最后一条消息的进程的pid */
};
/* 在系统中 每一个共享内存段都有一个shmid_ds 数据结构. */
struct shmid_ds
{
struct ipc_perm shm_perm; /* 操作权限 */
int shm_segsz; /* 段的大小(以字节为单位) */
time_t shm_atime; /* 最后一个进程附加到该段的时间 */
time_t shm_dtime; /* 最后一个进程离开该段的时间 */
time_t shm_ctime; /* 最后一次修改这个结构的时间 */
unsigned short shm_cpid; /*创建该段进程的 pid */
unsigned short shm_lpid; /* 在该段上操作的最后一个进程的pid */
short shm_nattch; /*当前附加到该段的进程的个数 */
/* 下面是私有的 */
unsigned short shm_npages; /*段的大小(以页为单位) */
unsigned long *shm_pages; /* 指向frames -> SHMMAX 的指针数组 */
struct vm_area_struct *attaches; /* 对共享段的描述 */
};