Linux用户进程间通信机制在内核的实现



目录

[隐藏]

用户进程间通信

Linux下的进程间通信方法是从Unix平台继承而来的。Linux遵循POSIX标准(计算机环境的可移植性操作系统界面)。进程间通信有system V IPC标准、POSIX IPC标准及基于套接口(socket)的进程间通信机制。前两者通信进程局限在单个计算机内,后者则在单个计算机及不同计算机上都可以通信。

System V IPC包括System V消息队列、System V信号灯和System V共享内存,Posix IPC包括Posix消息队列、Posix信号灯和Posix共享内存区。Linux支持所有System V IPC和Posix IPC,并分别为它们提供了系统调用。其中,System V IPC在内核以统一的数据结构方式实现,Posix IPC一般以文件系统的机制实现。

用户应用程序经常用到C库的进程间通信函数,进程间通信函数的功能在内核中实现,C库通过系统调用和文件系统获取该功能。本章分别介绍了用户进程间通信机制在内核中的实现。

System V IPC对象管理

Linux内核将信号量、消息队列和共享内存三类IPC的通用操作进行抽象,形成通用的方法函数(函数名中含有"ipc")。每类具体的操作函数通过在函数参数中传入通用函数名的方法,继承通用方法函数。下面以信号量为例分析IPC通用操作函数,消息队列和共享内存的操作实现方法类似于信号量。

System V IPC数据结构

System V IPC数据结构包括名字空间结构ipc_namespace、ID集结构ipc_ids和IPC对象属性结构kern_ipc_perm。其中,结构ipc_namespace是所有IPC对象ID集的入口,结构ipc_ids描述每一类IPC对象(如:信号量对象)的ID,结构kern_ipc_perm描述IPC对象许可的共有对象,被每一类IPC对象结构继承。

进程通过系统调用进入内核空间后,通过结构ipc_namespace类型全局变量init_ipc_ns找到对应类型IPC对象的ID集,由ID集找到ID,再由ID找到对象的描述结构,从对象描述结构中获取通信数据了。结构ipc_ids与结构kern_ipc_perm的关系如图1所示。

第4章用户进 07.png

图1 结构ipc_ids与结构kern_ipc_perm的关系

下面分别说明System V IPC数据结构:

(1)IPC对象属性结构kern_ipc_perm

System V IPC在内核以统一的数据结构形式进行实现,它的对象包括System V的消息队列、信号量和共享内存。每个对象都有以下属性:

  • 每个创建者、创建者群组和其他人的读和写许可。
  • 对象创建者的UID和GID。
  • 对象所有者的UID和GID(初始时等于创建者的UID)。

进程在存取System V IPC对象时,规则如下:

  • 如果进程有root权限,则可以存取对象。
  • 如果进程的EUID是对象所有者或创建者的UID,那么检查相应的创建者许可比特位,查看是否有存取权限。
  • 如果进程的EGID是对象所有者或创建者的GID,或者进程所属群组中某个群组的GID就是对象所有者或创建者的GID,那么,检查相应的创建者群组许可比特位是滞有权限。
  • 否则,在存取时检查相应的"其他人"许可比特位。
System V IPC对象的属性用结构kern_ipc_perm描述,包括uid、gid、键值、ID号等信息,其列出如下(在include/linux/ipc.h中):
struct kern_ipc_perm
{
	spinlock_t	lock;     
	int		deleted;  /*删除标识,表示该结构对象已删除*/
	int		id;      /*id识别号,每个IPC对象的身份号,便于从IPC对象数组中获取该对象*/
	key_t		key;    /*键值:公有或私有*/
	uid_t		uid;
	gid_t		gid;
	uid_t		cuid;
	gid_t		cgid;
	mode_t		mode;   /*许可的组合*/
	unsigned long	seq;    /*在每个IPC对象类型中的序列号*/
	void		*security;     /*与SELinux安全相关的变量*/
};

在结构kern_ipc_perm中,键值为公有和私有。如果键是公有的,则系统中所有的进程通过权限检查后,均可以找到System V IPC 对象的识别号。如果键是私有的,则键值为0,说明每个进程都可以用键值0建立一个专供其私用的对象。System V IPC对象的引用是通过识别号而不是通过键。

结构kern_ipc_perm是IPC对象的共有属性,每个具体的IPC对象结构将继承此结构。

(2)结构ipc_ids

每个System V IPC 对象有一个ID号,每一类System V IPC 对象(如:信号量对象)的所有ID构成ID集,ID集用结构ipc_ids描述,对象指针与ID通过IDR机制关联起来。IDR机制是一种用radix树存放ID和对象映射,作用类似于以ID为序号的数组,但不受数组尺寸的限制。

结构ipc_ids是进程通信ID集的描述结构,该结构列出如下(在include/linux/ipc_namespace.h中):
struct ipc_ids {
	int in_use;
	unsigned short seq;
	unsigned short seq_max;
	struct rw_semaphore rw_mutex;
	struct idr ipcs_idr;      /*通过IDR机制将ID与结构kern_ipc_perm类型指针建立关联*/
};

(3)结构ipc_namespace

所有System V IPC 对象的ID集存放在结构ipc_namespace类型的全局变量init_ipc_ns中,用户空间的进程进入内核空间后为内核空间的线程,内核空间的线程共享全局变量。因此,不同的进程可以通过全局变量init_ipc_ns查询IPC对象的信息,从而实现进程间通信。

结构ipc_namespace列出如下(在include/linux/ipc_namespace.h中):
struct ipc_namespace {
	struct kref	kref;
	struct ipc_ids	ids[3];   /*分别对应信号量、消息队列和共享内存的ID集*/
 
	int		sem_ctls[4];
	int		used_sems;
 
	int		msg_ctlmax;
	int		msg_ctlmnb;
	int		msg_ctlmni;
	atomic_t	msg_bytes;
	atomic_t	msg_hdrs;
 
	size_t		shm_ctlmax;
	size_t		shm_ctlall;
	int		shm_ctlmni;
	int		shm_tot;
 
	struct notifier_block ipcns_nb;
};

全局变量init_ipc_ns的定义列出如下(在ipc/util.c中):
struct ipc_namespace init_ipc_ns = {

.kref = { .refcount = ATOMIC_INIT(2), },

};

IPC对RCU的支持

进程间通信是进程最常见的操作,进程间通信的效率直接影响程序的执行效率。为了提供同步操作IPC对象的效率,Linux内核使用了自旋锁、读/写信号量、RCU、删除标识和引用计数等机制。

同步机制以数据结构操作为中心,针对不同大小的数据结构操作,使用不同的同步机制。自旋锁用于操作占用内存少、操作快速的小型数据结构,如:结构kern_ipc_perm;读/写信号量用于读操作明显多于写操作的中小型数据结构,如:结构ipc_ids,它还含有一个用于IDR机制简单的radix树;RCU用于操作含有队列或链表、操作时间较长的大型数据结构。如:sem_array,它含有多个链表。删除标识和引用计数用于协调各种同步机制。

(1)RCU前缀对象结构
为了让IPC支持RCU,在IPC对象前面需要加入与RCU操作相关的前缀对象,这样,可最小限度地改动原函数。前缀对象结构有ipc_rcu_hdr、ipc_rcu_grace和ipc_rcu_sched三种,ipc_rcu_hdr在原对象使用期间使用,增加了引用计数成员;ipc_rcu_grace在RCU宽限期间使用,增加了RCU更新请求链表;ipc_rcu_sched仅在使用函数vmalloc时使用,增加了vmalloc所需要的工作函数。这些对象放在原对象前面,与原对象使用同一个内存块,通过函数container_of可分离前缀对象和原对象。三个前缀对象结构分别列出如下(在ipc/util.c中):
struct ipc_rcu_hdr
{
	int refcount;
	int is_vmalloc;
     /*对于信号量对象,它指向信号量集数组sem_array *sma,用于从IPC对象获取本结构*/
	void *data[0]; 
};
 
struct ipc_rcu_grace
{
	struct rcu_head rcu;	
	void *data[0]; /*对于信号量对象,指向struct sem_array *sma,用于从IPC对象获取本结构*/
};
 
struct ipc_rcu_sched
{
	struct work_struct work;	/*工作队列的工作函数,函数vmalloc需要使用工作队列 */
	void *data[0]; /*对于信号量对象,指向struct sem_array *sma,用于从IPC对象获取本结构 */
};

(2)分配IPC对象时加入RCU前缀对象

用户分配IPC对象空间时,调用函数ipc_rcu_alloc分配内存。函数ipc_rcu_alloc封装了内存分配函数,在IPC对象前面加入了RCU前缀对象,并初始化前缀对象。函数的参数size为IPC对象的大小,返回指向前缀对象和IPC对象(称为RCU IPC对象)所在内存块的地址。

函数ipc_rcu_alloc列出如下(在ipc/util.c中):
void* ipc_rcu_alloc(int size)
{
	void* out;	
 
	if (rcu_use_vmalloc(size)) {  /*如果分配尺寸大于1个物理页时,使用分配函数vmalloc*/
		out = vmalloc(HDRLEN_VMALLOC + size);
		if (out) {
			out += HDRLEN_VMALLOC;
             /*利用函数container_of从IPC对象获取前缀对象,并初始化前缀对象的结构成员*/
			container_of(out, struct ipc_rcu_hdr, data)->is_vmalloc = 1;
			container_of(out, struct ipc_rcu_hdr, data)->refcount = 1;
		}
	} else {
		out = kmalloc(HDRLEN_KMALLOC + size, GFP_KERNEL);
		if (out) {
			out += HDRLEN_KMALLOC;
			container_of(out, struct ipc_rcu_hdr, data)->is_vmalloc = 0;
			container_of(out, struct ipc_rcu_hdr, data)->refcount = 1;
		}
	}
 
	return out;  /*返回RCU IPC对象的地址*/
}

IPC前缀对象尺寸计算的宏定义列出如下:
#define HDRLEN_KMALLOC		(sizeof(struct ipc_rcu_grace) > sizeof(struct ipc_rcu_hdr) ? \

sizeof(struct ipc_rcu_grace) : sizeof(struct ipc_rcu_hdr� #define HDRLEN_VMALLOC (sizeof(struct ipc_rcu_sched) > HDRLEN_KMALLOC ? \

sizeof(struct ipc_rcu_sched) : HDRLEN_KMALLOC)

(3)修改IPC对象引起延迟更新

当修改对象时,RCU将通过函数call_rcu进行延迟更新。RCU IPC对象通过引用计数触发延迟更新函数call_rcu的调用。在对象修改前调用函数ipc_rcu_getref增加引用计数,修改后调用函数ipc_rcu_putref将引用计数减1,当引用计数为0时,调用call_rcu进行延迟更新。

函数ipc_rcu_getref列出如下:
void ipc_rcu_getref(void *ptr)
{
	container_of(ptr, struct ipc_rcu_hdr, data)->refcount++;
}

函数ipc_rcu_putref列出如下:
void ipc_rcu_putref(void *ptr)

{ if (--container_of(ptr, struct ipc_rcu_hdr, data)->refcount > 0) return;   if (container_of(ptr, struct ipc_rcu_hdr, data)->is_vmalloc) { call_rcu(&container_of(ptr, struct ipc_rcu_grace, data)->rcu, ipc_schedule_free); } else { call_rcu(&container_of(ptr, struct ipc_rcu_grace, data)->rcu, ipc_immediate_free); }

}

在对IPC对象进行修改时,操作还应加上自旋锁,例如:信号量对象修改的加锁函数sem_lock_and_putref和解锁函数sem_getref_and_unlock分别列出如下(在ipc/sem.c中):
static inline void sem_lock_and_putref(struct sem_array *sma)

{ ipc_lock_by_ptr(&sma->sem_perm); ipc_rcu_putref(sma); }   static inline void sem_getref_and_unlock(struct sem_array *sma) { ipc_rcu_getref(sma); ipc_unlock(&(sma)->sem_perm);

}

ipc通用对象加锁函数ipc_lock_by_ptr和解锁函数ipc_unlock分别列出如下(在ipc/util.h):
static inline void ipc_lock_by_ptr(struct kern_ipc_perm *perm)

{ rcu_read_lock(); spin_lock(&perm->lock); }   static inline void ipc_unlock(struct kern_ipc_perm *perm) { spin_unlock(&perm->lock); rcu_read_unlock();

}

IPC对象查找

进程通信操作指用户空间进程通信的具体操作,如:信号量的加1和减1操作。不同类型的IPC对象,该操作是不同的,实现方法也不同,各类型操作在信号量、共享内存和消息队列中详细介绍。

不同类型进程间通信的操作不一样,但有一些通用的操作,如:从ID查找IPC对象、增加/减少ID等通用操作。下面以信号量为例说明这些通用操作。

信号量操作时,进程在内核空间先通过信号量ID找到对应的信号量对象,然后再信号量对象进行修改操作。查找信号量对象的过程是读操作过程,通过RCU机制可以无阻塞地并发操作,而对信号量对象进行修改操作则需要加自旋锁才能进行。

信号量操作系统调用sys_semtimedop完成信号量的增加或减小操作,与信号量对象查找相关的代码列出如下:
asmlinkage long sys_semtimedop(int semid, struct sembuf __user *tsops,
			unsigned nsops, const struct timespec __user *timeout)
{
……
	sma = sem_lock_check(ns, semid); /*通过semid 查找信号量对象,并加自旋锁*/
	……
	error = try_atomic_semop (sma, sops, nsops, un, task_tgid_vnr(current)); /*对信号量进行加/减操作*/
	……
out_unlock_free:
	sem_unlock(sma); /*操作完成后,解自旋锁*/
	……
}

下面分别说明函数sem_lock_check和sem_unlock。
  • 函数sem_lock_check
函数sem_lock_check通过id查找到IPC对象并加上自旋锁,以便修改对象。再调用函数container_of获取信号量对象。函数sem_lock_check列出如下(在ipc/sem.c中):
static inline struct sem_array *sem_lock_check(struct ipc_namespace *ns,
						int id)
{
	struct kern_ipc_perm *ipcp = ipc_lock_check(&sem_ids(ns), id);
 
	if (IS_ERR(ipcp))
		return (struct sem_array *)ipcp;
 
	return container_of(ipcp, struct sem_array, sem_perm);
}

函数ipc_lock_check在获取IPC对象后,检查对象的id序列号是否正确,其列出如下(在ipc/util.c中):
struct kern_ipc_perm *ipc_lock_check(struct ipc_ids *ids, int id)

{ struct kern_ipc_perm *out;   out = ipc_lock(ids, id); /*通过id查找到结构kern_ipc_perm类型的对象*/ if (IS_ERR(out)) return out;   if (ipc_checkid(out, id)) { /*检查id的序列号是否正确:id / 32768 != out->seq*/ ipc_unlock(out); return ERR_PTR(-EIDRM); }   return out;

}

函数ipc_lock在ids中查找一个id,查找过程加读者锁,找到id获取IPC对象后,锁住对象。该函数在返回时,仍然锁住IPC对象,以便通信操作修改IPC对象。该函数应该在未持有rw_mutex、radix树ids->ipcs_idr未被保护的情况下调用。 函数ipc_lock列出如下(在ipc/util.c中):
struct kern_ipc_perm *ipc_lock(struct ipc_ids *ids, int id)
{
	struct kern_ipc_perm *out;
	int lid = ipcid_to_idx(id);
 
	down_read(&ids->rw_mutex);   /*操作ids用读/写信号量,加读者锁*/
 
	rcu_read_lock();    /*操作radix树ids->ipcs_idr用RCU机制,加RCU读者锁*/
	out = idr_find(&ids->ipcs_idr, lid);  /*从radix树ids->ipcs_idr中找到lid对应的对象指针*/
	if (out == NULL) {  /*如果没找到,解锁后返回错误*/
		rcu_read_unlock();
		up_read(&ids->rw_mutex);
		return ERR_PTR(-EINVAL);
	}
 
	up_read(&ids->rw_mutex); /*ids完成读操作,解读者锁*/
 
	spin_lock(&out->lock);  /*加自旋锁,以便后面的函数修改out*/
 
	/*此时,其他进程的ipc_rmid()可能已经在ipc_lock正自旋时释放了ID,这里检查标识验证out是否还有效*/
	if (out->deleted) { /*out已被删除,释放锁返回错误*/
		spin_unlock(&out->lock);
		rcu_read_unlock();
		return ERR_PTR(-EINVAL);
	}
 
	return out;
}

  • 函数sem_unlock
函数sem_unlock在通信操作修改完成IPC对象后解自旋锁。其列出如下:
#define sem_unlock(sma)		ipc_unlock(&(sma)->sem_perm)
static inline void ipc_unlock(struct kern_ipc_perm *perm)
{
	spin_unlock(&perm->lock);
	rcu_read_unlock();
}

释放IPC命名空间

函数free_ipcsfree_ipcs释放IPC命名空间,IPC命名空间是由结构ipc_namespace表示,是IPC的总入口描述。进程在内核空间通过结构ipc_namespace类型的全局变量找到每一类IPC的ID集结构,再从ID集中找到IPC对象id,由id可找到IPC对象。

释放IPC命名空间操作将释放三类IPC对象,由于IPC对象为多个线程共享,释放操作使用了读/写信号量、RCU等多种同步机制,是应用内核同步机制的典范,因此,对同步机制的应用也进行了分析。

函数free_ipcsfree_ipcs的调用层次图如图1所示,下面以信号量为例按图分析函数的实现,说明内核同步机制的应用。



第4章用户进 06.gif
图1 函数free_ipcsfree_ipcs的调用层次图
函数free_ipcsfree_ipcs列出如下(在ipc/namespace.c中):
void free_ipc_ns(struct kref *kref)
{
	struct ipc_namespace *ns;
 
	ns = container_of(kref, struct ipc_namespace, kref); /*通过kref获取命名空间对象*/
	/*在开始处注销hotplug通知器可以保证在回调例程中不释放IPC命名空间对象。因为通知器含有IPC对象读/写锁,读/写锁释放后,通知器才会释放对象*/	
	unregister_ipcns_notifier(ns);
	sem_exit_ns(ns);   /*释放信号量的IPC对象*/
	msg_exit_ns(ns);   /*释放消息队列的IPC对象*/
	shm_exit_ns(ns);   /*释放共享内存的IPC对象*/
	kfree(ns);
	atomic_dec(&nr_ipc_ns);  /*IPC命名空间对象引用计数减1*/
 
	/*发出通知*/
	ipcns_notify(IPCNS_REMOVED);
}

下面仅说明信号量命名空间的释放。 信号量命令空间用过调用函数sem_exit_ns释放命名空间,其列出如下(在ipc/sem.c中):
void sem_exit_ns(struct ipc_namespace *ns)
{
    /* #define sem_ids(ns)	�ns)->ids[IPC_SEM_IDS]) */
	free_ipcs(ns, &sem_ids(ns), freeary);
}

在一个结构ipc_namespace实例退出时,函数free_ipcs被调用来释放一种IPC类型的所有IPC对象。参数ns为将删除IPC对象的命名空间,参数ids为将释放IPC对象的ID集,参数free为调用来释放指定IPC类型的函数。

函数free_ipcs先加写者锁ids->rw_mutex用于修改ids,接着加自旋锁perm->lock用于释放每个IPC对象。

函数free_ipcs列出如下(在ipc/namespace.c中):
void free_ipcs(struct ipc_namespace *ns, struct ipc_ids *ids,
	       void (*free)(struct ipc_namespace *, struct kern_ipc_perm *))
{
	struct kern_ipc_perm *perm;
	int next_id;
	int total, in_use;
 
	down_write(&ids->rw_mutex);  /*给ids加写者锁*/
 
	in_use = ids->in_use;
 
	for (total = 0, next_id = 0; total < in_use; next_id++) {
        /*通过id从radix树ipcs_idr中查找对应的结构kern_ipc_perm类型指针*/
		perm = idr_find(&ids->ipcs_idr, next_id);  
		if (perm == NULL)
			continue;
          /*执行加锁操作:rcu_read_lock()和spin_lock(&perm->lock)*/
		ipc_lock_by_ptr(perm);
		free(ns, perm);  /*执行释放perm操作,实际上调用函数freeary完成*/
		total++;
	}
	up_write(&ids->rw_mutex); /*解写者锁*/
}

函数freeary释放一个信号量集。在调用此函数时,sem_ids.rw_mutex已作为写者锁锁住,用于操作sem_ids,并且它持有结构kern_ipc_perm的自旋锁,用于操作ipcp。sem_ids.rw_mutex在函数退出时一直保持锁住状态。

由于结构sem_array(使用RCU)包含结构kern_ipc_perm(使用自旋锁),它需要延迟删除,但结构kern_ipc_perm使用自旋锁而无法延迟删除,因此,它使用了删除标识,在删除时,将删除标识设置为1,等待到RCU延迟删除结构sem_array时,RCU再一起删除结构kern_ipc_perm。

函数freeary列出如下(在ipc/sem.c中):
static void freeary(struct ipc_namespace *ns, struct kern_ipc_perm *ipcp)
{
	struct sem_undo *un;
	struct sem_queue *q;
    /*获取信号量集的指针:通过基类对象指针获取子类对象指针*/
	struct sem_array *sma = container_of(ipcp, struct sem_array, sem_perm); 
 
	/* 使此信号量集正存在的结构undo无效。结构undo在exit_sem()或下一个semop期间,如果没有其他操作时,将被释放*/
	for (un = sma->undo; un; un = un->id_next)
		un->semid = -1;
 
	/* 唤醒所有挂起进程,让它们运行失败返回错误EIDRM */
	q = sma->sem_pending;
	while(q) {
		struct sem_queue *n;
		/* lazy remove_from_queue: 正杀死整个队列*/
		q->prev = NULL;
		n = q->next;
		q->status = IN_WAKEUP;
		wake_up_process(q->sleeper); /* 唤醒进程 */
		smp_wmb();
		q->status = -EIDRM;	/* 标识状态为ID被删除状态*/
		q = n;
	}
 
	/* 从IDR中删除信号量集,IDR是ID到指针映射的树 */
	sem_rmid(ns, sma);
	sem_unlock(sma);   /*解锁:ipc_unlock(&(sma)->sem_perm ) */
 
	ns->used_sems -= sma->sem_nsems;
	security_sem_free(sma);  /*安全检查*/
	ipc_rcu_putref(sma);     /*当引用计数为0时,调用函数call_rcu延迟释放sma*/
}
 
static inline void sem_rmid(struct ipc_namespace *ns, struct sem_array *s)
{
	ipc_rmid(&sem_ids(ns), &s->sem_perm);
}

函数ipc_rmid删除IPC ID,它设置了删除标识,相应的信号量集将还保留在内存中,直到RCU宽限期之后才释放。在调用此函数前,sem_ids.rw_mutex作为写者锁锁住,并且它持有信号量集的自旋锁。sem_ids.rw_mutex在退出时一直保持锁住状态。 函数ipc_rmid列出如下:
void ipc_rmid(struct ipc_ids *ids, struct kern_ipc_perm *ipcp)
{
	int lid = ipcid_to_idx(ipcp->id);  /* (id) % 32768 */
	idr_remove(&ids->ipcs_idr, lid);   /*从ids中删除lid*/
	ids->in_use--;                /*使用的id计数减1*/
	ipcp->deleted = 1;      /*将ipcp标识为已删除*/
	return;
}

管道

管道(pipe)是指用于连接读进程和写进程,以实现它们之间通信的共享文件。因而它又称共享文件。向管道(共享文件)提供输入的发送进程(即写进程),以字符流形式将大量的数据送入管道;而接受管道输出的接收进程(即读进程),可从管道中接收数据。由于发送进程和接收进程是利用管道进行通信的,所以将这些共享文件又称为管道。

为了协调双方的通信,管道通信机制必须提供以下三方面的协调能力。

  • 互斥。当一个进程正在对管道进行读写操作时,另一个进程必须等待。
  • 同步。当写(输入)进程把一定数量(如4 KB)数据写入管道后,便去睡眠等待,直到读(输出)进程取走数据后,再把它唤醒。当读进程读到一空管道时,也应睡眠,直到写进程将数据写入管道后,才将它唤醒。
  • 判断对方是否存在。只有确定对方已经存在时,方能进行通信。

管道是一个固定大小的缓冲区,缓冲的大小为1页,即4 KB。管道借用了文件系统的file结构和VFS的索引节点inode。通过将两个file结构指向同一个临时的VFS索引节点,而这个索引节点又指向一个物理页而实现管道。它们定义的文件操作地址是不同的,其中一个是向管道中写入数据的例程地址,而另一个是从管道中读出数据的例程地址。这样,用户程序的系统调用仍然是通常的文件操作,而内核却利用这种抽象机制实现了管道这一特殊操作。

例如: $ ls | grep *.m | lp

这个shell命令是管道的一个应用,ls列当前目录的输出被作为标准输入送到grep程序中,而grep的输出又被作为标准输入送到lp程序中。

Linux支持命名管道(named pipe)。命名管道是一类特殊的FIFO文件,它像普通文件一样有名字,也像普通文件一样访问。它总是按照"先进先出"的原则工作,又称为FIFO管道。FIFO管道不是临时对象,它们是文件系统中的实体并且可以通过mkfifo命令来创建。

管道的实现

管道在内核中是以文件系统的形式实现的一个模块,应用程序通过系统调用sys_pipe建立管道,再通过文件的读写函数进行操作。

下面就其实现代码进行分析,这些代码在(fs/pipe.c中)。函数init_pipe_fs初始化管道模块,装载管道文件系统,列出如下:
static int __init init_pipe_fs(void)
{
    //注册管道文件系统
	int err = register_filesystem(&pipe_fs_type);
	if (!err) {
    //装载文件系统pipe_fs_type
		pipe_mnt = kern_mount(&pipe_fs_type);
		if (IS_ERR(pipe_mnt)) {
			err = PTR_ERR(pipe_mnt);
			unregister_filesystem(&pipe_fs_type);
		}
	}
	return err;
}

系统调用sys_pipe创建一个管道,它得到两个文件描述符:fd[0]代表管道的输入端;fd[1]代表管道的输出端。系统调用sys_pipe调用层次图如图16.1所示,系统调用sys_pipe列出如下(在arch/i386/kernel/sys_i386.c中):
asmlinkage int sys_pipe(unsigned long __user * fildes)
{
	int fd[2];
	int error;
	error = do_pipe(fd);
	if (!error) {
    //将创建的两个文件描述符拷贝到用户空间
		if (copy_to_user(fildes, fd, 2*sizeof(int)))
			error = -EFAULT;
	}
	return error;
}


第4章用户进 05.gif
图16.1 系统调用sys_pipe调用层次图
函数do_pipe 实现了管道的创建工作,函数列出如下(在fs/pipe.c中):
int do_pipe(int *fd)
{
	struct qstr this;
	char name[32];
	struct dentry *dentry;
	struct inode * inode;
	struct file *f1, *f2;
	int error;
	int i,j;
  
	error = -ENFILE;
  //得到管道入口的文件结构
	f1 = get_empty_filp();
	if (!f1)
		goto no_files;
  //得到管道出口的文件结构
	f2 = get_empty_filp();
	if (!f2)
		goto close_f1;
    //创建管道文件节点
	inode = get_pipe_inode();
	if (!inode)
		goto close_f12;
  //得到管道入口的文件描述符
	error = get_unused_fd();
	if (error < 0)
		goto close_f12_inode;
	i = error;
  //得到管道出口的文件描述符
	error = get_unused_fd();
	if (error < 0)
		goto close_f12_inode_i;
	j = error;
 
	error = -ENOMEM;
  //以节点号为文件名
	sprintf(name, "[%lu]", inode->i_ino);
	this.name = name;
	this.len = strlen(name);
	this.hash = inode->i_ino; /* will go */
  //分配dentry
	dentry = d_alloc(pipe_mnt->mnt_sb->s_root, &this);
	if (!dentry)
		goto close_f12_inode_i_j;
  //对于管道来说只允许pipefs_delete_dentry()操作
	dentry->d_op = &pipefs_dentry_operations;
	d_add(dentry, inode);
	f1->f_vfsmnt = f2->f_vfsmnt = mntget(mntget(pipe_mnt));
	f1->f_dentry = f2->f_dentry = dget(dentry);
	f1->f_mapping = f2->f_mapping = inode->i_mapping;
 
	//管道出口只许读操作
	f1->f_pos = f2->f_pos = 0;
	f1->f_flags = O_RDONLY;
	f1->f_op = &read_pipe_fops;
	f1->f_mode = FMODE_READ;
	f1->f_version = 0;
 
	//管道入口只许写操作
	f2->f_flags = O_WRONLY;
	f2->f_op = &write_pipe_fops;
	f2->f_mode = FMODE_WRITE;
	f2->f_version = 0;
  //安装file指针f1到fd数组中去
	fd_install(i, f1);
	fd_install(j, f2);
	fd[0] = i;
	fd[1] = j;
	return 0;
……	
}

函数get_pipe_inode创建一个特殊的节点,它在内存中分配一页缓冲区当做文件,操作管道文件实际上就是操作一个缓冲区。函数get_pipe_inode列出如下:
static struct inode * get_pipe_inode(void)

{   //新建节点inode struct inode *inode = new_inode(pipe_mnt->mnt_sb);   if (!inode) goto fail_inode;   if(!pipe_new(inode)) goto fail_iput; PIPE_READERS(*inode) = PIPE_WRITERS(*inode) = 1; inode->i_fop = &rdwr_pipe_fops;     //初始化节点 inode->i_state = I_DIRTY;  //标志节点dirty inode->i_mode = S_IFIFO | S_IRUSR | S_IWUSR; inode->i_uid = current->fsuid; inode->i_gid = current->fsgid; inode->i_atime = inode->i_mtime = inode->i_ctime = CURRENT_TIME; inode->i_blksize = PAGE_SIZE; return inode;   fail_iput: iput(inode); fail_inode: return NULL;

}

函数pipe_new创建一个管道的信息结构,以及分配一页缓冲区,当做管道,这个函数列出如下:
struct inode* pipe_new(struct inode* inode)

{ unsigned long page;

   //分配内存页面作为管道的缓冲区

page = __get_free_page(GFP_USER);if (!page)return NULL;  //分配管道的信息结构对象inode->i_pipe = kmalloc(sizeof(struct pipe_inode_info), GFP_KERNEL);if (!inode->i_pipe)goto fail_page;   //初始化节点init_waitqueue_head(PIPE_WAIT(*inode));   PIPE_BASE(*inode) = (char*) page;PIPE_START(*inode) = PIPE_LEN(*inode) = 0;PIPE_READERS(*inode) = PIPE_WRITERS(*inode) = 0;PIPE_WAITING_WRITERS(*inode) = 0;PIPE_RCOUNTER(*inode) = PIPE_WCOUNTER(*inode) = 1;*PIPE_FASYNC_READERS(*inode) = *PIPE_FASYNC_WRITERS(*inode) = NULL; return inode;fail_page:free_page(page);return NULL;

}

结构实例read_pipe_fops是管道入口的操作函数,列出如下:
struct file_operations read_pipe_fops = {

.llseek = no_llseek,  //空操作 .read = pipe_read, .readv = pipe_readv, .write = bad_pipe_w,  //空操作 .poll = pipe_poll, .ioctl = pipe_ioctl, .open = pipe_read_open, .release = pipe_read_release, .fasync = pipe_read_fasync,

};

结构实例write_pipe_fops是管道出口的操作函数,列出如下:
struct file_operations write_pipe_fops = {

.llseek = no_llseek,   //空操作 .read = bad_pipe_r,   //空操作 .write = pipe_write, .writev = pipe_writev, .poll = pipe_poll, .ioctl = pipe_ioctl, .open = pipe_write_open, .release = pipe_write_release, .fasync = pipe_write_fasync,

};

读函数pipe_read与写函数pipe_write是典型的在环形缓冲区上的读者-写者问题的解决方法。对读者进程而言,缓冲区中有数据就读取,然后唤醒可能正在等待着的写者。如果没有数据可读,就进入睡眠。对写者而言,只要缓冲区有空间,就往里写,并唤醒可能正在等待的读者;如果没有空间,就睡眠。 下面对这两个函数进行分析:
static ssize_t
pipe_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos)
{
	struct iovec iov = { .iov_base = buf, .iov_len = count };
	return pipe_readv(filp, &iov, 1, ppos);
}
 
static ssize_t
pipe_readv(struct file *filp, const struct iovec *_iov,
	   unsigned long nr_segs, loff_t *ppos)
{
	struct inode *inode = filp->f_dentry->d_inode;
	int do_wakeup;
	ssize_t ret;
	struct iovec *iov = (struct iovec *)_iov;
	size_t total_len;
 
	total_len = iov_length(iov, nr_segs);
	// Null表读成功
	if (unlikely(total_len == 0))
		return 0;
 
	do_wakeup = 0;
	ret = 0;
	down(PIPE_SEM(*inode));
	for (;;) {
		int size = PIPE_LEN(*inode);
		if (size) {
      //字符开始位置
			char *pipebuf = PIPE_BASE(*inode) + PIPE_START(*inode);
      //字符数,即PAGE_SIZE- PIPE_START(*inode)
			ssize_t chars = PIPE_MAX_RCHUNK(*inode);
 
			if (chars > total_len)
				chars = total_len;
			if (chars > size)
				chars = size;
      //从pipebuf拷贝读出到iov用户空间中
			if (pipe_iov_copy_to_user(iov, pipebuf, chars)) {
				if (!ret) ret = -EFAULT;
				break;
			}
			ret += chars;
      //计算拷贝的下一块字符数
			PIPE_START(*inode) += chars;
			PIPE_START(*inode) &= (PIPE_SIZE - 1);
			PIPE_LEN(*inode) -= chars;
			total_len -= chars;
			do_wakeup = 1;
			if (!total_len)
				break;	/* common path: read succeeded */
		}
		if (PIPE_LEN(*inode)) /* test for cyclic buffers */
			continue;
		if (!PIPE_WRITERS(*inode)) //如果有写者,则跳出
			break;
		if (!PIPE_WAITING_WRITERS(*inode)) {如果有等待的写者
      //如果设置O_NONBLOCK或者得到一些数据,就不能进入睡眠。
      //但如果一个写者在内核空间睡眠了,就能等待数据。
			if (ret)
				break;
			if (filp->f_flags & O_NONBLOCK) {
				ret = -EAGAIN;
				break;
			}
		}
		if (signal_pending(current)) { //挂起当前进程
			if (!ret) ret = -ERESTARTSYS;
			break;
		}
		if (do_wakeup) {
      //唤醒等待的进程
			wake_up_interruptible_sync(PIPE_WAIT(*inode));
 			kill_fasync(PIPE_FASYNC_WRITERS(*inode), SIGIO, POLL_OUT);
		}
    //调度等待队列
		pipe_wait(inode);
	}
	up(PIPE_SEM(*inode));
	//发信号给异步写者没有更多的空间 
	if (do_wakeup) {//唤醒等待进程
		wake_up_interruptible(PIPE_WAIT(*inode));
		kill_fasync(PIPE_FASYNC_WRITERS(*inode), SIGIO, POLL_OUT);
	}
	if (ret > 0)
		file_accessed(filp);//访问时间更新
	return ret;
}
 
static ssize_t
pipe_write(struct file *filp, const char __user *buf,
	   size_t count, loff_t *ppos)
{
  //用户空间缓冲区
	struct iovec iov = { .iov_base = (void __user *)buf, .iov_len = count };
	return pipe_writev(filp, &iov, 1, ppos);
}
 
static ssize_t
pipe_writev(struct file *filp, const struct iovec *_iov,
	    unsigned long nr_segs, loff_t *ppos)
{
	struct inode *inode = filp->f_dentry->d_inode;
	ssize_t ret;
	size_t min;
	int do_wakeup;
	struct iovec *iov = (struct iovec *)_iov;
	size_t total_len;
 
	total_len = iov_length(iov, nr_segs);
	// Null表示写成功
	if (unlikely(total_len == 0))
		return 0;
 
	do_wakeup = 0;
	ret = 0;
	min = total_len;
	if (min > PIPE_BUF)
		min = 1;
	down(PIPE_SEM(*inode));
	for (;;) {
		int free;
		if (!PIPE_READERS(*inode)) { //读者不为0
      //给当前进程发SIGPIPE信号,信号的数据为0
			send_sig(SIGPIPE, current, 0);
			if (!ret) ret = -EPIPE;
			break;
		}
		free = PIPE_FREE(*inode); //得到空闲空间大小
		if (free >= min) {
			//向环形缓冲区写数据
			ssize_t chars = PIPE_MAX_WCHUNK(*inode);
			char *pipebuf = PIPE_BASE(*inode) + PIPE_END(*inode);
			//总是唤醒,即使是拷贝失败,我们锁住由于系统调用造成睡眠的读者 
			do_wakeup = 1;
			if (chars > total_len)
				chars = total_len;
			if (chars > free)
				chars = free;
      //从用户空间的iov中拷贝数据到pipebuf中。
			if (pipe_iov_copy_from_user(pipebuf, iov, chars)) {
				if (!ret) ret = -EFAULT;
				break;
			}
			ret += chars;
 
			PIPE_LEN(*inode) += chars;
			total_len -= chars;
			if (!total_len)
				break;
		}
		if (PIPE_FREE(*inode) && ret) {
			//处理环形缓冲区
			min = 1;
			continue;
		}
		if (filp->f_flags & O_NONBLOCK) {
			if (!ret) ret = -EAGAIN;
			break;
		}
		if (signal_pending(current)) {//挂起当前进程
			if (!ret) ret = -ERESTARTSYS;
			break;
		}
		if (do_wakeup) {//唤醒等待的进程
			wake_up_interruptible_sync(PIPE_WAIT(*inode));
			kill_fasync(PIPE_FASYNC_READERS(*inode), SIGIO, POLL_IN);
			do_wakeup = 0;
		}
		PIPE_WAITING_WRITERS(*inode)++; //等待的写者增加
		pipe_wait(inode); //调度等待队列
		PIPE_WAITING_WRITERS(*inode)--;
	}
	up(PIPE_SEM(*inode));
	if (do_wakeup) {//唤醒等待的进程
		wake_up_interruptible(PIPE_WAIT(*inode));
		kill_fasync(PIPE_FASYNC_READERS(*inode), SIGIO, POLL_IN);
	}
  //节点时间更新
	if (ret > 0)
		inode_update_time(inode, 1);	/* mtime and ctime */
	return ret;
}

函数pipe_wait原子操作地释放信号量,并等待一次管道事件。它将进程中断,通过调度来运行等待队列中的进程,然后清除等待队列。函数列出如下:
void pipe_wait(struct inode * inode)

{ DEFINE_WAIT(wait);   //将inode等待队列加入到wait上,并设置进程状态TASK_INTERRUPTIBLE prepare_to_wait(PIPE_WAIT(*inode), &wait, TASK_INTERRUPTIBLE); up(PIPE_SEM(*inode)); schedule();//进程调度   //设置当前进程为运行状态,并清除wait队列 finish_wait(PIPE_WAIT(*inode), &wait); down(PIPE_SEM(*inode));

}

消息队列

消息队列就是一个消息的链表。具有权限的一个或者多个进程进程可对消息队列进行读写。

消息队列分别有POSIX和System V的消息队列系统调用,其中属于POSIX的系统调用有sys_mq_open,sys_mq_unlink,sys_mq_timedsend,sys_mq_timedreceive,sys_mq_notify,sys_mq_getsetattr,属于System V的消息队列系统调用有sys_msgget,sys_msgsnd,sys_msgrcv,sys_msgctl。

POSIX消息队列是利用消息队列文件系统来实现,一个文件代表一个消息队列。利用文件节点的结构扩展进消息队列信息结构来容纳消息内容。

System V的消息队列实现是在内核内存中建立消息队列的结构缓存区,通过自定义的消息队列ID,在全局变量static struct ipc_ids msg_ids中定位找到消息队列的结构缓存区,并最终找到消息。全局数据结构struct ipc_ids msg_ids可以访问到每个消息队列头的第一个成员:struct kern_ipc_perm;而每个struct kern_ipc_perm能够与具体的消息队列对应起来,是因为在该结构中,有一个key_t类型成员key,而key则惟一确定一个消息队列。System V消息队列数据结构之间的关系如图5所示。

第4章用户进 08.png

图5 System V消息队列数据结构之间的关系

由于System V的消息队列的实现与其他的System V通信机制类似,因而,这里只分析POSIX消息队列。

消息队列结构

系统中每个消息队列用一个msq_queue结构描述,列出如下(在include/linux/msg.h中):
struct msg_queue {
	struct kern_ipc_perm q_perm;
	time_t q_stime;			//上次消息发送的时间 
	time_t q_rtime;			//上次消息接收的时间
	time_t q_ctime;			//上次发生变化的时间
	unsigned long q_cbytes;	//队列上当前的字节数
	unsigned long q_qnum;		//队列里的消息数
	unsigned long q_qbytes;	//队列上的最大字节数
	pid_t q_lspid;			//上次发送消息进程的pid 
	pid_t q_lrpid;			//上次接收消息进程的pid
 
	struct list_head q_messages; //消息队列
	struct list_head q_receivers; //消息接收进程链表
	struct list_head q_senders;  //消息发送进程链表
};

每个消息用一个msg_msg结构描述,结构列出如下:
struct msg_msg {

struct list_head m_list; long m_type; //消息类型 int m_ts; //消息的文本大小 struct msg_msgseg* next;//下一条消息

};

每个正在睡眠的接收者用一个msg_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;//消息

};

每个正在睡眠的发送者用一个msg_sender结构描述,结构列出如下:
struct msg_sender {

struct list_head list; struct task_struct* tsk; //发送消息的进程

};

消息队列文件系统

POSIX消息队列是用特殊的消息队列文件系统来与用户空间进行接口的。下面分析消息队列文件系统(在ipc/mqueue.c中)。

函数init_mqueue_fs初始化消息队列文件系统。它注册消息队列文件系统结构,并挂接到系统中。函数init_mqueue_fs分析如下:
static struct inode_operations mqueue_dir_inode_operations;
static struct file_operations mqueue_file_operations;
static struct super_operations mqueue_super_ops;
static kmem_cache_t *mqueue_inode_cachep;
static struct ctl_table_header * mq_sysctl_table;
static int __init init_mqueue_fs(void)
{
	int error;
  //分配结构对象cache缓冲区,结构对象可从缓冲区中分配
	mqueue_inode_cachep = kmem_cache_create("mqueue_inode_cache",
				sizeof(struct mqueue_inode_info), 0,
				SLAB_HWCACHE_ALIGN, init_once, NULL);
	if (mqueue_inode_cachep == NULL)
		return -ENOMEM;
  // 注册到sysctl表,即加mq_sysctl_root 到sysctl表尾
	mq_sysctl_table = register_sysctl_table(mq_sysctl_root, 0);
	if (!mq_sysctl_table) {
		error = -ENOMEM;
		goto out_cache;
	}
  //注册文件系统mqueue_fs_type
	error = register_filesystem(&mqueue_fs_type);
	if (error)
		goto out_sysctl;
  //装载文件系统
	if (IS_ERR(mqueue_mnt = kern_mount(&mqueue_fs_type))) {
		error = PTR_ERR(mqueue_mnt);
		goto out_filesystem;
	}
 
	//内部初始化,不是一般vfs所需要的 
	queues_count = 0;
	spin_lock_init(&mq_lock);
 
	return 0;
……
}
 
static struct file_system_type mqueue_fs_type = {
	.name = "mqueue",
	.get_sb = mqueue_get_sb,
	.kill_sb = kill_litter_super,
};

下面分析mqueue_fs_type中的得到超级块操作函数mqueue_get_sb:
static struct super_block *mqueue_get_sb(struct file_system_type *fs_type,

int flags, const char *dev_name, void *data) { //创建超级块并用函数mqueue_fill_super填充,再装载文件系统 return get_sb_single(fs_type, flags, data, mqueue_fill_super);

}

函数mqueue_fill_super用来初始化超级块,分配节点及根目录,加上超级块操作函数,函数mqueue_fill_super列出如下:
static int mqueue_fill_super(struct super_block *sb, void *data, int silent)

{ struct inode *inode;   sb->s_blocksize = PAGE_CACHE_SIZE; sb->s_blocksize_bits = PAGE_CACHE_SHIFT; sb->s_magic = MQUEUE_MAGIC; sb->s_op = &mqueue_super_ops; //超级块操作函数集实例   //创建节点 inode = mqueue_get_inode(sb, S_IFDIR | S_ISVTX | S_IRWXUGO, NULL); if (!inode) return -ENOMEM;

   //分配根目录的dentry

sb->s_root = d_alloc_root(inode);if (!sb->s_root) {iput(inode);return -ENOMEM;} return 0;

}

函数mqueue_get_inode创建节点并初始化。函数分析如下:
static struct inode *mqueue_get_inode(struct super_block *sb, int mode,

struct mq_attr *attr) { struct inode *inode;   //创建节点结构,分配新节点号,加入到节点链表 inode = new_inode(sb); if (inode) {     //填充时间及从当前进程继承来的uid等, inode->i_mode = mode; inode->i_uid = current->fsuid; inode->i_gid = current->fsgid; inode->i_blksize = PAGE_CACHE_SIZE; inode->i_blocks = 0; inode->i_mtime = inode->i_ctime = inode->i_atime = CURRENT_TIME; if (S_ISREG(mode)) {//是内存模式 struct mqueue_inode_info *info; struct task_struct *p = current; struct user_struct *u = p->user; //每uid的用户信息结构 unsigned long mq_bytes, mq_msg_tblsz;   inode->i_fop = &mqueue_file_operations; //文件操作函数 inode->i_size = FILENT_SIZE;  //80字节大小 //消息队列的特定信息       //得到包含有节点inode成员的mqueue_inode_info结构 info = MQUEUE_I(inode); spin_lock_init(&info->lock);       //初始化等待队列 init_waitqueue_head(&info->wait_q); INIT_LIST_HEAD(&info->e_wait_q[0].list); INIT_LIST_HEAD(&info->e_wait_q[1].list);       //初始化mqueue_inode_info结构 info->messages = NULL; info->notify_owner = 0; info->qsize = 0; info->user = NULL; /* set when all is ok */ memset(&info->attr, 0, sizeof(info->attr)); info->attr.mq_maxmsg = DFLT_MSGMAX; info->attr.mq_msgsize = DFLT_MSGSIZEMAX; if (attr) { info->attr.mq_maxmsg = attr->mq_maxmsg; info->attr.mq_msgsize = attr->mq_msgsize; }

          //计算队列消息表的大小,即有多少条消息

mq_msg_tblsz = info->attr.mq_maxmsg * sizeof(struct msg_msg *);     //计算整个队列消息的字节数mq_bytes = (mq_msg_tblsz +(info->attr.mq_maxmsg * info->attr.mq_msgsize)); spin_lock(&mq_lock);      //检查最大消息字节数是否超过限制if (u->mq_bytes + mq_bytes < u->mq_bytes || u->mq_bytes + mq_bytes > p->rlim[RLIMIT_MSGQUEUE].rlim_cur) {spin_unlock(&mq_lock);goto out_inode;}      //用户结构user_struct中能分配给队列的字节数计算u->mq_bytes += mq_bytes;spin_unlock(&mq_lock);      //分配消息表空间info->messages = kmalloc(mq_msg_tblsz, GFP_KERNEL);if (!info->messages) {//分配不成功就进行清除处理spin_lock(&mq_lock);u->mq_bytes -= mq_bytes;spin_unlock(&mq_lock);goto out_inode;}/* all is ok */info->user = get_uid(u); } else if (S_ISDIR(mode)) { //是目录inode->i_nlink++;//赋上节点操作函数 inode->i_size = 2 * DIRENT_SIZE;inode->i_op = &mqueue_dir_inode_operations;inode->i_fop = &simple_dir_operations;}}return inode;out_inode:make_bad_inode(inode);iput(inode);return NULL;

}

下面是消息队列文件系统的一些特殊结构,结构mqueue_inode_info记录了节点的特殊信息,通过其成员vfs_inode可以找到对应的mqueue_inode_info结构。这个结构列出如下:
struct mqueue_inode_info {

spinlock_t lock; struct inode vfs_inode; //文件系统节点 wait_queue_head_t wait_q; struct msg_msg **messages; //消息结构数组指针 struct mq_attr attr;  //消息队列属性   struct sigevent notify; //信号事件 pid_t notify_owner; //给信号的进程pid

	struct user_struct *user;	//创建消息的用户结构

struct sock *notify_sock;struct sk_buff *notify_cookie; struct ext_wait_queue e_wait_q[2]; //分别等待释放空间和消息的进程 unsigned long qsize; //内存中队列的大小,它是所有消息的总和

};

结构mq_attr记录了消息队列的属性,列出如下(在include/linux/mqueue.h中):
struct mq_attr {

long mq_flags; //消息队列标志 long mq_maxmsg; //最大消息数 long mq_msgsize; //最大消息尺寸 long mq_curmsgs; //当前排队的消息数 */ long __reserved[4]; //保留,为0

};

在下面两个结构中,mqueue_dir_inode_operations是目录节点操作函数,mqueue_file_operations是文件节点操作函数。
static struct inode_operations mqueue_dir_inode_operations = {

.lookup = simple_lookup, //目录查找函数, .create = mqueue_create, //创建消息队列,见下一节中分析。 .unlink = mqueue_unlink, };   static struct file_operations mqueue_file_operations = { .flush = mqueue_flush_file, .poll = mqueue_poll_file, .read = mqueue_read_file,

};

消息队列系统调用函数

函数sys_mq_open打开一个消息队列,创建一个消息队列或从文件系统中找到消息队列名对应的文件的file结构。函数分析如下:
asmlinkage long sys_mq_open(const char __user *u_name, int oflag, mode_t mode,
				struct mq_attr __user *u_attr)
{
	struct dentry *dentry;
	struct file *filp;
	char *name;
	int fd, error;
 
	if (IS_ERR(name = getname(u_name)))
		return PTR_ERR(name);
  //得到未用的文件描述符
	fd = get_unused_fd();
	if (fd < 0)
		goto out_putname;
 
	down(&mqueue_mnt->mnt_root->d_inode->i_sem);
  //以name为关键字用hash算法找到对应的dentry
	dentry = lookup_one_len(name, mqueue_mnt->mnt_root, strlen(name));
	if (IS_ERR(dentry)) {
		error = PTR_ERR(dentry);
		goto out_err;
	}
	mntget(mqueue_mnt);
 
	if (oflag & O_CREAT) {
		if (dentry->d_inode) {	//entry已存在
			filp = (oflag & O_EXCL) ? ERR_PTR(-EEXIST) :
					do_open(dentry, oflag);//打开dentry
		} else {//创建dentry,即创建新的队列
			filp = do_create(mqueue_mnt->mnt_root, dentry,
						oflag, mode, u_attr);
		}
	} else  //得到消息队列名对应的文件file结构
		filp = (dentry->d_inode) ? do_open(dentry, oflag) :
					ERR_PTR(-ENOENT);
 
	dput(dentry);
 
	if (IS_ERR(filp)) {
		error = PTR_ERR(filp);
		goto out_putfd;
	}
 
	set_close_on_exec(fd, 1); //设置files->close_on_exec中的fd
	fd_install(fd, filp); //安装文件指针到fd数组里,即current->files->fd[fd] = filp
	goto out_upsem;
……
}

函数do_create创建一个新的队列,它调用节点的操作函数create来完成创建队列工作,即调用对应为函数mqueue_create。函数do_create分析如下:
static struct file *do_create(struct dentry *dir, struct dentry *dentry,

int oflag, mode_t mode, struct mq_attr __user *u_attr) { struct file *filp; struct mq_attr attr; int ret;   if (u_attr != NULL) { if (copy_from_user(&attr, u_attr, sizeof(attr))) return ERR_PTR(-EFAULT); if (!mq_attr_ok(&attr)) return ERR_PTR(-EINVAL); //存起来以便在创建期间使用 dentry->d_fsdata = &attr; }

   //调用dir的节点操作函数create创建队列的节点,即函数mqueue_create

ret = vfs_create(dir->d_inode, dentry, mode, NULL);dentry->d_fsdata = NULL;if (ret)return ERR_PTR(ret);  //打开dentry得到文件结构filp,filp = dentry_open(dentry, mqueue_mnt, oflag);if (!IS_ERR(filp))dget(dentry); // dentry->d_count加1 return filp;

}

函数mqueue_create完成创建消息队列的具体工作,函数列出如下:
static int mqueue_create(struct inode *dir, struct dentry *dentry,

int mode, struct nameidata *nd) { struct inode *inode; struct mq_attr *attr = dentry->d_fsdata; int error;   spin_lock(&mq_lock); if (queues_count >= queues_max && !capable(CAP_SYS_RESOURCE)) { error = -ENOSPC; goto out_lock; } queues_count++; //消息队列计数 spin_unlock(&mq_lock);   //创建节点,填充消息队列的信息结构 inode = mqueue_get_inode(dir->i_sb, mode, attr); if (!inode) { error = -ENOMEM; spin_lock(&mq_lock); queues_count--; goto out_lock; }   dir->i_size += DIRENT_SIZE;   //dir的时间更新 dir->i_ctime = dir->i_mtime = dir->i_atime = CURRENT_TIME;   //将节点加入到dentry中 d_instantiate(dentry, inode); dget(dentry);  // dentry->d_count加1 return 0; out_lock: spin_unlock(&mq_lock); return error;

}

系统调用sys_mq_timedsend是进程向消息队列发送消息的操作函数。函数分析如下:
asmlinkage long sys_mq_timedsend(mqd_t mqdes, const char __user *u_msg_ptr,

size_t msg_len, unsigned int msg_prio, const struct timespec __user *u_abs_timeout) { struct file *filp; struct inode *inode; struct ext_wait_queue wait; struct ext_wait_queue *receiver; struct msg_msg *msg_ptr; struct mqueue_inode_info *info; long timeout; int ret;

   //

if (unlikely(msg_prio >= (unsigned long) MQ_PRIO_MAX))return -EINVAL;

   //将用户空间的定时值拷贝到内核并转换成内核的时间jiffies

timeout = prepare_timeout(u_abs_timeout); ret = -EBADF;filp = fget(mqdes); //由文件描述符得到文件结构if (unlikely(!filp))goto out; inode = filp->f_dentry->d_inode;//得到节点…… //分配空间并将用户空间的消息拷贝到msg_ptr = msgmsg结构+消息内容msg_ptr = load_msg(u_msg_ptr, msg_len);if (IS_ERR(msg_ptr)) {ret = PTR_ERR(msg_ptr);goto out_fput;}msg_ptr->m_ts = msg_len;msg_ptr->m_type = msg_prio; spin_lock(&info->lock);  //当前消息数达到最大,则阻塞睡眠一段时间后再调度进程发送消息if (info->attr.mq_curmsgs == info->attr.mq_maxmsg) {if (filp->f_flags & O_NONBLOCK) {//如果为不需要阻塞,则返回spin_unlock(&info->lock);ret = -EAGAIN;} else if (unlikely(timeout < 0)) {//超时spin_unlock(&info->lock);ret = timeout;} else {//阻塞一段时间再发送wait.task = current;wait.msg = (void *) msg_ptr;wait.state = STATE_NONE;

          //加wait到info->e_wait_q[SEND]队列中优先级小于它的元素前面,

      //调用schedule_timeout函数进行定时调度,即睡眠一段时间再调度ret = wq_sleep(info, SEND, timeout, &wait);}if (ret < 0)free_msg(msg_ptr); //释放对象空间} else {//发送消息    //从接收等待队列中得到第一个等待的ext_wait_queue结构receiver = wq_get_first_waiter(info, RECV);if (receiver) {//如果有等待的接收者,消息给接收者,并唤醒接收者进程pipelined_send(info, msg_ptr, receiver);} else {//如果没有等待的接收者,加消息到info的消息数组未尾msg_insert(msg_ptr, info);__do_notify(info); //信号处理及唤醒info中的等待队列}    //节点时间更新inode->i_atime = inode->i_mtime = inode->i_ctime =CURRENT_TIME;spin_unlock(&info->lock);ret = 0;}out_fput:fput(filp);out:return ret;

}

函数load_msg将用户空间的消息拷贝到内核空间并存入消息结构中,函数分析如下:
struct msg_msg *load_msg(const void __user *src, int len)

{ struct msg_msg *msg; struct msg_msgseg **pseg; int err; int alen;   alen = len; if (alen > DATALEN_MSG) alen = DATALEN_MSG;   //分配消息结构空间及消息内容空间 msg = (struct msg_msg *)kmalloc(sizeof(*msg) + alen, GFP_KERNEL); if (msg == NULL) return ERR_PTR(-ENOMEM);   msg->next = NULL; msg->security = NULL;   //从用户空间拷贝消息内容到内核的msg空间里的msgmsg结构后面内容区 if (copy_from_user(msg + 1, src, alen)) { err = -EFAULT; goto out_err; }   len -= alen; src = ((char __user *)src) + alen; pseg = &msg->next;     //将超过DATALEN_MSG的消息,分存几个消息结构中 while (len > 0) { struct msg_msgseg *seg; alen = len; if (alen > DATALEN_SEG) alen = DATALEN_SEG; seg = (struct msg_msgseg *)kmalloc(sizeof(*seg) + alen, GFP_KERNEL); if (seg == NULL) { err = -ENOMEM; goto out_err; } *pseg = seg; seg->next = NULL; if (copy_from_user(seg + 1, src, alen)) { err = -EFAULT; goto out_err; } pseg = &seg->next; len -= alen; src = ((char __user *)src) + alen; }   err = security_msg_msg_alloc(msg); if (err) goto out_err;   return msg;   out_err: free_msg(msg); return ERR_PTR(err);

}

像管道线似的发送与接收函数的处理逻辑说明如下:

如果接收者没发现等待消息,它就把自己注册进等待接收者的链表里。发送者在向消息数组加新消息之前检查链表。如果有一个等待的接收者,它就忽略消息数组,并且直接处理在接收者上的消息。

接收者在没有抢夺队列自旋锁的情况下接受消息并返回。因此,一个中间的STATE_PENDING状态和内存屏障是必需的。同样的算法用到了System V的信号量上,见ipc/sem.c。

同样的算法也用在发送者上。

函数pipelined_send直接发送一个消息给等待在sys_mq_timedreceive()里的任务,而没有把消息插入到队列中。
static inline void pipelined_send(struct mqueue_inode_info *info,
				  struct msg_msg *message,
				  struct ext_wait_queue *receiver)
{
	receiver->msg = message;  //接收者得到消息
	list_del(&receiver->list); //清除接收者链表
	receiver->state = STATE_PENDING; //设置状态为挂起接收者
	wake_up_process(receiver->task); //唤醒接收者进程
	wmb();   //内存屏障
	receiver->state = STATE_READY; //设置接收者状态为准备好状态
}

系统调用sys_mq_timedreceive被消息接收进程用来定时接收消息。其列出如下:
asmlinkage ssize_t sys_mq_timedreceive(mqd_t mqdes, char __user *u_msg_ptr,

size_t msg_len, unsigned int __user *u_msg_prio, const struct timespec __user *u_abs_timeout) { long timeout; ssize_t ret; struct msg_msg *msg_ptr; struct file *filp; struct inode *inode; struct mqueue_inode_info *info; struct ext_wait_queue wait;   //将用户空间的定时值拷贝到内核并转换成内核的时间jiffies timeout = prepare_timeout(u_abs_timeout);   ret = -EBADF; filp = fget(mqdes);//从文件描述符中得到文件file结构 if (unlikely(!filp)) goto out;   //得到节点 inode = filp->f_dentry->d_inode; if (unlikely(filp->f_op != &mqueue_file_operations)) goto out_fput; info = MQUEUE_I(inode); //从节点得到消息队列信息结构    if (unlikely(!(filp->f_mode & FMODE_READ))) goto out_fput;   //检查buffer是否足够大 if (unlikely(msg_len < info->attr.mq_msgsize)) { ret = -EMSGSIZE; goto out_fput; }   spin_lock(&info->lock); if (info->attr.mq_curmsgs == 0) { //如果当前没有消息可接收,阻塞进程 if (filp->f_flags & O_NONBLOCK) {//如果不允许阻塞,返回 spin_unlock(&info->lock); ret = -EAGAIN; msg_ptr = NULL; } else if (unlikely(timeout < 0)) {//超时 spin_unlock(&info->lock); ret = timeout; msg_ptr = NULL; } else {//阻塞睡眠进程 wait.task = current; wait.state = STATE_NONE;       //加wait到info->e_wait_q[RECV]队列中优先级小于它的元素前面,       //调用schedule_timeout函数进行定时调度,即睡眠一段时间再调度 ret = wq_sleep(info, RECV, timeout, &wait); msg_ptr = wait.msg; } } else {//接收消息 msg_ptr = msg_get(info);//从info中得到msgmsg结构     //更新节点当前时间 inode->i_atime = inode->i_mtime = inode->i_ctime = CURRENT_TIME;   //接收消息,这样消息队列里就有空间了  pipelined_receive(info); spin_unlock(&info->lock); ret = 0; } if (ret == 0) { ret = msg_ptr->m_ts;   if ((u_msg_prio && put_user(msg_ptr->m_type, u_msg_prio)) ||      //将消息msg_ptr拷贝存入用户空间的u_msg_ptr中 store_msg(u_msg_ptr, msg_ptr, msg_ptr->m_ts)) { ret = -EFAULT; } free_msg(msg_ptr); } out_fput: fput(filp); out: return ret;

}

函数pipelined_receive完成消息接收工作。如果有进程正等待在sys_mq_timedsend()上发送消息,就得到它的消息,并放到队列中,必须确信队列中有空闲空间。
static inline void pipelined_receive(struct mqueue_inode_info *info)

{   //得到等待发送者 struct ext_wait_queue *sender = wq_get_first_waiter(info, SEND);   if (!sender) {//如果没有发送者,唤醒info中的等待队列中 /* for poll */ wake_up_interruptible(&info->wait_q); return; } msg_insert(sender->msg, info);//将发送者消息插入到info中 list_del(&sender->list);//删除发送者链表 sender->state = STATE_PENDING;//发送者状态为挂起 wake_up_process(sender->task);//唤醒发送者进程 wmb();//内存屏障 sender->state = STATE_READY; //发送者状态为准备

}

共享内存

进程A,B共享内存是指同一块物理内存被映射到进程A,B各自的进程地址空间。进程A可以即时地看到进程B对共享内存中数据的更新,反之亦然。

共享内存方式有mmap()系统调用、Posix共享内存,以及系统V共享内存。其中mmap()系统调用是通过把普通文件在不同进程中打开并映射到内存后,在不同进程间可访问这个映射,最终达到共享内存的目的。Posix共享内存在Linux2.6中还没实现。系统V共享内存是在内存文件系统-tmpfs文件系统中建立文件,然后把文件映射到不同进程空间达到共享内存的作用。

每个新创建的共享内存区域由一个shmid_ds数据结构来表示。它们被保存在shm_segs数组中。shmid_ds数据结构描述共享内存的大小,进程如何使用,以及共享内存映射到其各自地址空间的方式。由共享内存创建者控制对此内存的存取权限,以及其键是公有还是私有。如果它有足够的权限,则它还可以将此共享内存加载到物理内存中。

每个使用此共享内存的进程必须通过系统调用将其连接到虚拟内存上。这时进程创建新的vm_area_struct来描述此共享内存。进程可以决定此共享内存在其虚拟地址空间的位置,或者让Linux选择一块足够大的区域。

新的vm_area_struct结构将被放到由shmid_ds指向的vm_area_struct链表中。通过vm_next_shared和vm_prev_shared指针将它们连接起来。虚拟内存在连接时并没有创建;进程访问它时才创建。

当进程首次访问共享虚拟内存中的页面时,将产生页面错。当取回此页面后,Linux找到了描述此页面的vm_area_struct数据结构。它包含指向使用此种类型虚拟内存的处理函数地址指针。共享内存页面错误处理代码将在此shmid_ds对应的页表入口链表中寻找是否存在此共享虚拟内存页面。如果不存在,则它将分配物理页面,并为其创建页表入口。同时还将它放入当前进程的页表中,此入口被保存在shmid_ds结构中。这意味着下个试图访问此内存的进程还会产生页面错误,共享内存错误处理函数将为此进程使用其新创建的物理页面。这样,第一个访问虚拟内存页面的进程创建这块内存,随后的进程把此页面加入到各自的虚拟地址空间中。

当进程不再共享此虚拟内存时,进程和共享内存的连接将被断开。如果其他进程还在使用这个内存,则此操作只影响当前进程。其对应的vm_area_struct结构将从shmid_ds结构中删除并回收。当前进程对应此共享内存地址的页表入口也将被更新并置为无效。

当最后一个进程断开与共享内存的连接时,当前位于物理内存中的共享内存页面将被释放,同时还有此共享内存的shmid_ds结构。

共享内存相关结构

每一个共享内存区都有一个控制结构struct shmid_kernel,它对于内核来说是私有的。结构中成员shm_file存储了将被映射文件的地址。每个共享内存区对象都对应特殊文件系统shm中的一个文件。在一般情况下,特殊文件系统shm中的文件是不能用read()、write()等方法访问的。当采取共享内存的方式把其中的文件映射到进程地址空间后,可直接采用访问内存的方式对其访问。

结构shmid_kernel列出如下(在include/linux/shm.h中):
struct shmid_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 */
	pid_t			shm_lprid;      /* 在该段上操作的最后一个进程的pid */
	struct user_struct	*mlock_user;
};

内核通过全局数据结构struct ipc_ids shm_ids维护系统中的所有共享内存区域。shm_ids.entries变量指向一个ipc_id结构数组,而在每个ipc_id结构数组中都有个指向kern_ipc_perm结构的指针。共享内存数据结构之间的关系如图3所示。

第4章用户进 09.png

图3 共享内存数据结构之间的关系

对于系统V共享内存区来说,kern_ipc_perm的宿主或说容器是shmid_kernel结构,shmid_kernel描述了一个共享内存区域,通过shm_ids可访问到系统中所有的共享区域。

同时,在shmid_kernel结构的file类型指针shm_file指向tmpfs文件系统中对应的文件,这样,共享内存区域就与tmpfs文件系统中的文件对应起来。可通过文件系统来映射到共享内存了。

共享内存文件系统

调用shmget()时,创建了一个共享内存区域,并且创建了tmpfs文件系统中的一个同名文件,与共享内存区域相对应。在创建了一个共享内存区域后,还要将它映射到进程地址空间,系统调用shmat()完成此项功能。调用shmat()的过程就是映射到tmpfs文件系统中的同名文件过程,类似于mmap()系统调用。

另外,还有一个hugetlbfs内存文件系统可用于共享内存,它的功能与tmpfs文件系统大同小异。这里只分析tmpfs文件系统。

tmpfs文件系统是基于内存的文件系统,使用磁盘交换空间来存储,并且当为存储文件请求页面时,使用虚拟内存(VM)子系统。Ext2fs和JFFS2等文件系统驻留在底层块设备之上,而tmpfs直接位于VM上。默认系统就会加载/dev/shm。

tmpfs文件系统与ramfs文件系统相比,tmpfs可获得交换与限制检查。还有一个以内存作为操作对象的Ramdisk盘(在/dev/ram*下),Ramdisk在物理ram上模拟一个固定尺寸的硬盘,在Ramdisk上面可创建普通的文件系统。Ramdisk不能交换并且不能改变大小。这几个概念不能混淆。

函数init_tmpfs用来初始化tmpfs文件系统,它注册tmpfs_fs_type文件系统类型,挂接文件系统。函数init_tmpfs分析如下(在mm/tmpfs.c中):
static int __init init_tmpfs(void)
{
	int error;
    //创建结构shmem_inode_info的对象缓冲区
	error = init_inodecache();
	if (error)
		goto out3;
   //注册文件系统
	error = register_filesystem(&tmpfs_fs_type);
	if (error) {
		printk(KERN_ERR "Could not register tmpfs\n");
		goto out2;
	}
#ifdef CONFIG_TMPFS
	devfs_mk_dir("shm"); //在设备文件系统中建立shm目录
#endif
  //挂接文件系统
	shm_mnt = do_kern_mount(tmpfs_fs_type.name, MS_NOUSER,
				tmpfs_fs_type.name, NULL);
	……
}

结构shmem_inode_info是共享内存特殊节点信息结构,通过其成员vfs_inode节点,可找到这个结构。结构shmem_inode_info分析如下(在include/linux/shmem_fs.h中):
struct shmem_inode_info {

spinlock_t lock; unsigned long flags; unsigned long alloced; //分配给文件的数据页 unsigned long swapped; //指定给swap的总数 unsigned long next_index; /* highest alloced index + 1 */ struct shared_policy policy; //NUMA内存分配策略 struct page *i_indirect; //顶层间接块页 swp_entry_t i_direct[SHMEM_NR_DIRECT]; //第一个块 struct list_head swaplist; //可能在交换(swap)的链表 struct inode vfs_inode;

};

结构shmem_sb_info是共享内存超级块信息结构,描述了文件系统的块数和节点数。这个结构列出如下(在include/linux/shmem_fs.h中):
struct shmem_sb_info {

unsigned long max_blocks; //允许的最大块数 unsigned long free_blocks; //可分配的空闲块数 unsigned long max_inodes; //允许的最大节点数 unsigned long free_inodes; //可分配的节点数 spinlock_t stat_lock;

};

结构tmpfs_fs_type描述了文件系统类型,列出如下:
static struct file_system_type tmpfs_fs_type = {

.owner = THIS_MODULE, .name = "tmpfs", .get_sb = shmem_get_sb, .kill_sb = kill_litter_super,

};

函数shmem_fill_super填充超级块结构,分配根节点及根目录,函数列出如下(在mm/shmem.c中):
static int shmem_fill_super(struct super_block *sb,

void *data, int silent) { struct inode *inode; struct dentry *root; int mode = S_IRWXUGO | S_ISVTX; uid_t uid = current->fsuid; gid_t gid = current->fsgid; int err = -ENOMEM;   #ifdef CONFIG_TMPFS unsigned long blocks = 0; unsigned long inodes = 0;   if (!(sb->s_flags & MS_NOUSER)) { blocks = totalram_pages / 2; //限制块数到内存页数的一半 inodes = totalram_pages - totalhigh_pages;//限制节点数 if (inodes > blocks) //节点数不超过块数 inodes = blocks;

      //分析data得到mode、uid、gid、blocks、inodes

if (shmem_parse_options(data, &mode,&uid, &gid, &blocks, &inodes))return -EINVAL;} if (blocks || inodes) {struct shmem_sb_info *sbinfo;

       //分配对象空间

sbinfo = kmalloc(sizeof(struct shmem_sb_info), GFP_KERNEL);if (!sbinfo)return -ENOMEM;sb->s_fs_info = sbinfo;spin_lock_init(&sbinfo->stat_lock);sbinfo->max_blocks = blocks;sbinfo->free_blocks = blocks;sbinfo->max_inodes = inodes;sbinfo->free_inodes = inodes;}#endif sb->s_maxbytes = SHMEM_MAX_BYTES;sb->s_blocksize = PAGE_CACHE_SIZE;sb->s_blocksize_bits = PAGE_CACHE_SHIFT;sb->s_magic = TMPFS_MAGIC;sb->s_op = &shmem_ops; //超级块操作函数inode = shmem_get_inode(sb, S_IFDIR | mode, 0);//分配节点if (!inode)goto failed;inode->i_uid = uid;inode->i_gid = gid;root = d_alloc_root(inode);//分配根目录if (!root)goto failed_iput;sb->s_root = root;return 0; failed_iput:iput(inode);failed:shmem_put_super(sb);return err;

}

函数shmem_get_inode创建节点并初始化,赋上各种操作函数集结构。函数列出如下:
static struct inode *shmem_get_inode(struct super_block *sb, 

                  int mode, dev_t dev) { struct inode *inode; struct shmem_inode_info *info;

   //得到sb->s_fs_info成员

struct shmem_sb_info *sbinfo = SHMEM_SB(sb); if (sbinfo) {spin_lock(&sbinfo->stat_lock);if (!sbinfo->free_inodes) { //判断是否有空节点spin_unlock(&sbinfo->stat_lock);return NULL;}sbinfo->free_inodes--;spin_unlock(&sbinfo->stat_lock);}  //分配一个节点号,创建inode对象空间,初始化inodeinode = new_inode(sb);if (inode) { //inode初始化inode->i_mode = mode;inode->i_uid = current->fsuid;inode->i_gid = current->fsgid;inode->i_blksize = PAGE_CACHE_SIZE;inode->i_blocks = 0;    //地址空间操作函数,提供.writepage等对页的操作函数inode->i_mapping->a_ops = &shmem_aops;inode->i_mapping->backing_dev_info = &shmem_backing_dev_info;inode->i_atime = inode->i_mtime = inode->i_ctime = CURRENT_TIME;    //通过inode得到它的容器结构shmem_inode_info info = SHMEM_I(inode);

       //结构成员清0

memset(info, 0, (char *)inode - (char *)info);spin_lock_init(&info->lock);

		mpol_shared_policy_init(&info->policy);

INIT_LIST_HEAD(&info->swaplist); switch (mode & S_IFMT) {default:init_special_inode(inode, mode, dev);break;case S_IFREG:inode->i_op = &shmem_inode_operations;//节点操作函数inode->i_fop = &shmem_file_operations;//文件操作函数break;case S_IFDIR:inode->i_nlink++;/* Some things misbehave if size == 0 on a directory */inode->i_size = 2 * BOGO_DIRENT_SIZE; //即2*20inode->i_op = &shmem_dir_inode_operations;//目录节点操作函数inode->i_fop = &simple_dir_operations;//目录文件操作函数break;case S_IFLNK:break;}}return inode;

}

这里只列出了共享内存文件操作函数集,列出如下:
static struct file_operations shmem_file_operations = {

.mmap = shmem_mmap, #ifdef CONFIG_TMPFS .llseek = generic_file_llseek, .read = shmem_file_read, .write = shmem_file_write, .fsync = simple_sync_file, .sendfile = shmem_file_sendfile, #endif

};

函数shmem_file_read将内存上的内容读入到用户空间buf中去。函数列出如下:
static ssize_t shmem_file_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos)

{ read_descriptor_t desc;   if ((ssize_t) count < 0) return -EINVAL; if (!access_ok(VERIFY_WRITE, buf, count)) //如果没有访问权限,返回 return -EFAULT; if (!count) return 0;   desc.written = 0; desc.count = count; desc.arg.buf = buf; desc.error = 0;   do_shmem_file_read(filp, ppos, &desc, file_read_actor); if (desc.written) return desc.written; return desc.error;

}

函数do_shmem_file_read完成具体的读操作,它从共享内存文件中读数据到用户空间。函数分析如下:
static void do_shmem_file_read(struct file *filp, loff_t *ppos, read_descriptor_t *desc, read_actor_t actor)

{ struct inode *inode = filp->f_dentry->d_inode; struct address_space *mapping = inode->i_mapping; unsigned long index, offset;   index = *ppos >> PAGE_CACHE_SHIFT; //得到以页序号 offset = *ppos & ~PAGE_CACHE_MASK;//得到页内偏移   for (;;) { struct page *page = NULL; unsigned long end_index, nr, ret;     //读出inode->i_size loff_t i_size = i_size_read(inode);

      //算出结束的页序号

end_index = i_size >> PAGE_CACHE_SHIFT;if (index > end_index)break;if (index == end_index) {nr = i_size & ~PAGE_CACHE_MASK;//页内偏移if (nr <= offset)break;}    //从swap中得到一页或分配新的一页desc->error = shmem_getpage(inode, index, &page, SGP_READ, NULL);if (desc->error) {if (desc->error == -EINVAL)desc->error = 0;break;} nr = PAGE_CACHE_SIZE; //nr为一页大小i_size = i_size_read(inode); //读出inode->i_sizeend_index = i_size >> PAGE_CACHE_SHIFT;if (index == end_index) {nr = i_size & ~PAGE_CACHE_MASK;if (nr <= offset) {if (page)page_cache_release(page);break;}}nr -= offset; if (page) {//如果用户能用任意的虚拟地址写这页,      //在内核里读这页前注意存在潜在的别名。      //这个文件的页在用户空间里已被修改?if (mapping_writably_mapped(mapping))flush_dcache_page(page); if (!offset)//偏移为0,即从页的头部开始mark_page_accessed(page); //标志页为被访问} else       //得到一页,ZERO_PAGE是为0的全局共享页,      //大小由全局变量定义得到:unsigned long empty_zero_page[1024];page = ZERO_PAGE(0); //我们有了这页,并且它是更新的,因而我们把它拷贝到用户空间。    //actor例程返回实际被使用的字节数。它实际上是file_read_actor函数。ret = actor(desc, page, offset, nr);//拷贝到用户空间offset += ret;index += offset >> PAGE_CACHE_SHIFT;offset &= ~PAGE_CACHE_MASK; page_cache_release(page); //释放页结构if (ret != nr || !desc->count)break; cond_resched();//需要时就进行调度} *ppos = ((loff_t) index << PAGE_CACHE_SHIFT) + offset;file_accessed(filp);

}

函数file_read_actor拷贝page中偏移为offset,大小为size的数据到用户空间desc中。函数分析如下(在mm/filemap.c中):
int file_read_actor(read_descriptor_t *desc, struct page *page,

unsigned long offset, unsigned long size) { char *kaddr; unsigned long left, count = desc->count;   if (size > count) size = count;  

   //在用户空间分配size大小,若能写0到用户空间,返回0。

if (!fault_in_pages_writeable(desc->arg.buf, size)) {kaddr = kmap_atomic(page, KM_USER0); //映射page到kaddr地址

      //拷贝数据从内核空间kaddr + offset到用户空间desc->arg.buf

left = __copy_to_user_inatomic(desc->arg.buf,kaddr + offset, size);kunmap_atomic(kaddr, KM_USER0);//取消映射if (left == 0)goto success;} /* Do it the slow way */kaddr = kmap(page); //映射page到kaddr地址

   //拷贝数据从内核空间kaddr + offset到用户空间desc->arg.buf

left = __copy_to_user(desc->arg.buf, kaddr + offset, size);kunmap(page); //取消映射 if (left) {size -= left;desc->error = -EFAULT;}success:desc->count = count - size;desc->written += size;desc->arg.buf += size;return size;

}

共享内存系统调用

函数shm_init初始化共享内存,函数列出如下(在ipc/shm.c中):
void __init shm_init (void)
{
	ipc_init_ids(&shm_ids, 1);//建立有1个ID的shm_ids共享内存ID集,
#ifdef CONFIG_PROC_FS  //在/proc文件系统中建立文件
	create_proc_read_entry("sysvipc/shm", 0, NULL, sysvipc_shm_read_proc, NULL);
#endif
}

函数ipc_init_ids创建size个ID的ids共享内存ID集,初始化IPC的ID,给IPC ID一个范围值(限制在IPCMNI以下)。建立一个序列的范围,接着分配并初始化数组本身。函数ipc_init_ids分析如下(在ipc/util.c中):
void __init ipc_init_ids(struct ipc_ids* ids, int size)

{ int i;   //设置信号量&ids->sem->count = 1,并初始化信号量的等待队列 sema_init(&ids->sem,1);   if(size > IPCMNI) //ID超出最大值32768 size = IPCMNI;   //ids初始化 ids->size = size; ids->in_use = 0; ids->max_id = -1; ids->seq = 0; {//算出最大序列 int seq_limit = INT_MAX/SEQ_MULTIPLIER;//最大int值/最大数组值32768 if(seq_limit > USHRT_MAX) ids->seq_max = USHRT_MAX; //为0xffff else ids->seq_max = seq_limit; } //分配对象数组空间 ids->entries = ipc_rcu_alloc(sizeof(struct ipc_id)*size);   if(ids->entries == NULL) { printk(KERN_ERR "ipc_init_ids() failed, ipc service disabled.\n"); ids->size = 0; } for(i=0;i<ids->size;i++) ids->entries[i].p = NULL;

}

创建共享内存

系统调用sys_shmget是用来获得共享内存区域ID的,如果不存在指定的共享区域就创建相应的区域。函数sys_shmget分析如下(在ipc/shm.c中):
asmlinkage long sys_shmget (key_t key, size_t size, int shmflg)
{
	struct shmid_kernel *shp;
	int err, id = 0;
 
	down(&shm_ids.sem);
	if (key == IPC_PRIVATE) {//创建共享内存区
		err = newseg(key, shmflg, size);
	} else if ((id = ipc_findkey(&shm_ids, key)) == -1) {//没找到共享内存
		if (!(shmflg & IPC_CREAT))
			err = -ENOENT;
		else
			err = newseg(key, shmflg, size); //创建共享内存区
	} else if ((shmflg & IPC_CREAT) && (shmflg & IPC_EXCL)) {
		err = -EEXIST;
	} else {
		shp = shm_lock(id);//由id号在数组中找到对应shmid_kernel结构
		if(shp==NULL)
			BUG();
		if (shp->shm_segsz < size)//检查共享内存的大小
			err = -EINVAL;
		else if (ipcperms(&shp->shm_perm, shmflg))//检查IPC许可
			err = -EACCES;
		else {
      //即shmid =  IPC最大数组个数(SEQ_MULTIPLIER)*seq + id
			int shmid = shm_buildid(id, shp->shm_perm.seq);
			err = security_shm_associate(shp, shmflg);
			if (!err)
				err = shmid;//返回共享内存区ID
		}
		shm_unlock(shp);
	}
	up(&shm_ids.sem);
 
	return err;
}
 
#define shm_lock(id)	

在全局结构变量shm_ids中,成员shm_ids-> entries是kern_ipc_perm结构数组,由下标id可得到shm_ids-> entries[id],即第id个kern_ipc_perm结构。

由于结构shmid_kernel中的第一个成员就是kern_ipc_perm结构,所以shm_lock(id)可找到对应的shmid_kernel结构,进而找到file结构,完成在不同进程间由id查找共享内存的过程。

函数ipc_lock分析如下(在ipc/util.c中):
struct kern_ipc_perm* ipc_lock(struct ipc_ids* ids, int id)
{
	struct kern_ipc_perm* out;
	int lid = id % SEQ_MULTIPLIER;//与最大的id模除,即不超过最大的id数
	struct ipc_id* entries;
 
	rcu_read_lock();
	if(lid >= ids->size) {
		rcu_read_unlock();
		return NULL;
	}
    /*下面两个读屏障是与grow_ary()中的两个写屏障对应,它们保证写与读有同样的次序。smp_rmb()影响所有的CPU。如果在两个读之间在数据依赖性,rcu_dereference()被使用,这仅在Alpha平台上起作用。*/
 
	smp_rmb(); //阻止用新的尺寸对旧数组进行索引
	entries = rcu_dereference(ids->entries);
	out = entries[lid].p;//得到kern_ipc_perm结构
	if(out == NULL) {
		rcu_read_unlock();
		return NULL;
	}
	spin_lock(&out->lock);
 
	/*在ipc_lock锁起作用时,ipc_rmid()可能已释放了ID,这里验证结构是否还有效。*/
	if (out->deleted) {
		spin_unlock(&out->lock);
		rcu_read_unlock();
		return NULL;
	}
	return out;
}

函数newseg创建一个共享内存,即创建一个内存中的文件,并设置文件操作函数结构。
static int newseg (key_t key, int shmflg, size_t size)

{ int error; struct shmid_kernel *shp;   //将分配的大小转换成以页为单位 int numpages = (size + PAGE_SIZE -1) >> PAGE_SHIFT; struct file * file; char name[13]; int id;   //大小超界检查 if (size < SHMMIN || size > shm_ctlmax) return -EINVAL;   //当前共享内存的总页数 >= 系统提供的最大共享内存总页数 if (shm_tot + numpages >= shm_ctlall) return -ENOSPC;   //分配对象空间 shp = ipc_rcu_alloc(sizeof(*shp)); if (!shp) return -ENOMEM;   shp->shm_perm.key = key; shp->shm_flags = (shmflg & S_IRWXUGO); shp->mlock_user = NULL;   shp->shm_perm.security = NULL; error = security_shm_alloc(shp); //安全机制检查 if (error) { ipc_rcu_putref(shp); return error; }   if (shmflg & SHM_HUGETLB) { //使用hugetlb文件系统创建size大小的文件  file = hugetlb_zero_setup(size); shp->mlock_user = current->user; } else {     //使用tmpfs文件系统创建名为key的文件 sprintf (name, "SYSV%08x", key); file = shmem_file_setup(name, size, VM_ACCOUNT); } error = PTR_ERR(file); if (IS_ERR(file)) goto no_file;   error = -ENOSPC; id = shm_addid(shp); //找到一个空闲的id if(id == -1) goto no_id;   shp->shm_cprid = current->tgid; shp->shm_lprid = 0; shp->shm_atim = shp->shm_dtim = 0; shp->shm_ctim = get_seconds(); shp->shm_segsz = size; shp->shm_nattch = 0;

   //即shmid =  IPC最大数组个数(SEQ_MULTIPLIER)*seq + id

shp->id = shm_buildid(id,shp->shm_perm.seq);shp->shm_file = file;file->f_dentry->d_inode->i_ino = shp->id;if (shmflg & SHM_HUGETLB)

      //设置hugetlb文件系统文件操作函数结构

set_file_hugepages(file);elsefile->f_op = &shm_file_operations;shm_tot += numpages; //总的共享内存页数shm_unlock(shp);return shp->id; no_id:fput(file);no_file:security_shm_free(shp);ipc_rcu_putref(shp);return error;

}

函数shmem_file_setup得到一个在tmpfs中的非链接文件file,其参数name是dentry的名字,在/proc/<pid>/maps中是可见的,参数size指的是所设置的file大小。函数分析如下:
struct file *shmem_file_setup(char *name, loff_t size, unsigned long flags)

{ int error; struct file *file; struct inode *inode; struct dentry *dentry, *root; struct qstr this;   if (IS_ERR(shm_mnt)) return (void *)shm_mnt;   if (size < 0 || size > SHMEM_MAX_BYTES) return ERR_PTR(-EINVAL);  

   //预先计算VM对象的整个固定大小。对于共享内存和共享匿名(/dev/zero)映射来说,和私有映射的预先设置是一样的。

if (shmem_acct_size(flags, size))return ERR_PTR(-ENOMEM); error = -ENOMEM;this.name = name;this.len = strlen(name);this.hash = 0; /* will go */root = shm_mnt->mnt_root;//得到根目录dentry = d_alloc(root, &this);//分配dentry对象并初始化if (!dentry)goto put_memory; error = -ENFILE;file = get_empty_filp(); //得到一个未用的file结构if (!file)goto put_dentry; error = -ENOSPC;inode = shmem_get_inode(root->d_sb, S_IFREG | S_IRWXUGO, 0);//分配节点if (!inode)goto close_file; SHMEM_I(inode)->flags = flags & VM_ACCOUNT;d_instantiate(dentry, inode);//加inode到dentry上inode->i_size = size;inode->i_nlink = 0; //它是非链接的file->f_vfsmnt = mntget(shm_mnt);file->f_dentry = dentry;file->f_mapping = inode->i_mapping;file->f_op = &shmem_file_operations; //文件操作函数集file->f_mode = FMODE_WRITE | FMODE_READ;return file; close_file:put_filp(file);put_dentry:dput(dentry);put_memory:shmem_unacct_size(flags, size);return ERR_PTR(error);

}

映射函数shmat

在应用程序中调用函数shmat(),把共享内存区域映射到调用进程的地址空间中去。这样,进程就可以对共享区域方便地进行访问操作。

在内核中,函数shmat对应系统调用的执行函数是函数do_shmat,它分配描述符,映射shm,把描述符加到链表中。其参数shmaddr是当前进程所要求映射的目标地址。函数do_shmat分析如下:
long do_shmat(int shmid, char __user *shmaddr, int shmflg, ulong *raddr)
{
	struct shmid_kernel *shp;
	unsigned long addr;
	unsigned long size;
	struct file * file;
	int    err;
	unsigned long flags;
	unsigned long prot;
	unsigned long o_flags;
	int acc_mode;
	void *user_addr;
 
	if (shmid < 0) {//不正确的共享内存ID
		err = -EINVAL;
		goto out;
	} else if ((addr = (ulong)shmaddr)) {
		if (addr & (SHMLBA-1)) {//不能整除,没与页对齐,需调整
			if (shmflg & SHM_RND)//对齐调整标志SHM_RND
				addr &= ~(SHMLBA-1);	   //向移动对齐
			else
#ifndef __ARCH_FORCE_SHMLBA
				if (addr & ~PAGE_MASK)
#endif
					return -EINVAL;
		}
		flags = MAP_SHARED | MAP_FIXED;
	} else {
		if ((shmflg & SHM_REMAP))
			return -EINVAL;
 
		flags = MAP_SHARED;
	}
 
	if (shmflg & SHM_RDONLY) {//仅读
		prot = PROT_READ;
		o_flags = O_RDONLY;
		acc_mode = S_IRUGO;
	} else {
		prot = PROT_READ | PROT_WRITE;
		o_flags = O_RDWR;
		acc_mode = S_IRUGO | S_IWUGO;
	}
	if (shmflg & SHM_EXEC) {//可运行
		prot |= PROT_EXEC;
		acc_mode |= S_IXUGO;
	}
 
  //由id号在数组中找到对应shmid_kernel结构
	shp = shm_lock(shmid);
	if(shp == NULL) {
		err = -EINVAL;
		goto out;
	}
	err = shm_checkid(shp,shmid); //检查shmid是否正确
	if (err) {
		shm_unlock(shp);
		goto out;
	}
	if (ipcperms(&shp->shm_perm, acc_mode)) {//检查IPC许可
		shm_unlock(shp);
		err = -EACCES;
		goto out;
	}
 
	err = security_shm_shmat(shp, shmaddr, shmflg);//安全检查
	if (err) {
		shm_unlock(shp);
		return err;
	}
 
	file = shp->shm_file;//得到文件结构
	size = i_size_read(file->f_dentry->d_inode);//读文件中size大小
	shp->shm_nattch++; //对共享内存访问进程计数
	shm_unlock(shp);
 
	down_write(&current->mm->mmap_sem);
	if (addr && !(shmflg & SHM_REMAP)) {
		user_addr = ERR_PTR(-EINVAL);
    //如果当前进程的虚拟内存中的VMA与共享内存地址交叉
		if (find_vma_intersection(current->mm, addr, addr + size))
			goto invalid;
		//如果shm段在堆栈之下,确信有剩下空间给堆栈增长用(最少4页)
		if (addr < current->mm->start_stack &&
		    addr > current->mm->start_stack - size - PAGE_SIZE * 5)
			goto invalid;
	}
	//建立起文件与虚存空间的映射,即将文件映射到进程空间	
	user_addr = (void*) do_mmap (file, addr, size, prot, flags, 0);
……
}

函数shmdt()用来解除进程对共享内存区域的映射。函数shmctl实现对共享内存区域的控制操作。

信号量

信号量主要提供对进程间共享资源访问控制机制,确保每次只有一个进程资源进行访问。信号量主要用于进程间同步。信号量集是信号量的集合,用于多种共享资源的进程间同步。信号量的值表示当前共享资源可用数量,如果一个进程要申请共享资源,那么就从信号量值中减去要申请的数目,如果当前没有足够的可用资源,进程可以睡眠等待,也可以立即返回。

用户空间信号量机制是在内核空间实现,用户进程直接使用。与信号量相关的操作的系统调用有:sys_semget(),sys_semop()和sys_semctl()。

信号量数据结构

信号量通过内核提供的数据结构实现,信号量数据结构之间的关系如图4所示。结构sem_array的sem_base指向一个信号量数组,信号量用结构sem描述,信号量集合用结构sem_array结构描述。下面分别说明信号的数据结构。

第4章用户进 10.png

图4 信号量数据结构之间的关系
(1)信号量结构sem
系统中每个信号量用一个信号量结构sem进行描述。结构sem列出如下(在include/linux/sem.h中):
struct sem {
	int	semval;		//信号量当前的值
	int	sempid;		//上一次操作的进程pid
};

(2)信号量集结构sem_array
系统中的每个信号量集用一个信号量集结构sem_array描述,信号量集结构列出如下:
struct sem_array {
	struct kern_ipc_perm	sem_perm; 	//IPC许可的结构,包含uid、gid等
	time_t			sem_otime; 	//上一次信号量操作时间
	time_t			sem_ctime;	    //上一次发生变化的时间
	struct sem		*sem_base;	        //集合中第一个信号量的指针
	struct sem_queue	*sem_pending;	  //将被处理的正挂起的操作
	struct sem_queue	**sem_pending_last; //上一次挂起的操作
	struct sem_undo		*undo;		//集合上的undo请求
	unsigned long		sem_nsems;	//集合中信号量的序号
};

(3)信号量集合的睡眠队列结构sem_queue
系统中每个睡眠的进程用一个队列结构sem_queue描述。结构sem_queue列出如下:
struct sem_queue {
	struct sem_queue *	next;	 //队列里的下一个元素
	struct sem_queue **	prev;	
	struct task_struct*	sleeper; //这个睡眠进程
	struct sem_undo *	undo;	 
	int    			pid;	     //正在请求的进程的pid
	int    			status;	 //操作的完成状态
	struct sem_array *	sma;   //操作的信号量集合
	int			id;	     //内部的信号量ID
	struct sembuf *		sops;	 //正挂起的操作的集合
	int			nsops;	 //操作的数量
};

(4)信号量操作值结构sembuf

系统调用semop会从用户空间传入结构sembuf实例值,其中,成员sem_op是一个表示操作的整数,它表示取得或归还资源的数量。该整数将加到对应信号量的当前值上。如果具体的信号量数加入这个整数后为负数,则表明没有资源可用,当前进程就会进入睡眠等待中。成员sem_flag设置两个标志位:一个是IPC_NOWAIT,表示在条件不能满足时不要睡眠等待而立即返回,错误代码为EAGAIN;另一个为SEM_UNDO,表示进程未归还资源就退出时,由内核归还资源。

结构sembuf如下:
struct sembuf {
	unsigned short  sem_num;	//数组中信号量的序号
	short		sem_op;		/* 信号量操作值(正数、负数或0) */
	short		sem_flg;	     //操作标志,为IPC_NOWAIT或SEM_UNDO
};

(5)死锁恢复结构sem_undo

当进程修改了信号量而进入临界区后,进程因为崩溃或被"杀死"而没有退出临界区,此时,其他被挂起在此信号量上的进程永远得不到运行机会,从而引起死锁。

为了避免死锁,Linux内核维护一个信号量数组的调整列表,让信号量的状态退回到操作实施前的状态。

Linux为每个信号量数组的每个进程维护至少一个结构sem_undo。新创建的结构sem_undo实现既在进程结构task_struct的成员undo上排队,也在信号量数组结构semid_array的成员undo上排队。当对信号量数组上的一个信号量进行操作时,操作值的负数与该信号量的"调整值"相加。例如:如果操作值为2,则把-2加到该信号量的"调整值"域semadj。

每个任务有一个undo请求的链表,当进程退出时,它们被自动地执行。当进程被删除时,Linux完成了对结构sem_undo的设置及对信号量数组的调整。如果一个信号量集合被删除,结构sem_undo依然留在该进程结构task_struct中,但信号量集合的识别号变为无效。

结构sem_undo列出如下:
struct sem_undo {
  //这个进程的下一个sem_undo节点条目,链入结构task_struct中的undo队列
	struct sem_undo *	proc_next;	
  //信号量集的下一个条目,链入结构sem_array中的undo队列
	struct sem_undo *	id_next;	
	int			semid;		//信号量集ID
	short *			semadj;	//信号量数组的调整,每个进程一个
};

结构sem_undo_list控制着对sem_undo结构链表的共享访问。sem_undo结构待在一个CLONE_SYSVSEM被任务组里所有任务共享。结构sem_undo_list列出如下:
struct sem_undo_list {

atomic_t refcnt; spinlock_t lock; struct sem_undo *proc_list; };   struct sysv_sem { struct sem_undo_list *undo_list;

};

系统调用函数功能说明

与信号量相关的操作的系统调用有:sys_semget(),sys_semop()和sys_semctl()。下面分别说明各个系统调用的功能。

(1)系统调用sys_semget

系统调用sys_semget创建或获取一个信号量集合,参数nsems为信号量的个数,参数semflg为操作标识,值为IPC_CREAT或者IPC_EXCL。其定义列出如下:

asmlinkage long sys_semget(key_t key, int nsems, int semflg)

(2)系统调用sys_semop

系统调用sys_semop用来操作信号量,其定义列出如下:

asmlinkage long sys_semop (int semid, struct sembuf __user *tsops, unsigned nsops)

函数sys_semop参数semid是信号量的识别号,可以由系统调用sys_semget获取;参数sops指向执行操作值的数组;参数nsop是操作的个数。

信号量操作时,操作值和信号量的当前值相加,如果大于 0,或操作值和当前值均为 0,则操作成功。如果所有操作中的任一个操作不能成功,则 Linux 会挂起此进程。如果不能挂起,系统调用返回并指明操作不成功,进程可以继续执行。如果进程被挂起,Linux保存信号量的操作状态,并将当前进程放入等待队列。

(3)系统调用sys_semctl

系统调用sys_semctl执行指定的控制命令,其定义列出如下:

asmlinkage long sys_semctl (int semid, int semnum, int cmd, union semun arg)

参数semid是信号量集的ID,参数semnum为信号量的个数,参数cmd为控制命令,参数arg为传递信号量信息或返回信息的联合体。结构semun的定义列出如下:
union semun {
	int val;			/* 命令SETVAL的值 */
	struct semid_ds __user *buf;	/* 命令IPC_STAT和IPC_SET的buffer */
	unsigned short __user *array;	/* 命令GETALL和SETALL的信号量数组 */
	struct seminfo __user *__buf;	/*命令IPC_INFO 的buffer*/
	void __user *__pad;
};

系统调用sys_semctl的命令参数cmd说明如表7所示。
表7 cmd命令说明
命令说明
IPC_STAT从信号量集合上获致结构semid_ds,存放到semun的成员buf中返回。
IPC_SET设置信号量集合结构semid_ds中ipc_perm域,从semun的buf中读取值。
IPC_RMID删除信号量集合。
GETALL从信号量集合中获取所有信号量值,并把其整数值存放到semun的array中返回。
GETNCNT返回当前等待进程个数。
GETPID返回最后一个执行系统调用semop进程的PID。
GETVAL返回信号量集内单个信号量的值。
GETZCNT返回当前等待100%资源利用的进程个数。
SETALL设置信号量集合中所有信号量值。
SETVAL用semun的val设置信号量集中单个信号量值。

系统调用函数的实现

(1)初始化函数sem_init
函数sem_init初始信号量的全局变量结构sem_ids,并在/proc文件系统中加上信号量的文件,函数sem_init列出如下(在ipc/sem.c中):
static struct ipc_ids sem_ids;
void __init sem_init (void)
{
	used_sems = 0;
  //初始化全局变量结构sem_ids,分配最大128的信号量ID
	ipc_init_ids(&sem_ids,sc_semmni);
//在proc进程中创建信号的文件及内容
#ifdef CONFIG_PROC_FS
	create_proc_read_entry("sysvipc/sem", 0, NULL, sysvipc_sem_read_proc, NULL);
#endif
}

(2)系统调用sys_semget
系统调用sys_semget创建或打开一个信号量。其列出如下:
asmlinkage long sys_semget(key_t key, int nsems, int semflg)
{
	struct ipc_namespace *ns;
	struct ipc_ops sem_ops;
	struct ipc_params sem_params;
 
	ns = current->nsproxy->ipc_ns;
 
	if (nsems < 0 || nsems > ns->sc_semmsl)
		return -EINVAL;
 
	sem_ops.getnew = newary;
	sem_ops.associate = sem_security;
	sem_ops.more_checks = sem_more_checks;
 
	sem_params.key = key;
	sem_params.flg = semflg;
	sem_params.u.nsems = nsems;
 
	return ipcget(ns, &sem_ids(ns), &sem_ops, &sem_params);
}

函数newary分配新的信号量集的大小,加信号量集中的成员sem_perm到全局变量结构sem_ids中,这样,其他进程通过sem_ids就可找到这个信号量集。另外,还初始化信号量集成员,得到信号量集的ID。 函数newary列出如下:
#define IN_WAKEUP	1
static int newary (key_t key, int nsems, int semflg)
{
	int id;
	int retval;
	struct sem_array *sma;
	int size;
 
	if (!nsems)
		return -EINVAL;
	if (used_sems + nsems > sc_semmns)
		return -ENOSPC;
  //信号量集大小 = 信号量集结构大小 + 信号量数目*信号量结构大小
	size = sizeof (*sma) + nsems * sizeof (struct sem);
	sma = ipc_rcu_alloc(size); //分配空间
	if (!sma) {
		return -ENOMEM;
	}
	memset (sma, 0, size);
 
	sma->sem_perm.mode = (semflg & S_IRWXUGO);
	sma->sem_perm.key = key;
 
	sma->sem_perm.security = NULL;
	retval = security_sem_alloc(sma); //安全检查
	if (retval) {
		ipc_rcu_putref(sma);
		return retval;
	}
  //加一个IPC ID到全局变量结构sem_ids。
	id = ipc_addid(&sem_ids, &sma->sem_perm, sc_semmni);
	if(id == -1) {
		security_sem_free(sma);
		ipc_rcu_putref(sma);
		return -ENOSPC;
	}
	used_sems += nsems;
 
	sma->sem_base = (struct sem *) &sma[1];
	/* sma->sem_pending = NULL; */
	sma->sem_pending_last = &sma->sem_pending;
	/* sma->undo = NULL; */
	sma->sem_nsems = nsems;
	sma->sem_ctime = get_seconds();
	sem_unlock(sma);
      //由IPC ID生成信号量集的ID,即SEQ_MULTIPLIER*seq + id;
 	return sem_buildid(id, sma->sem_perm.seq);
}

(3)系统调用sys_semop
系统调用sys_semop操作信号量,决定当前进程是否睡眠等待。其列出如下:
asmlinkage long sys_semop (int semid, struct sembuf __user *tsops, unsigned nsops)
{
	return sys_semtimedop(semid, tsops, nsops, NULL);
}

在进程的task_struct结构中维持了一个sem_undo结构队列,用于防止死锁,它表示进程占用资源未还,即进程有"债务",在进程exit退出时由内核归还。 函数sys_semtimedop列出如下:
asmlinkage long sys_semtimedop(int semid, struct sembuf __user *tsops,
			unsigned nsops, const struct timespec __user *timeout)
{
	int error = -EINVAL;
	struct sem_array *sma;
	struct sembuf fast_sops[SEMOPM_FAST];
	struct sembuf* sops = fast_sops, *sop;
	struct sem_undo *un;
	int undos = 0, decrease = 0, alter = 0, max;
	struct sem_queue queue;
	unsigned long jiffies_left = 0;
 
	if (nsops < 1 || semid < 0)
		return -EINVAL;
	if (nsops > sc_semopm)
		return -E2BIG;
	if(nsops > SEMOPM_FAST) {
    //分配多个信号量操作的空间
		sops = kmalloc(sizeof(*sops)*nsops,GFP_KERNEL);
		if(sops==NULL)
			return -ENOMEM;
	}
  //从用户空间拷贝得到信号量操作
	if (copy_from_user (sops, tsops, nsops * sizeof(*tsops))) {
		error=-EFAULT;
		goto out_free;
	}
	if (timeout) {
		struct timespec _timeout;
    //从用户空间拷贝得到定时信息
		if (copy_from_user(&_timeout, timeout, sizeof(*timeout))) {
			error = -EFAULT;
			goto out_free;
		}
		if (_timeout.tv_sec < 0 || _timeout.tv_nsec < 0 ||
			_timeout.tv_nsec >= 1000000000L) {
			error = -EINVAL;
			goto out_free;
		}
    //将定时转换成内核时间计数jiffies
		jiffies_left = timespec_to_jiffies(&_timeout);
	}
	max = 0;
	for (sop = sops; sop < sops + nsops; sop++) {
		if (sop->sem_num >= max) //设置信号量最大值
			max = sop->sem_num;
		if (sop->sem_flg & SEM_UNDO) //undo信号量
			undos++;
		if (sop->sem_op < 0) //操作小于0,表示要取得资源
			decrease = 1;
		if (sop->sem_op > 0) //操作大于0,归还资源
			alter = 1;
	}
	alter |= decrease; 
 
retry_undos:
	if (undos) {
    /*查找当前进程的undo_list链表得到sem_undo结构un,如果没有un,就分配一个到semid对应的信号量集合中并初始化*/
		un = find_undo(semid);
		if (IS_ERR(un)) {
			error = PTR_ERR(un);
			goto out_free;
		}
	} else
		un = NULL;
    //通过id在全局变量结构成员sem_ids中找到信号量集
	sma = sem_lock(semid);
	error=-EINVAL;
	if(sma==NULL)
		goto out_free;
	error = -EIDRM;
	if (sem_checkid(sma,semid))//检查semid是否是与sma对应的
		goto out_unlock_free;
 
	if (un && un->semid == -1) {
		sem_unlock(sma);
		goto retry_undos;
	}
	error = -EFBIG;
	if (max >= sma->sem_nsems)
		goto out_unlock_free;
 
	error = -EACCES;
    //检查IPC的访问权限保护
	if (ipcperms(&sma->sem_perm, alter ? S_IWUGO : S_IRUGO))
		goto out_unlock_free;
 
	error = security_sem_semop(sma, sops, nsops, alter);//安全检查
	if (error)
		goto out_unlock_free;
	/*信号量操作,是原子操作性的函数,返回0表示操作成功,当前进程已得到所有资源,返回负值表示操作失败,返回1表示需要睡眠等待*/
	error = try_atomic_semop (sma, sops, nsops, un, current->tgid);
	if (error <= 0) //如果不需要睡眠等待,跳转去更新
		goto update;
 
	/*需要在这个操作上睡眠,放当前进程到挂起队列中并进入睡眠,填充信号量队列*/
	queue.sma = sma;
	queue.sops = sops;
	queue.nsops = nsops;
	queue.undo = un;
	queue.pid = current->tgid;
	queue.id = semid;
	//睡眠时,将一个代表着当前进程的sem_queue数据结构链入到相应的sma->sem_pending队列中
    if (alter)
		append_to_queue(sma ,&queue); //加在队尾
	else
		prepend_to_queue(sma ,&queue); //加在
 
	queue.status = -EINTR;
	queue.sleeper = current;//睡眠进程是当前进程
	current->state = TASK_INTERRUPTIBLE;
	sem_unlock(sma);
  //调度
	if (timeout)
		jiffies_left = schedule_timeout(jiffies_left);
	else
		schedule();
 
	error = queue.status;
	while(unlikely(error == IN_WAKEUP)) {
		cpu_relax();
		error = queue.status;
	}
 
	if (error != -EINTR) {
		//update_queue已获得所有请求的资源
		goto out_free;//正常退出
	}
  //通过id在全局变量结构成员sem_ids中找到信号量集
	sma = sem_lock(semid);
	if(sma==NULL) {
		if(queue.prev != NULL)
			BUG();
		error = -EIDRM;
		goto out_free;
	}
  //如果queue.status != -EINTR,表示我们被另外一个进程唤醒
	error = queue.status;
	if (error != -EINTR) {
		goto out_unlock_free;
	}
  //如果一个中断发生,我们将必须清除队列
	if (timeout && jiffies_left == 0)
		error = -EAGAIN;
	remove_from_queue(sma,&queue);
	goto out_unlock_free;
 
update:
	if (alter)//如果操作需要改变信号量的值
		update_queue (sma);
out_unlock_free:
	sem_unlock(sma);
out_free:
	if(sops != fast_sops)
		kfree(sops);
	return error;

函数try_atomic_semop决定一系列信号量操作是否成功,如果成功就返回0,返回1表示需要睡眠,其他表示错误。函数try_atomic_semop列出如下:
static int try_atomic_semop (struct sem_array * sma, struct sembuf * sops,

int nsops, struct sem_undo *un, int pid) { int result, sem_op; struct sembuf *sop; struct sem * curr;   //遍历每个信号操作 for (sop = sops; sop < sops + nsops; sop++) { curr = sma->sem_base + sop->sem_num;//得到操作对应的信号量 sem_op = sop->sem_op; result = curr->semval;//信号量的值   if (!sem_op && result) goto would_block;   result += sem_op;//信号量的值+操作值 if (result < 0) //小于0,无资源可用,应阻塞 goto would_block; if (result > SEMVMX) //超出信号量值的范围 goto out_of_range;//去恢复到操作前的semval值 if (sop->sem_flg & SEM_UNDO) {//undo操作:减去操作值, int undo = un->semadj[sop->sem_num] - sem_op; //超出undo范围是一个错误 if (undo < (-SEMAEM - 1) || undo > SEMAEM) goto out_of_range; } curr->semval = result; }   //遍历每个信号操作, sop--; while (sop >= sops) {     //信号量集中每个信号量赋上pid sma->sem_base[sop->sem_num].sempid = pid;  if (sop->sem_flg & SEM_UNDO)

          //保存操作的undo值

un->semadj[sop->sem_num] -= sop->sem_op;sop--;}//得到操作时间sma->sem_otime = get_seconds();return 0; out_of_range:result = -ERANGE;goto undo; would_block: //阻塞进程if (sop->sem_flg & IPC_NOWAIT) //不等待,立即返回result = -EAGAIN;elseresult = 1; //需等待 undo:  //将前面已完成的操作都减掉,恢复到操作前的semval值sop--;while (sop >= sops) {sma->sem_base[sop->sem_num].semval -= sop->sem_op;sop--;} return result;

}

函数update_queue遍历挂起队列,找到所要的信号量,以及能被完成的进程,对它们进行信号量操作,并从队列中移走挂起的进程,进而唤醒进程。函数update_queue列出如下:
static void update_queue (struct sem_array * sma)

{ int error; struct sem_queue * q;   q = sma->sem_pending; while(q) {//遍历睡眠中等待队列来进行信号量操作 error = try_atomic_semop(sma, q->sops, q->nsops, q->undo, q->pid); //信号量操作   //q->sleeper是否还需要睡眠 if (error <= 0) {//不需要睡眠等待 struct sem_queue *n; remove_from_queue(sma,q);//从队列中移走挂起的进程 n = q->next; q->status = IN_WAKEUP; wake_up_process(q->sleeper);唤醒睡眠进程 //q将在写q->status操作后立即消失  q->status = error; q = n; } else { q = q->next; } }

}

快速用户空间互斥锁(Futex)

快速用户空间互斥锁(fast userspace mutex,Futex)是快速的用户空间的锁,是对传统的System V同步方式的一种替代,传统同步方式如:信号量、文件锁和消息队列,在每次锁访问时需要进行系统调用。而futex仅在有竞争的操作时才用系统调用访问内核,这样,在竞争出现较少的情况下,可以大幅度地减少工作负载

futex在非竞争情况下可从用户空间获取和释放,不需要进入内核。与信号量类似,它有一个可以原子增减的计数器,进程可以等待计数器值变为正数。用户进程通过系统调用对资源的竞争作一个公断。

futex是一个用户空间的整数值,被多个线程或进程共享。Futex的系统调用对该整数值时进行操作,仲裁竞争的访问。glibc中的NPTL库封装了futex系统调用,对futex接口进行了抽象。用户通过NPTL库像传统编程一样地使用线程同步API函数,而不会感觉到futex的存在。

futex的实现机制是:如果当前进程访问临界区时,该临界区正被另一个进程使用,当前进程将锁用一个值标识,表示"有一个等待者正挂起",并且调用sys_futex(FUTEX_WAIT)等待其他进程释放它。内核在内部创建futex队列,以便以后与唤醒者匹配等待者。当临界区拥有者线程释放了futex,它通过变量值发出通知表示还有多个等待者在挂起,并调用系统调用sys_futex(FUTEX_WAKE)唤醒它们。一旦所有等待者已获取资源并释放锁时,futex回到非竞争状态,并没有内核状态与它相关。

robust futex是为了解决futex锁崩溃而对futex进行了增强。例如:当一个进程在持有pthread_mutex_t锁正与其他进程发生竞争时,进程因某种意外原因而提前退出,如:进程发生段错误,或者被用户用shell命令kill -9-ed"强行退出,此时,需要有一种机制告诉等待者"锁的最一个持有者已经非正常地退出"。"

为了解决此类问题,NPTL创建了robust mutex用户空间API pthread_mutex_lock(),如果锁的拥有者进程提前退出,pthread_mutex_lock()返回一个错误值,新的拥有者进程可以决定是否可以安全恢复被锁保护的数据。

信号

信号概述

信号(signal)用来向一个或多个进程发送异步事件信号,是在软件层次上对中断机制的一种模拟,一个进程收到信号与处理器收到一个中断请求的处理过程类似。进程间通信机制中只有信号是异步的,进程不必通过任何操作等待信号的到达,也不知信号何时到达。信号来源于硬件(如硬件故障)或软件(如:一些非法运算)。信号机制经过POSIX实时扩展后,功能更加强大,除了基本通知功能外,还可以传递附加信息。

(1)信号定义

Linux内核用一个word类型变量代表所有信号,每个信号占一位,因此,32位平台最多有32个信号。Linux定义好了一组信号,可以由内核线程或用户进程产生。POSIX.1定义的信号说明如表1,它们定义在include/asm-x86/signal.h中。

表1 POSIX.1定义的信号说明
信号信号值处理动作发出信号的原因
SIGHUP1A终端挂起或者控制进程终止
SIGINT2A键盘中断(如break键被按下)
SIGQUIT3C键盘的退出键被按下
SIGILL4C非法指令
SIGABRT6C由abort(3)发出的退出指令
SIGFPE8C浮点异常
SIGKILL9AEFKill信号
SIGSEGV11C无效的内存引用
SIGPIPE13A管道破裂: 写一个没有读端口的管道
SIGALRM14A由alarm(2)发出的信号
SIGTERM15A终止信号
SIGUSR130,10,16A用户自定义信号1
SIGUSR231,12,17A用户自定义信号2
SIGCHLD20,17,18B子进程结束信号
SIGCONT19,18,25 进程继续(曾被停止的进程)
SIGSTOP17,19,23DEF终止进程
SIGTSTP18,20,24D控制终端(tty)上按下停止键
SIGTTIN21,21,26D后台进程企图从控制终端读
SIGTTOU22,22,27D后台进程企图从控制终端
备注:
1. "值"列表示不同硬件平台的信号定义值,第1个值对应Alpha和Sparc,中间值对应i386、ppc和sh,最后值对应mips。
2. "处理动作"列字母含义:A表示缺省的动作是终止进程,B表示缺省的动作是忽略此信号,C表示缺省的动作是终止进程并进行内核转储(dump core),D表示缺省的动作是停止进程,E表示信号不能被捕获,F表示信号不能被忽略。
3. 信号SIGKILL和SIGSTOP既不能被捕捉,也不能被忽略。

信号的两个主要目的是使一个进程意识到特定事件已发生,以及强迫一个进程执行一个信号处理。信号由事件引起,事件的来源说明如下:

  • 异常:进程运行过程中出现异常;
  • 其他进程:一个进程可以向另一个或一组进程发送信号;
  • 终端中断:按下键Ctrl-C,Ctrl-\等;
  • 作业控制:前台、后台进程的管理;
  • 分配额:CPU超时或文件大小突破限制;
  • 通知:通知进程某事件发生,如I/O就绪等;
  • 报警:计时器到期。
(2)实时信号与非实时信号

非实时信号是值位于SIGRTMIN(值为31)以下的常规信号,发射多次时,只有其中一个送到接收进程。

实时信号是值位于SIGRTMIN(值为31)和SIGRTMAX(值为63)之间的信号,是POSIX标准在原常规信号的基础上扩展而成。实时信号支持信号排队,当进程发射多个信号时,多个信号都能被接收到。

(3)信号响应

信号的生命周期包括信号的产生、挂起的信号和信号的响应。挂起的信号是指已发送但还没有被接收的信号;信号的响应采取注册的动作来传送或处理信号。

当一个进程通过系统调用给另一个进程发送信号时,Linux内核将接收进程的任务结构信号域设置对应该信号的位。如果接收进程睡眠在可被中断的任务状态上时,则唤醒进程,如果睡眠在其他任务状态时,则仅设置信号域的相应位,不唤醒进程。

接收进程检查信号的时机是:从系统调用返回,或者进入/离开睡眠状态时。因此,接收进程对信号并不立即响应,而是在检查信号的时机才执行相应的响应函数。

进程对信号的响应有三种方式:忽略信号、捕捉信号和执行默认操作。忽略信号指接收到信号,但不执行响应函数,忽略信号与信号阻塞的区别是:信号阻塞是将信号用掩码过滤掉,不传递信号,忽略信号是传递了信号,但不执行响应函数。

捕捉信号是指给信号定义响应函数,当信号发生时,就执行自定义的处理函数。由于用户定义的响应函数在用户空间,而信号的检查在内核空间进行,用户空间的函数不允许在内核空间执行,因此,内核在用户栈上创建一个新的层,该层中将返回地址的值设置成用户定义的处理函数的地址,这样进程从内核返回弹出栈顶时就返回到用户定义的函数处,从函数返回再弹出栈顶时,才返回原先进入内核的地方。

执行默认操作是指执行Linux对每种信号规定了的默认操作函数。

信号相关系统调用说明

Linux内核分别为非实时信号和实时信号提供了两套系统调用,用来让用户进程发送信号、设置信号响应函数、挂起信号等操作。这些系统调用的功能说明如表3所示。

表3 与信号相关的系统调用功能说明
信号种类系统调用函数名功能说明
非实时信号sys_signal较早使用,已被sys_sigaction替代。
sys_kill向进程组发送一个信号。
sys_tkill向进程发送一个信号。
sys_tgkill向一个特定线程组中的进程发送信号。
sys_sigaction设置或改变信号的响应函数。
sys_sigsuspend将进程挂起等待一个信号。
sys_sigpending检查是否有挂起的信号。
sys_sigreturn当用户的信号响应函数结束时将自动调用此系统调用。将保存于信号堆栈中的进程上下文恢复至内核堆栈的上下文中。
sys_sigprocmask修改信号的集合。
sys_sigaltstack允许进程定义可替换的信号堆栈。
sys_rt_sigreturn与sys_sigreturn一样。
实时信号sys_rt_sigaction与sys_sigaction一样。
sys_rt_sigprocmask与sys_sigprocmask一样。
sys_rt_sigpending与sys_sigpending一样。
sys_rt_sigtimedwait等待一段时间后,向线程发送一个信号。
sys_rt_sigqueueinfo向线程发送一个信号。
sys_rt_sigsuspend与sys_sigsuspend一样。

信号相关数据结构

与信号相关的数据结构之间的关系如图1所示,下面按此图分别说明各个数据结构。


第4章用户进 04.gif
图1 与信号相关的数据结构之间的关系
(1)进程描述结构中的信号域

进程描述结构task_struct中有信号处理的数据成员,用来存储信号信息及处理信号。下面列出task_struct结构中与信号处理相关的成员:

struct task_struct{int sigpending ;
    ……
    struct signal_struct *signal;   //该进程待处理的全部信号
    struct sighand_struct *sighand;
    ……
    int exit_code,exit_signal;
    int pdeath_signal; //当父进程死时发送的信号
    ……
    spinlock_t sigmask_lock; //保护信号和阻塞
    struct signal_struct *sig;
 
    /*blocker是一个位图,存放该进程需要阻塞的信号掩码,如果某位为1,说明对应的信号正被阻塞。除了SIGSTOP和SIGKILL,其他信号都可被阻塞。被阻塞信号一直保留等待处理,直到进程解除阻塞*/
    sigset_t blocked; 
    struct sigpending pending;   
    //记录当进程在用户空间执行信号处理程序时的堆栈位置
    unsigned long sas_ss_sp; 
    size_t sas_ss_size; //堆栈的大小
    ……
}

(2)信号描述结构signal_struct

信号描述结构signal_struct用来跟踪挂起信号,还包括信号需要使用的一些进程信息,如:资源限制数组rlim、时间变量等。同一进程组的所有进程共享一个信号描述结构,含有进程组共享的信号挂起队列。

信号描述结构signal_struct没有它自己的锁,因为一个共享的信号结构总是暗示一个共享的信号处理结构,这样锁住信号处理结构(sighand_struct)也总是锁住信号结构。

信号描述结构signal_struct列出如下(在include/linux/sched.h中):

struct signal_struct {
	atomic_t		count;
	atomic_t		live;
 
	wait_queue_head_t	wait_chldexit;	/* 用于wait4() */
 
	/*当前线程组信号负载平衡目标*/
	struct task_struct	*curr_target;
 
	/* 共享的挂起信号 */
	struct sigpending	shared_pending;
 
	/* 线程组退出支持*/
	int			group_exit_code;
	/* 超负载:
	 * - 当->count 计数值等于notify_count 时,通知group_exit_task任务
	 * -在致命信号分发期间,除了group_exit_task外,所有任务被停止,group_exit_task处理该信号*/
	struct task_struct	*group_exit_task;
	int			notify_count;
 
	/* 支持线程组停止*/
	int			group_stop_count;
	unsigned int		flags; /* 信号标识SIGNAL_*  */
 
	/* POSIX.1b内部定时器*/
	struct list_head posix_timers;
 
	/*用于进程的ITIMER_REAL 实时定时器*/
	struct hrtimer real_timer;
	struct pid *leader_pid;
	ktime_t it_real_incr;
 
	/* 用于进程的ITIMER_PROF和ITIMER_VIRTUAL 定时器 */
	cputime_t it_prof_expires, it_virt_expires;
	cputime_t it_prof_incr, it_virt_incr;
 
	/* 工作控制ID*/
 
	/*不推荐使用pgrp和session域,而使用task_session_Xnr和task_pgrp_Xnr*/
 
	union {
		pid_t pgrp __deprecated;
		pid_t __pgrp;
	};
 
	struct pid *tty_old_pgrp;
 
	union {
		pid_t session __deprecated;
		pid_t __session;
	};
 
	/* 是否为会话组领导*/
	int leader;
 
	struct tty_struct *tty; /* 如果没有控制台,值为NULL*/
 
	/*可累积的资源计数器,用于组中死线程和该组创建的死孩子线程。活线程维护它们自己的计数器,并在__exit_signal中添加到除了组领导之外的成员中*/
	cputime_t utime, stime, cutime, cstime;
	cputime_t gtime;
	cputime_t cgtime;
	unsigned long nvcsw, nivcsw, cnvcsw, cnivcsw;
	unsigned long min_flt, maj_flt, cmin_flt, cmaj_flt;
	unsigned long inblock, oublock, cinblock, coublock;
 
	/*已调度CPU的累积时间(ns),用于组中的死线程,还包括僵死的组领导*/
	unsigned long long sum_sched_runtime;
 
	struct rlimit rlim[RLIM_NLIMITS];  /*资源限制*/
 
	struct list_head cpu_timers[3];
	……
}

(2)信号处理结构sighand_struct

每个进程含有信号处理结构sighand_struct,用来包含所有信号的响应函数。结构sighand_struct用数组存放这些函数,还添加了引用计数、自旋锁和等待队列用于管理该数组。结构sighand_struct列出如下(在include/linux/sched.h中):

struct sighand_struct {
	atomic_t		count;
	struct k_sigaction	action[_NSIG];  //平台所有信号的响应函数,x86-64平台上_NSIG为64
	spinlock_t		siglock;      //自旋锁
	wait_queue_head_t	signalfd_wqh;    //等待队列
};

(3)信号响应结构k_sigaction

信号响应用结构k_sigaction描述,它包含处理函数地址、标识等信息,其列出如下(在include/asm-x386/signal.h):

struct k_sigaction {
	struct sigaction sa;
};
struct sigaction {
	__sighandler_t sa_handler;    //信号处理程序的入口地址,为用户空间函数
	unsigned long sa_flags;      //信号如何处理标识,如:忽略信号、内核处理信号 
	__sigrestore_t sa_restorer;    //信号处理后的恢复函数
    /*每一位对应一个信号,位为1时,屏蔽该位对应的信号。在执行一个信号处理程序的过程中应该将该种信号自动屏蔽,以防同一处理程序的嵌套。*/
	sigset_t sa_mask;		
};

(4)挂起信号及队列结构

挂起信号用结构sigpending描述,内核通过共享挂起信号结构存放进程组的挂起信号,用私挂起信号结构存放特定进程的挂起信号。对于实时信号,结构sigpending用挂起信号队列list存放挂起的信号。

结构sigpending列出如下(在include/linux/signal.h中):

struct sigpending {
	struct list_head list;   /*挂起信号队列*/
	sigset_t signal;    //挂起信号的位掩码
};

结构sigqueue描述了实时挂起信号,其结构实例组成挂起信号队列,只有实时信号才会用到该结构。其列出如下:

struct sigqueue {
	struct list_head list;
	int flags;        //信号如何处理标识
	siginfo_t info;   //描述产生信号的事件
	struct user_struct *user;  //指向进程拥有者的用户数据结构
};

设置信号响应

在C库中,安装信号响应的函数为sigaction,其定义列出如下:

int sigaction(int signum, const struct sigaction *newact, struct *sigaction oldact);

内核有三个系统调用sys_signal,sys_sigaction和sys_rt_sigaction与之对应,根据函数sigaction传递的signum来确定选用哪个系统调用。这三个系统调用都是调用函数do_sigaction完成具体操作的。它们区别只是在参数上的处理有些不同,系统调用sys_signal是为了向后兼容而用的,功能上被sigaction替代了。这里只分析do_sigaction函数。

这三个系统调用允许用户给一个信号定义一个信号响应动作,如果没有定义一个动作,内核接收信号时执行默认的动作。

函数do_sigaction的功能是删除挂起的信号,存储旧的信号响应,设置新的信号响应。其中,参数sig是信号号码,参数act是新的信号动作定义,参数oact是输出参数,它输出与信号相关的以前的动作定义,函数列出如下(在kernel/signal.c中):

int do_sigaction(int sig, struct k_sigaction *act, struct k_sigaction *oact)
{
	struct task_struct *t = current;    /*得到当前进程的任务结构*/
	struct k_sigaction *k;
	sigset_t mask;
 
	/*_NSIG是信号最大数目64,函数sig_kernel_only 表示sig<64 && sig是SIGKILL或SIGSTOP,此两个信号的响应不允许更改*/
    if (!valid_signal(sig) || sig < 1 || (act && sig_kernel_only(sig)))
		return -EINVAL;
 
	k = &t->sighand->action[sig-1];   /*获取当前进程中信号对应的响应函数*/
 
	spin_lock_irq(&current->sighand->siglock);
	if (oact)
		*oact = *k;       /*返回旧的信号响应函数*/
 
	if (act) {/*删除已响应信号对应的掩码,防止信号递归*/
		sigdelsetmask(&act->sa.sa_mask,
			      sigmask(SIGKILL) | sigmask(SIGSTOP));  
		*k = *act;        /*设置新的信号响应函数*/
 
		if (__sig_ignored(t, sig)) {  /*如果为忽略信号,则删除信号*/
			sigemptyset(&mask);
			sigaddset(&mask, sig);
              /*从挂起信号集和队列中用掩码删除信号,如果发现信号,返回1*/ 
			rm_from_queue_full(&mask, &t->signal->shared_pending);
			do {
				rm_from_queue_full(&mask, &t->pending);
				t = next_thread(t);
			} while (t != current);
		}
	}
 
	spin_unlock_irq(&current->sighand->siglock);
	return 0;
}

信号分发

发送信号的系统调用有sys_kill,sys_tgkill,sys_tkill和sys_rt_sigqueueinfo。其中,sys_kill中的参数pid为0时,表示发送给当前进程所在进程组中所有的进程,pid为-1时则发送给系统中的所有进程。系统调用sys_tgkill发送信号到指定组ID和进程ID的进程,系统调用sys_tkill发送信号只给一个为ID的进程。系统调用sys_rt_sigqueueinfo发送的信号可传递附加信息,只发送给特定的进程。

分发给特定进程的信号,存放在进程的任务结构的私有挂起信号结构中,分发给进程组的信号,存放在组中各个进程的任务结构的共享挂起信号结构中。

下面仅分析系统调用sys_kill,其调用层次图如图3所示。


第4章用户进 03.gif
图3 函数sys_kill调用层次图

系统调用sys_kill列出如下(在kernel/signal.c中):

asmlinkage long sys_kill(int pid, int sig)
{
	struct siginfo info;
 
	info.si_signo = sig;
	info.si_errno = 0;
	info.si_code = SI_USER;
	info.si_pid = current->tgid;
	info.si_uid = current->uid;
 
	return kill_something_info(sig, &info, pid);
}

函数kill_something_info根据pid值的不同,调用不同函数发送信号。其列出如下:

static int kill_something_info(int sig, struct siginfo *info, int pid)
{
	if (!pid) {//pid为0时,表示发送给当前进程所在进程组中所有的进程
		return kill_pg_info(sig, info, process_group(current));
	} else if (pid == -1) {
    // pid为-1时发送给系统中的所有进程,
        //除了swapper(PID 0)、init(PID 1)和当前进程
		int retval = 0, count = 0;
		struct task_struct * p;
 
		read_lock(&tasklist_lock);
		for_each_process(p) {
			if (p->pid > 1 && p->tgid != current->tgid) {
				int err = group_send_sig_info(sig, info, p);
				++count;
				if (err != -EPERM)
					retval = err;
			}
		}
		read_unlock(&tasklist_lock);
		return count ? retval : -ESRCH;
	} else if (pid < 0) {//pid < -1时,发送给进程组中所有进程
		return kill_pg_info(sig, info, -pid);
	} else {//发送给pid进程
		return kill_proc_info(sig, info, pid);
	}
}

函数group_send_sig_info发送信号到进程组,函数分析如下:

int group_send_sig_info(int sig, struct siginfo *info, struct task_struct *p)
{
	unsigned long flags;
	int ret;
    //检查是否有发信号许可
	ret = check_kill_permission(sig, info, p);
	if (!ret && sig && p->sighand) {
		spin_lock_irqsave(&p->sighand->siglock, flags);
		ret = __group_send_sig_info(sig, info, p);//发送给进程组
		spin_unlock_irqrestore(&p->sighand->siglock, flags);
	}
 
	return ret;
}
 
static int __group_send_sig_info(int sig, struct siginfo *info,  struct task_struct *p)
{
	int ret = 0;
 
#ifdef CONFIG_SMP
	if (!spin_is_locked(&p->sighand->siglock))
		BUG();
#endif
  //处理stop/continue信号进程范围内的影响 
	handle_stop_signal(sig, p);
 
	if (((unsigned long)info > 2) && (info->si_code == SI_TIMER))
		//建立ret来表示我们访问了这个信号
		ret = info->si_sys_private;
 
	/*短路忽略的信号,如果目标进程的“信号向量表”中对所投递信号的响应是“忽略”(SIG_IGN),并且不在跟踪模式中,也没有加以屏蔽,就不用投递了。*/
	if (sig_ignored(p, sig))
		return ret;
 
	if (LEGACY_QUEUE(&p->signal->shared_pending, sig))
		//这是非实时信号并且我们已有一个排队 
		return ret;
  /*把信号放在共享的挂起队列里,我们总是对进程范围的信号使用共享队列,避免几个信号的竞争*/
	ret = send_signal(sig, info, p, &p->signal->shared_pending);
	if (unlikely(ret))
		return ret;
 
	__group_complete_signal(sig, p);
	return 0;
}

函数send_signal完成了信号投递工作,将发送的信号排队到signals中。函数send_signal分析如下(在kernel/signal.c中):

static int send_signal(int sig, struct siginfo *info, struct task_struct *t,
			struct sigpending *signals)
{
	struct sigqueue * q = NULL;
	int ret = 0;
    //内核内部的快速路径信号,是SIGSTOP或SIGKILL
	if ((unsigned long)info == 2)
		goto out_set;
     //如果由sigqueue发送的信号,实时信号必须被排队
	if (atomic_read(&t->user->sigpending) <
			t->rlim[RLIMIT_SIGPENDING].rlim_cur)
		q = kmem_cache_alloc(sigqueue_cachep, GFP_ATOMIC);//分配对象空间
 
	if (q) {//信号排队
		q->flags = 0;
		q->user = get_uid(t->user);
		atomic_inc(&q->user->sigpending);
    //加入到signals链表
		list_add_tail(&q->list, &signals->list);
		switch ((unsigned long) info) {
		case 0:
			q->info.si_signo = sig;
			q->info.si_errno = 0;
			q->info.si_code = SI_USER;
			q->info.si_pid = current->pid;
			q->info.si_uid = current->uid;
			break;
		case 1:
			q->info.si_signo = sig;
			q->info.si_errno = 0;
			q->info.si_code = SI_KERNEL;
			q->info.si_pid = 0;
			q->info.si_uid = 0;
			break;
		default:
			copy_siginfo(&q->info, info);
			break;
		}
	} else {
		if (sig >= SIGRTMIN && info && (unsigned long)info != 1
		   && info->si_code != SI_USER)
		//队列溢出,退出。如果信号是实时的,并且被使用非kill的用户发送,就可以退出。
					return -EAGAIN;
		if (((unsigned long)info > 1) && (info->si_code == SI_TIMER))
			ret = info->si_sys_private;
	}
 
out_set:
	sigaddset(&signals->signal, sig);//将接收位图中相应的标志位设置成1
	return ret;
}

函数void __group_complete_signal进行完成信号分发后的处理,它唤醒线程从队列中取下信号,如果信号是致命的,则将线程组停下来。其列出如下:

static void __group_complete_signal(int sig, struct task_struct *p)
{
	unsigned int mask;
	struct task_struct *t;
 
	/*不打搅僵死或已停止的任务,但SIGKILL将通过停止状态给一定的惩罚值*/
	mask = TASK_DEAD | TASK_ZOMBIE | TASK_TRACED;
	if (sig != SIGKILL)
		mask |= TASK_STOPPED;
   //如果进程p需要信号
	if (wants_signal(sig, p, mask))
		t = p;
	else if (thread_group_empty(p))//目标线程组是否为空
		/*线程组为空,仅仅有一个线程并且它不必被唤醒,它在再次运行之前将从队列取下非阻塞的信号。*/
		 return;
	else {//尝试查找一个合适的线程
 
		t = p->signal->curr_target;
		if (t == NULL)
			/* 在这个线程重启动平衡*/
			t = p->signal->curr_target = p;
		BUG_ON(t->tgid != p->tgid);
 
		while (!wants_signal(sig, t, mask)) {
			t = next_thread(t);
			if (t == p->signal->curr_target)
				//没有线程需要被唤醒,不久后任何合格的线程将看见信号在队列里
				return;
		}
		p->signal->curr_target = t;
	}
 
	//找到一个可杀死的线程,如果信号将是致命的,那就开始把整个组停下来*
	if (sig_fatal(p, sig) && !p->signal->group_exit &&
	    !sigismember(&t->real_blocked, sig) &&
	    (sig == SIGKILL || !(t->ptrace & PT_PTRACED))) {
		//这个信号对整个进程组是致命的?如果SIGQUIT、SIGABRT等
		if (!sig_kernel_coredump(sig)) {//非coredump信号
			/*开始一个进程组的退出并且唤醒每个组成员。这种方式下,在一个较慢线程致使的信号挂起后,我们没有使其他线程运行并且做一些事*/
			p->signal->group_exit = 1;
			p->signal->group_exit_code = sig;
			p->signal->group_stop_count = 0;
			t = p;
			do {
				sigaddset(&t->pending.signal, SIGKILL);//设置上SIGKILL
        /*告诉一个进程它有一个新的激活信号,唤醒进程t,状态为1即TASK_INTERRUPTIBLE*/
       	signal_wake_up(t, 1);
				t = next_thread(t);
			} while (t != p);
			return;
		}
        /*这里是core dump,我们让所有线程而不是一个选中的线程进入一个组停止,以至于直到它得到调度,从共享队列取出信号,并且做core dump之前没有事情发生。这比严格的需要有更多一点复杂性,但它保持了在core dump中信号状态从死状态起没有变化,在死亡状态中线程上有非阻塞的core-dump信号*/
 
		rm_from_queue(SIG_KERNEL_STOP_MASK, &t->pending);
		rm_from_queue(SIG_KERNEL_STOP_MASK, &p->signal->shared_pending);
		p->signal->group_stop_count = 0;
		p->signal->group_exit_task = t;
		t = p;
		do {
			p->signal->group_stop_count++;
			signal_wake_up(t, 0); //唤醒进程
			t = next_thread(t);
		} while (t != p);
		/*在信号分发致命信号期间,除了group_exit_task外的其他任务被停止,group_exit_task任务处理这个致命信号。*/
         wake_up_process(p->signal->group_exit_task);//唤醒group_exit_task
		return;
	}
 
	//信号已被放在共享挂起队列里,告诉选中线程唤醒并从队列上取下信号。
	signal_wake_up(t, sig == SIGKILL);
	return;
}

信号响应

在中断机制中,CPU在每条指令结束时都要检测中断请求是否存在,信号机制则是与软中断一样,当从系统调用、中断处理或异常处理返回到用户空间前、进程唤醒时检测信号的存在,并做出响应的。

信号响应因信号操作方式的不同而不同。分别说明如下:

  • 如果信号操作方式指定为默认(SIG_DEL)处理,则通常的操作为终止进程,即调用函数do_exit退出。少数信号在进程退出时需要进行内核转储(core dump),内核转储通过函数do_coredump实现。如果信号为可延缓类型,则将进程转为TASK_STOPPED状态。
  • 信号为SIGCHLD且指定操作方式为忽视(SIG_IGN)时,则释放僵尸进程的子进程。
  • 如果接收进程注册了信号响应函数,则调应函数handle_signal完成信号响应。

信号响应过程如图3所示。当用户空间将一个信号发送给另一个进程时,接收进程在内核空间的进程上下文设置信号值,信号成为挂起的信号。当接收进程从系统调用返回或中断返回时,接收进程在内核空间将调用函数do_notify_resume检查处理挂起的信号,该函数调用函数handle_signal处理信号,调用函数setup_rt_frame建立响应函数(它是用户注册的用户空间响应函数)的用户空间堆栈。进程通过堆栈返回到用户空间执行响应函数。当响应函数执行完成时,堆栈返回代码调用系统调用sys_sigreturn,恢复内核空间和用户空间堆栈,此系统调用完成时,返回到用户空间继续执行程序。


第4章用户进 02.gif
图3 信号响应过程
(1)系统调用返回触发信号响应

当系统调用返回时,线程会处理信号,系统调用返回时处理信号的代码列出如下(在arch/x86/kernel/entry_64.S中):

sysret_signal:
	TRACE_IRQS_ON
	ENABLE_INTERRUPTS(CLBR_NONE)
	testl $_TIF_DO_NOTIFY_MASK,%edx
	jz    1f
 
	/* 是一个信号*/
	/* edx为函数第三个参数thread_info_flags */
	leaq do_notify_resume(%rip),%rax       /*将函数do_notify_resume的指令地址存入%rax*/
	leaq -ARGOFFSET(%rsp),%rdi    # 函数第1个参数&pt_regs
	xorl %esi,%esi      #函数第2个参数 oldset
	call ptregscall_common     /*调试并运行call *%rax调用函数do_notify_resume */
1:	movl $_TIF_NEED_RESCHED,%edi
 
	DISABLE_INTERRUPTS(CLBR_NONE)
	TRACE_IRQS_OFF
	jmp int_with_check

函数do_notify_resume的调用层次图如图3所示,它根据线程信息标识进行相应的操作,如: 处理挂起的信号。


第4章用户进 01.gif
图3 函数do_notify_resume调用层次图

函数do_notify_resume列出如下(在arch/x86/kernel/sigal_64.c中):

void do_notify_resume(struct pt_regs *regs, void *unused,
		      __u32 thread_info_flags)
{
	/* Pending single-step? */
	if (thread_info_flags & _TIF_SINGLESTEP) {
		regs->flags |= X86_EFLAGS_TF;
		clear_thread_flag(TIF_SINGLESTEP);
	}
 
	/* 处理挂起的信号 */
	if (thread_info_flags & _TIF_SIGPENDING)
		do_signal(regs);
 
	if (thread_info_flags & _TIF_HRTICK_RESCHED)
		hrtick_resched();
}

函数do_signal用来处理非阻塞(未被屏蔽)的挂起信号,其参数regs是堆栈区域的地址,含有当前进程的用户模式寄存器内容。它根据信号操作方式的不同进行不同的信号响应操作。其列出如下:

static void do_signal(struct pt_regs *regs)
{
	struct k_sigaction ka;
	siginfo_t info;
	int signr;
	sigset_t *oldset;
 
	if (!user_mode(regs))   //如果regs不是用户模式的堆栈,直接返回
		return;
 
	if (current_thread_info()->status & TS_RESTORE_SIGMASK)
		oldset = &current->saved_sigmask;  /*存放将恢复的信号掩码*/
	else
		oldset = &current->blocked;  /*存放阻塞的信号掩码*/
     /*获取需要分发的信号*/
	signr = get_signal_to_deliver(&info, &ka, regs, NULL);
	if (signr > 0) {
		/* 在分发信号到用户空间之间,重打开watchpoints,如果在内核内部触发watchpoint,线程必须清除处理器寄存器*/
		if (current->thread.debugreg7)
			set_debugreg(current->thread.debugreg7, 7);
 
		/* 处理信号 */
		if (handle_signal(signr, &info, &ka, oldset, regs) == 0) {
			/*信号被成功处理:存储的sigmask将已存放在信号帧中,并将被信号返回恢复,因此,这里仅简单地清除TS_RESTORE_SIGMASK标识*/			
			current_thread_info()->status &= ~TS_RESTORE_SIGMASK;
		}
		return;
	}
     /*运行到这里,说明获取分发的信号失败*/
	/*省略系统调用返回的错误处理*/
	…..
 
	/*如果没有信号分发,仅将存储的sigmask放回*/
	if (current_thread_info()->status & TS_RESTORE_SIGMASK) {
		current_thread_info()->status &= ~TS_RESTORE_SIGMASK;
		sigprocmask(SIG_SETMASK, &current->saved_sigmask, NULL);
	}
}

(2)从进程上下文中获取信号并进行信号的缺省操作

函数get_signal_to_deliver从进程上下文中获取信号,如果是缺省操作方式,则执行信号的缺省操作,否则返回信号,让函数handle_signal去执行。其列出如下(在kernel/signal.c中):

int get_signal_to_deliver(siginfo_t *info, struct k_sigaction *return_ka,
			  struct pt_regs *regs, void *cookie)
{
	struct sighand_struct *sighand = current->sighand;
	struct signal_struct *signal = current->signal;
	int signr;
 
relock:
	try_to_freeze();
 
	spin_lock_irq(&sighand->siglock);
	/*在唤醒后,每个已停止的线程运行到这里,检查看是否应通知父线程,prepare_signal(SIGCONT)将CLD_ si_code编码进SIGNAL_CLD_MASK位*/
    /* SIGNAL_CLD_MASK为(SIGNAL_CLD_STOPPED|SIGNAL_CLD_CONTINUED)*/
	if (unlikely(signal->flags & SIGNAL_CLD_MASK)) { 
		int why = (signal->flags & SIGNAL_STOP_CONTINUED)
				? CLD_CONTINUED : CLD_STOPPED;    //表示孩子线程继续或停止
		signal->flags &= ~SIGNAL_CLD_MASK;
		spin_unlock_irq(&sighand->siglock);
 
		read_lock(&tasklist_lock);
		do_notify_parent_cldstop(current->group_leader, why);
		read_unlock(&tasklist_lock);
		goto relock;
	}
 
	for (;;) {
		struct k_sigaction *ka;
 
		if (unlikely(signal->group_stop_count > 0) &&
		    do_signal_stop(0))
			goto relock;
         /*从当前进程上下文中获取一个信号*/
		signr = dequeue_signal(current, &current->blocked, info);
		if (!signr)
			break; /* 将返回0 */
 
		if (signr != SIGKILL) {
			signr = ptrace_signal(signr, info, regs, cookie);
			if (!signr)
				continue;
		}
 
		ka = &sighand->action[signr-1];
		if (ka->sa.sa_handler == SIG_IGN) /*操作为忽略信号,不做任何事情*/
			continue;
		if (ka->sa.sa_handler != SIG_DFL) { /*操作为非缺省操作*/
			/*将用户定义的响应函数赋值返回,以便执行该函数*/
			*return_ka = *ka;
          /*SA_ONESHOT为RESETHAND的历史名,表示复位信号响应操作方式*/
			if (ka->sa.sa_flags & SA_ONESHOT)
				ka->sa.sa_handler = SIG_DFL;
 
			break; /*将返回非0的信号值*/
		}
 
		/*现在执行信号的缺省操作*/
		if (sig_kernel_ignore(signr)) /* 如果是忽略操作方式,则不做任何事情*/
			continue;
 
		/* 进程init忽略致命信号,标识为SIGNAL_UNKILLABLE*/
		/*如果signal_group_exit 返回值,表示除了signal ->group_exit_task之外,所有线程的挂起的信号为SIGKILL*/
		if (unlikely(signal->flags & SIGNAL_UNKILLABLE) &&
		    !signal_group_exit(signal))
			continue;            /*不做任何操作*/
 
		if (sig_kernel_stop(signr)) {  /*为停止类信号*/
			/*缺省操作是停止线程组中所有线程。工作控制信号在孤儿进程给不做任何操作,但SIGSTOP总是运行*/
			if (signr != SIGSTOP) {
				spin_unlock_irq(&sighand->siglock);
 
				/* 在此窗口期间,信号能被传递*/
				if (is_current_pgrp_orphaned())  //当前进程组为孤儿
					goto relock;
 
				spin_lock_irq(&sighand->siglock);
			}
 
			if (likely(do_signal_stop(signr))) {
				/*函数do_signal_stop已释放siglock.  */
				goto relock;
			}
 
			/*由于与SIGCONT或其他类似于此的信号竞争,本线程实际上没有停止*/
			continue;
		}
 
		spin_unlock_irq(&sighand->siglock);
 
		/*其他任何致命的原因,将导致内核转储(core dump)*/
		current->flags |= PF_SIGNALED;
 
		if (sig_kernel_coredump(signr)) {
			if (print_fatal_signals)
				print_fatal_signal(regs, signr);
			/*如果能内核转储,将杀死线程组中所有其他线程,并与它们的死亡行为进行同步。如果本线程与其他到这里的线程失去竞争,本线程将首先设置表示组退出的编码给signr,并且下面的do_group_exit将使用此值,而忽略传给它的值*/
			do_coredump((long)signr, signr, regs);
		}
 
		/*死亡信号,没有内核转储*/		 
		do_group_exit(signr);   /*线程组的所有线程退出,返回错误号存放在signr中返回*/
		/*不会运行到这里 */
	}
	spin_unlock_irq(&sighand->siglock);
	return signr;
}

(3)利用堆栈处理用户注册的响应函数

用户注册的信号响应函数在进程的用户空间,而信号在内核空间进行处理,内核态进程需要切换到用户态进程执行信号响应函数。Linux内核通过修改用户态堆栈的方法巧妙地实现了切换,而避免了内核态进程直接访问用户态进程、导致内核的超级权限可能被盗走。

修改堆栈的方法是:进程的堆栈原来存放它将返回父函数的"现场"信息,函数setup_rt_frame在内核中将该堆栈进行修改,在堆栈上创建信号帧,并将内核态进程的各种信息拷贝到用户态的信号帧中,其中,还将信号响应例程的地址作为信号帧的指令寄存器rip的下一条指令地址。这样,当函数setup_rt_frame返回时进行出栈操作,将不再返回父函数,而执行信号响应例程。

执行完信号响应例程后,还需要返回到内核态进程继续执行函数handle_signal,即相当于从子函数setup_rt_frame返回到父函数。如何从用户态的信号响应函数回到内核态的函数呢 '这通过系统调用sys_sigreturn完成。用户态的信号响应例程返回时将自动调用系统调用sys_sigreturn,信号响应函数执行完时,返回栈顶地址,该地址指向帧的pretcode字段所引用的ka->sa.sa_restorer,而sa_restorer指向系统调用sys_sigreturn。

系统调用sys_sigreturn将来自信号帧的sc字段(信号上下文)的进程内核态"现场"(即各寄存器值)拷贝到内核态堆栈中,并从用户态堆栈中删除信号帧,从而可以回到父函数handle_signal继续执行。

函数handle_signal执行用户注册的信号响应函数,并屏蔽掉已响应的信号。其列出如下(在arch/x86/kernel/signal_64.c中):

static int handle_signal(unsigned long sig, siginfo_t *info, struct k_sigaction *ka,
	      sigset_t *oldset, struct pt_regs *regs)
{
	int ret;
 
	/*省略系统调用返回的错误处理*/
	…..
	/*如果由于调试器用TIF_FORCED_TF设置了TF,就清除TF标识,以便在信号上下文中寄存器信息是正确的*/
	if (unlikely(regs->flags & X86_EFLAGS_TF) &&
	    likely(test_and_clear_thread_flag(TIF_FORCED_TF)))
		regs->flags &= ~X86_EFLAGS_TF;
    ……
	ret = setup_rt_frame(sig, ka, info, oldset, regs);     /*在进程用户态堆栈中建立信号帧*/
 
	if (ret == 0) {   //建立成功
		/*下面设置对段寄存器没有影响,仅影响宏的行为。复位它到正常值 */
		set_fs(USER_DS);  //设置段限制为用户数据段
 
		/*为函数入口清除ABI(应用程序二进制接口)方向标识*/
		regs->flags &= ~X86_EFLAGS_DF;
 
		/*当进入信号响应例程时清除TF,需要通知对它进行单步跟踪的跟踪器。*/
		regs->flags &= ~X86_EFLAGS_TF;
		if (test_thread_flag(TIF_SINGLESTEP))
			ptrace_notify(SIGTRAP);
 
		spin_lock_irq(&current->sighand->siglock);
         /*执行“或”操作后,结果存放在current->blocked中,用于屏蔽掉已响应的信号*/
		sigorsets(&current->blocked,&current->blocked,&ka->sa.sa_mask);
		if (!(ka->sa.sa_flags & SA_NODEFER))
			sigaddset(&current->blocked,sig);
		recalc_sigpending();
		spin_unlock_irq(&current->sighand->siglock);
	}
 
	return ret;
}

信号帧存放了信号处理所需要的信息,并确保正确返回到函数handle_signal。为了在用户态执行信号响应函数并能返回内核,函数handle_signal调用函数setup_rt_frame在进程用户态堆栈中建立信号帧。在建立信号帧时,函数setup_rt_frame将信号帧中的指令寄存器rip的值设置信号响应的地址,因此,该函数返回时,堆栈中的信号帧弹出,CPU执行信号帧中rip的值,即执行信号响应例程。这样,通过堆栈完成了从内核态切换到用户态信号响应例程的过程。

信号帧用结构sigframe描述,其列出如下(arch/x86/kernel/sigframe.h中):

struct rt_sigframe {
	char __user *pretcode;  //信号响应函数的返回地址,
	struct ucontext uc;     //用户态上下文
	struct siginfo info;
};

用户态上下文结构ucontext列出如下:

struct ucontext {
	unsigned long	  uc_flags;
	struct ucontext  *uc_link;
	stack_t		  uc_stack;    /*信号任务下文堆栈*/
	struct sigcontext uc_mcontext;   /*信号上下文*/
	sigset_t	  uc_sigmask;	   /*用于扩展的上一次信号掩码*/
};

信号任务栈结构sigaltstack描述了信号所在进程的堆栈的地址与大小信息,其列出如下:

typedef struct sigaltstack {
	void __user *ss_sp;
	int ss_flags;
	size_t ss_size;
} stack_t;

结构sigcontext为信号上下文,存入了切换到内核态前用户态进程的"现场",包括各个寄存器值和被阻塞的信号。寄存器的值从变量current的内核堆栈中拷贝。其列出如下:

struct sigcontext {
	unsigned short gs, __gsh;
	unsigned short fs, __fsh;
	unsigned short es, __esh;
	unsigned short ds, __dsh;
	unsigned long di;
	unsigned long si;
	unsigned long bp;
	unsigned long sp;
	unsigned long bx;
	unsigned long dx;
	unsigned long cx;
	unsigned long ax;
	unsigned long trapno;
	unsigned long err;
	unsigned long ip;
	unsigned short cs, __csh;
	unsigned long flags;
	unsigned long sp_at_signal;
	unsigned short ss, __ssh;
	struct _fpstate __user *fpstate;    //用来存放用户态进程的浮点寄存器内容
	unsigned long oldmask; 
	unsigned long cr2;
};

函数setup_rt_frame在进程用户态堆栈中建立信号帧,并将内核中信号的各种信息拷贝到用户态堆栈的信号帧中。其列出如下:

static int setup_rt_frame(int sig, struct k_sigaction *ka, siginfo_t *info,
			   sigset_t *set, struct pt_regs * regs)  /*用户态的寄存器保存在变量regs中*/
{
	struct rt_sigframe __user *frame;
	struct _fpstate __user *fp = NULL; 
	int err = 0;
	struct task_struct *me = current;
 
	if (used_math()) {
          /*得到用户态栈的浮点状态帧fp的起始地址*/
         /*栈朝低地址方向延伸,因此,起始地址=栈顶- sizeof(struct _fpstate)*/
		fp = get_stack(ka, regs, sizeof(struct _fpstate)); 
         /*得到用户态栈的信号帧的起始地址,16字节对齐*/
		frame = (void __user *)round_down(
			(unsigned long)fp - sizeof(struct rt_sigframe), 16) - 8;
          /*检查访问是否在合法的地址空间*/
		if (!access_ok(VERIFY_WRITE, fp, sizeof(struct _fpstate)))
			goto give_sigsegv;
 
		if (save_i387(fp) < 0) 
			err |= -1; 
	} else
		frame = get_stack(ka, regs, sizeof(struct rt_sigframe)) - 8;
     /*检查访问是否在合法的地址空间*/
	if (!access_ok(VERIFY_WRITE, frame, sizeof(*frame)))
		goto give_sigsegv;
 
	if (ka->sa.sa_flags & SA_SIGINFO) {   /*将info拷贝到用户空间的frame->info中*/
		err |= copy_siginfo_to_user(&frame->info, info);
		if (err)
			goto give_sigsegv;
	}
 
	/* 创建用户态上下文ucontext  */
	err |= __put_user(0, &frame->uc.uc_flags);
	err |= __put_user(0, &frame->uc.uc_link);
	err |= __put_user(me->sas_ss_sp, &frame->uc.uc_stack.ss_sp);
	err |= __put_user(sas_ss_flags(regs->sp),
			  &frame->uc.uc_stack.ss_flags);
	err |= __put_user(me->sas_ss_size, &frame->uc.uc_stack.ss_size);
	err |= setup_sigcontext(&frame->uc.uc_mcontext, regs, set->sig[0], me);
	err |= __put_user(fp, &frame->uc.uc_mcontext.fpstate);
	if (sizeof(*set) == 16) { 
		__put_user(set->sig[0], &frame->uc.uc_sigmask.sig[0]);
		__put_user(set->sig[1], &frame->uc.uc_sigmask.sig[1]); 
	} else
		err |= __copy_to_user(&frame->uc.uc_sigmask, set, sizeof(*set));
 
	/* 建立从用户空间返回的代码。如果用户空间提供了一个stub,则使用它*/
	/* x86-64应该总使用SA_RESTORER. */
	if (ka->sa.sa_flags & SA_RESTORER) { 
		err |= __put_user(ka->sa.sa_restorer, &frame->pretcode);
	} else {
		/* 能使用vstub */
		goto give_sigsegv; 
	}
 
	if (err)
		goto give_sigsegv;    /*进行错误处理*/
 
	/* 建立信号处理例程的寄存器*/
	regs->di = sig;     /*sig为响应信号的值*/
	/* 避免信号处理例程没有原类型下被声明*/ 
	regs->ax = 0;
 
	/*对于非SA_SIGINFO例程,下面代码也工作,因为它们期望得到在堆栈上信号数值的后面下一个参数*/
	regs->si = (unsigned long)&frame->info;
	regs->dx = (unsigned long)&frame->uc;
	regs->ip = (unsigned long) ka->sa.sa_handler;     /*信号响应例程的地址*/
 
	regs->sp = (unsigned long)frame;
 
	/* 在64位模式,建立CS寄存器运行信号处理例程,即使该例程正中断32位代码*/
	regs->cs = __USER_CS;
 
	return 0;
 
give_sigsegv:
	force_sigsegv(sig, current);  /*强制向当前进程的发信号SIGSEGV(无效的内存引用)*/
	return -EFAULT;
}



      评论
      添加红包

      请填写红包祝福语或标题

      红包个数最小为10个

      红包金额最低5元

      当前余额3.43前往充值 >
      需支付:10.00
      成就一亿技术人!
      领取后你会自动成为博主和红包主的粉丝 规则
      hope_wisdom
      发出的红包
      实付
      使用余额支付
      点击重新获取
      扫码支付
      钱包余额 0

      抵扣说明:

      1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
      2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

      余额充值