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定義的信號說明
信號 信號值 處理動作 發出信號的原因
SIGHUP 1 A 終端掛起或者控制進程終止
SIGINT 2 A 鍵盤中斷(如break鍵被按下)
SIGQUIT 3 C 鍵盤的退出鍵被按下
SIGILL 4 C 非法指令
SIGABRT 6 C 由abort(3)發出的退出指令
SIGFPE 8 C 浮點異常
SIGKILL 9 AEF Kill信號
SIGSEGV 11 C 無效的內存引用
SIGPIPE 13 A 管道破裂: 寫一個沒有讀端口的管道
SIGALRM 14 A 由alarm(2)發出的信號
SIGTERM 15 A 終止信號
SIGUSR1 30,10,16 A 用戶自定義信號1
SIGUSR2 31,12,17 A 用戶自定義信號2
SIGCHLD 20,17,18 B 子進程結束信號
SIGCONT 19,18,25   進程繼續(曾被停止的進程)
SIGSTOP 17,19,23 DEF 終止進程
SIGTSTP 18,20,24 D 控制終端(tty)上按下停止鍵
SIGTTIN 21,21,26 D 後臺進程企圖從控制終端讀
SIGTTOU 22,22,27 D 後臺進程企圖從控制終端
備註:
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;
}



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章