「Linux」Linux的信号量集
所谓信号量集,就是由多个信号量组成的一个数组。作为一个整体,信号量集中的所有信号量使用同一个等待队列。Linux的信号量集为进程请求多个资源创造了条件。Linux规定,当进程的一个操作需要多个共享资源时,如果只成功获得了其中的部分资源,那么这个请求即告失败,进程必须立即释放所有已获得资源,以防止形成死锁。 信号量集的结构 信号量结构 描述信号量的内核数据结构如下: struct sem { intsemval; /* 信号量的当前值 */ intsempid; /* 上一次操作本信号的进程PID */ }; 其中,域semval为一个整型变量,表示相应共享资源的被占用情况;域sempid则记录了上一次使用这个信号量的进程的标识号。 信号量集的结构 如果把若干个信号量组成一个数组sem[],那么这个数组就是信号量集。使用信号量集可以同时把多个共享资源设置为互斥资源。 Linux用一个数组头来管理这个信号量集,它包含信号量集的所有基本信息。 数组头的结构sem_array如下: struct sem_array { struct kern_ipc_permsem_perm;/* IPC许可结构 */ time_t sem_otime;/* 上一次信号量的操作时间 */ time_t sem_ctime;/* 信号量变化时间 */ struct sem *sem_base;/* 指向信号量数组的指针 */ struct list_headsem_pending;/* 等待队列 */ struct list_headlist_id;/* undo结构 */ unsigned long sem_nsems;/* 信号量集里面信号量的数目 */ }; 结构的第一个域sem_perm为检查用户权限的许可结构。数组头结构中的指针sem_base指向信号量数组,该数组中的每一个元素都是sem结构的变量,即信号量。 一个信号量集的结构如下图所示: 从上图可以看出,信号量集统一有一个进程等待队列,而不是每个信号量都有一个,这正是信号量集的特点。 进程等待队列结构sem_queue如下: struct sem_queue { struct list_headlist; /* queue of pending operations */ struct task_struct*sleeper; /* 指向等待进程控制块的指针 */ struct sem_undo *undo; /* undo请求操作结构指针 */ int pid; /* 请求操作的进程标识 */ int status; /* 操作完成状态 */ struct sembuf *sops; /* 挂起的操作集 */ int nsops; /* 操作数目 */ int alter; /* does the operation alter the array? */ }; 等待队列是一个由进程控制块所组成的队列,每个进程控制块代表着一个等待进程,sem_queue的域为sleeper指向了该等待队列。 另外,为了使系统可以从等待进程控制块中得到该进程所在的等待队列,进程控制块task_struct中有一个指向等待队列的指针semsleeping。 内核管理结构 Linux系统所有的信号量集都注册在一个数组中,该数组是内核全局数据结构struct ipc_id_ary的一个域。结构struct ipc_id_ary的定义如下: struct ipc_id_ary { int size; struct kern_ipc_perm *p[0]; //存放段描述结构的数组 }; 结构中的数组p[]就是信号量集的注册数组。 数组p[]暂时只定义了0个元素,数组的长度在系统运行时会在相应的操作里动态地增加或减少。 为了方便对上述数组进行管理,Linux又定义了一个数组头struct ipc_ids。struct ipc_ids的定义如下: struct ipc_ids { int in_use; unsigned short seq; unsigned short seq_max; struct rw_semaphore rw_mutex; struct idr ipcs_idr; struct ipc_id_ary *entries; //指向struct ipc_id_ary的指针 }; 很清楚,域entries就是指向信号量集数组的指针。 另外需要注意的是,由于为了充分利用内存空间,进程消亡时需要及时释放其所创建的信号量集,所以数组p[]的下标是动态的。这也就意味着以信号量在数组中的位置(下标)来作为标识不唯一,因此在上述结构中有一个叫做序列号的域seq,系统每增加一个信号量集,系统就会将seq加1,然后把信号量集在数组p[]中的下标与之拼接起来形成唯一的标识,以供内核来识别。 内核对于信号量集的管理结构如下图所示: 信号量集的操作 信号量集的创建或打开 进程可以通过调用函数semget()创建或打开一个信号量集,这个函数是通过系统调用sys_semget()来实现的。系统调用sys_semget()的原型如下: asmlinkage long sys_semget(key_t key, int nsems, int semflg); 其中,参数key是用户给定的键值;参数semflg是该函数的功能标志。 系统调用sys_semget()有两个功能:如果参数semflg的IPC_CREATE的值给定为1,则这个系统调用会为用户创建或打开一个信号量集,并返回信号量集标识符;如果为0,则会在系统已有的信号量集中寻找与键值相同的信号量集,找到后,打开该信号量集并返回信号量集的标识号。参数nsems用来指明在所创建的信号量集中信号量的个数,即定义sem_base指向的数组的大小。 信号量集的操作 用于信号量操作的函数是semop()。为了用户的方便,Linux提供了数据结构sembuf,用户在这个数据结构中指明对信号量的操作。sembuf结构定义如下: struct sembuf { unsigned short sem_num;/* 信号量集在集中的序号 */ short sem_op; /* 信号量操作 */ short sem_flg;/* 操作标志 */ }; 其中,域sem_num指明待操作信号量在集中的位置;域sem_op就是对信号量的增量。通常,在访问共享资源之前,域sem_op应设为-1(对信号量进行减1的P操作);访问之后,设为1(对信号量进行加1的V操作)。 前面讲过,为了防止产生死锁,信号量集的操作必须对集中的所有信号量同时操作,所以用户还需要定义一个其长度与信号量数目相等的sembuf类型数组,以便把各个信号量的sembuf结构数据存放到对应的元素中。 函数semop()由系统调用sys_semop()实现,其原型如下: asmlinkage long sys_semop(int semid, struct sembuf __user *sops, unsigned nsops); 其中,参数semid为信号量集的标识;参数sops就是指向上述sembuf数组的指针,数组每个元素都是对应信号量集的操作结构sembuf;参数nspos为这个数组的长度。 结构undo 介绍信号量的基本原理时曾经说过,P和V操作必须成对出现。也就是说,对于Linux信号量集,在临界段前要用semop()来请求资源,而在临界段后要用semop()来释放资源,但在具体应用中可能会因进程非正常中止而导致临界段没有机会来释放资源。 如果有产生这种情况的可能,进程必须将释放资源的任务转交给内核来完成。即在调用semop()请求资源时,把传递给函数的sembuf结构的域sem_flg设置为SEM_UNDO。这样,函数semop()在执行时就会为信号量配置一个sem_undo的结构,并在该结构中记录释放信号量的调整值;然后把信号量集中所有sem_undo组成一个队列,并在等待进程队列中用指针undo指向该队列。 也就是说,通过设置SEM_UNDO,当进程非正常中止时内核会产生响应操作,以保证信号量处于正常状态。 结构sem_undo定义如下: struct sem_undo { struct list_headlist_proc;/* per-process list: all undos from one process. */ /* rcu protected */ struct rcu_head rcu; /* rcu struct for sem_undo() */ struct sem_undo_list*ulp; /* sem_undo_list for the process */ struct list_headlist_id;/* per semaphore array list: all undos for one array */ int semid; /* 信号量集标识符 */ short * semadj; /* 存放信号量集调整值的数组指针 */ }; 于是,当系统执行内核函数do_ext()结束一个进程时,如果sembuf结构中的sem_flg的值为SEM_UNDO,则该函数会扫描该进程的sem_undo队列,并根据每个sem_undo结构中的调整信息,依次调整各个信号量值,以释放各个信号量。 信号量的控制 为实现对信号量的初始化等控制,Linux提供了函数semctl()。其对应的内核函数原型如下: asmlinkage long sys_semctl(int semid, int semnum, int cmd, union semun arg); 其中,semid为信号量集的表示;semnum为信号量的数目;cmd为操作命令;arg为信号量的初始值。 从参数定义中可知,arg的类型为union semun。该类型定义如下: union semun { int val; /* 信号量初始值 */ struct semid_ds __user *buf;/* buffer for IPC_STAT & IPC_SET */ unsigned short __user *array;/* array for GETALL & SETALL */ struct seminfo __user *__buf;/* buffer for IPC_INFO */ void __user *__pad; }; 内核函数sys_semctl()将根据其命令参数cmd(第三个参数)及参数arg来对信号量集实时控制。 进程控制块中关于信号量集的域 进程使用信号量集的相关信息也被记录在进程控制块中。进程控制块与信号量及相关的域如下: struct task_struct { ... struct sem_undo * semundo; //指向进程使用的信号量集undo队列 struct sem_queue * semsleeping; //指向进程所在等待队列的指针 ... }; 其实就是两个指针:一个指向进程使用的信号量undo队列;另一个指向进程所在的等待队列。 特别地,信号量集的undo队列被组织在进程控制块和信号量集两个队列中,如下图所示: