進程資料結構(進程描述符)
直接查看下源碼(這裡是0.11版本的核心)中的檔案/include/linux/sched.h,Linux的進程式控制制塊為一個由結構task_struct所定義的資料結構,這個結構就在上面的sched.h中。這個檔案中有一行代碼:
extern struct task_struct *task[NR_TASKS];
為記錄指向各PCB的指標,指標數組定義於/kernel/sched.c中,原定義為
struct task_struct * task[NR_TASKS] = {&(init_task.task), };
NR_TASKS為最多可以同時啟動並執行進程的數目,在sched.h中定義(#define NR_TASKS 64)。
task_struct結構的內容如下(為了寫注釋有改動,像unsigned long start_code,end_code,end_data,brk,start_stack;會展開成多行寫):
struct task_struct {/* these are hardcoded - don't touch */ long state; /* -1 unrunnable, 0 runnable(就緒), >0 stopped */ long counter; //任務已耗用時間片 long priority; long signal; //訊號。每個位元代表一種訊號。 struct sigaction sigaction[32]; long blocked; /* bitmap of masked signals 進程訊號屏蔽碼*//* various fields */ int exit_code; //退出碼,父進程會取得 unsigned long start_code; //程式碼片段地址 unsigned long end_code; //代碼長度(位元組數) unsigned long end_data; //代碼長度+資料長度(位元組數) unsigned long brk; //總長度 unsigned long start_stack; //堆棧段地址 long pid,father,pgrp,session,leader; //進程、父進程、進程組、會話、會話首進程號 unsigned short uid,euid,suid; //使用者id、有效使用者id、儲存的使用者id unsigned short gid,egid,sgid; //組id、有效組id,儲存的組id long alarm; //鬧鐘定時 long utime,stime,cutime,cstime,start_time; //使用者態(系統態)已耗用時間、子進程使用者態(系統太) unsigned short used_math; //是否使用了副處理器/* file system info */ int tty; //進程使用tty的子裝置號,-1標識沒有使用 unsigned short umask; //檔案建立屏蔽字 struct m_inode * pwd; //當前工作目錄i節點結構 struct m_inode * root; //根節點i節點結構 struct m_inode * executable; //執行檔案i節點結構 unsigned long close_on_exec; //執行時關閉檔案控制代碼位元影像標誌 struct file * filp[NR_OPEN]; //進程使用的檔案表結構/* ldt for this task 0 - zero 1 - cs 2 - ds&ss */ struct desc_struct ldt[3]; //本任務的局部表描述符。0-空,1-程式碼片段cs,2-資料區段和堆棧段ds&ss/* tss for this task */ struct tss_struct tss; //進程的任務狀態段資訊結構};
進程部分屬性說明
1.進程ID和父進程ID
2.實際使用者ID、實際組ID、有效使用者ID、有效組ID、儲存的設定使用者ID、儲存的設定組ID、附加組、檔案模式建立屏蔽字
3.進程組ID、交談群組ID、控制終端
4.tms_utime、tms_stime、tms_cutime、tms_ustime
5.訊號屏蔽字、訊號集、鬧鐘
6.檔案鎖(記錄鎖)
7.環境
8.運行狀態
一、進程ID
每個進程都是有ID的,線程也有,但是系統中的進程ID號是有限的,而且可能同時啟動並執行進程的數目也是有限制的,所以進程ID號也是一種資源。殭屍是進程在進程結束後父進程沒有及時的回收終止子進程的有關資訊情況下產生的,這些殭屍進程會佔用進程ID。
二、實際使用者ID、實際組ID、有效使用者ID、有效組ID、儲存的設定使用者ID、儲存的設定組ID、附加組ID
這裡分三個問題來講:
1.許可權問題一直是很複雜的。先簡單介紹下幾個概念:
1)實際使用者ID和實際組ID
這兩個ID標識我們究竟是誰,也就是以什麼使用者登入的,在一個登入工作階段間這些值並不改變。
2)有效使用者ID和有效組ID
這兩個ID決定了我們檔案的存取權限,下面會具體說明這個ID的作用
3)儲存的設定使用者ID和儲存的設定組ID
其實為有效使用者ID和有效組ID的副本
4)檔案的存取權限
檔案的存取權限分為三類,所有者,組,其他,每類都有讀、寫、可執行三項。
可以用 setuid() setgid() 來設定實際使用者(組)和有效使用者(組)ID
可以用 chmod()改變檔案的許可權,chown改變檔案的所有者和所有組
2.進程每次開啟、建立或者刪除一個檔案的時候,核心就進行檔案存取權限測試,測試分四步:
1)若進程的有效使用者ID是0(root),則允許訪問。
2)若進程的有效使用者ID等於檔案的所有者ID(也就是進程擁有此檔案),:那麼若所有者適當的存取權限被設定,則允許訪問,否則拒絕訪問。
3)若進程的有效組或進程的附加組ID之一等於檔案的組ID,那麼若所有者適當的存取權限被設定,則允許訪問,否則拒絕訪問。
4)若其他使用者適當的存取權限被設定,則允許訪問,否則拒絕
按照順序執行四步,如果進程擁有此檔案(2),則按照使用者存取權限批註或拒絕該進程對檔案的訪問,不查看組存取權限。下面也類似,也就是說並不是一定四步都執行完,可能從第二步開始就break了。
3.關於檔案模式建立屏蔽字(umask)
每個進程建立一個新的檔案或者目錄的時候,都需要一個檔案模式建立屏蔽字,這個屏蔽字是預設的建立的檔案或者目錄的許可權。系統的大多數使用者並不處理,通常在登入的時候由shell的開機檔案設定一次,然後從不改變。可用umask命令直接查看,也可以直接用umask 777直接更改。注意,這裡是屏蔽(u意義),如果設定成777,那麼一個目錄的許可權為d---------。
一般而言,在設計程式時候,我們總是試圖使用最小特權(least privilege)模型。依照此模型,我們的程式應該具有完成給定任務所需的最小特權,這減少了安全性受到損害的可能性。
三、進程組ID、交談群組ID、控制終端
在網上搜了下,好多也都是apue上的概念,我理解,進程組,交談群組的概念出現是為了linux方便管理的,主要是為了作業控制的。最常見的就是在終端上發送一個中斷訊號(crtl+c),這個中斷訊號會發送到控制終端的前台進程組。簡單說,進程組是一個或者多個進程的集合,而會話(session)是一個或者多個進程組的集合。理解這兩個概念有以下幾點:
1)進程組中的進程通常與同一作業相關聯,可以接收來自同一終端的各種訊號,每個進程組有一個唯一的進程組ID;每個進程組可以有一個組長進程,組長進程的ID等於進程組的ID;
2)會話是一個或者多個進程組的集合。通常,shell的管道將幾個進程變成一組的(這也是為什麼說進程組中的進程與同一作業相關)。
3)一個會話可以有一個控制終端。這通常是登入到其上的終端裝置或偽終端裝置。
4)一個會話中的進程組可以被分成一個前台進程組(foreground process group)以及一個或幾個後台進程組(background process group)。
5)如果一個會話有一個控制終端,則它有一個前台進程組,會話中的其他進程組為後台進程組。
6)無論何時鍵入終端的中斷鍵(delete 或者ctrl + c),就會將中斷訊號發送給前台進程組的所有進程。
7)無論何時鍵入終端退出鍵(ctrl + \),就會將退出訊號發送給前台進程組的所有進程。
8)建立與終端串連的會話首進程被稱為控制進程。
對於這個問題,還是舉例簡單的例子來說明下,還是在red hat上搞的,用的為bourne-again shell。
ps中的TPGID:ID of the foreground process group on the tty (terminal),that the process is connected to, or -1 if the process is not connected to a tty.
運行以下命令
ps -o pid,ppid,pgrp,session,tpgid,comm
輸出:
PID PPID PGRP SESS TPGID COMMAND
3395 3393 3395 3395 3469 bash
3469 3395 3469 3395 3469 ps
上面說的tpgid為終端的前台進程組,其實也可以理解為一個會話的前台進程組,因為一個會話可以有一個終端,如果有的話,應該是一一對應的關係。可以看出在linux上(其他的作業系統可能不一樣,linux是支援作業的,如果不支援作業可能不一樣),bash為後台進程組的,而ps進程為前台進程組(tpgid可以看出),且這個會話(終端)的前台進程組只有一個進程為ps(前台進程組id為ps所在進程組id,且ps為這個組的組長)。還可以看出ps進程的父進程為3495即為bash,那麼可知bash在運行一個進程的時候都是用fork命令啟動一個子進程,在用exec命令來替換進程的。
ps -o pid,ppid,pgrp,session,tpgid,comm | cat
輸出:
PID PPID PGRP SESS TPGID COMMAND
3395 3393 3395 3395 3501 bash
3501 3395 3501 3395 3501 ps
3502 3395 3501 3395 3501 cat
可以看出,用管道串連在一起的兩個進程ps和cat在一個進程組裡面(3501),且為前台進程組,而且這兩個進程的父進程都為3395(bash)。
ps -o pid,ppid,pgrp,session,tpgid,comm | cat &
先輸出:
[1] 3508
在輸出:
PID PPID PGRP SESS TPGID COMMAND
3395 3393 3395 3395 3395 bash
3507 3395 3507 3395 3395 ps
3508 3395 3507 3395 3395 cat
先出的[1] 3508,為作業號號,然後輸出的為進程的輸出結果,在按一下空格會出現[1]+ Done ps -o pid,ppid,pgrp,session,tpgid,comm | cat為作業1完成的提示。這裡的ps和cat為一個後台進程組(3507),tpgid指示的前台進程組為3395,為使用者登入的bash。
四、tms_utime、tms_stime、tms_cutime、tms_cstime
注意:在fork後子進程中,這幾個時間參數被置為0。
任何一進程都可以調用times函數來獲得他自己,以及已經終止的子進程的牆上時鐘時間、使用者CPU時間和系統CPU時間。這幾個概念在前面的部落格中(http://blog.csdn.net/ysu108/article/details/7476810)提到過,不再重複。這裡說明下這個times函數,用man 2 times可以得到這個函數的說明:times() stores the current process times in the struct tms that buf points to. The struct tms is as defined in <sys/times.h>:
struct tms {
clock_t tms_utime; /* user time */
clock_t tms_stime; /* system time */
clock_t tms_cutime; /* user time of children */
clock_t tms_cstime; /* system time of children */
};
The tms_utime field contains the CPU time spent executing instructions of the calling process. The tms_stime field contains the CPU time spent in the system while executing tasks on behalf of the calling process. The tms_cutime field contains the sum of the tms_utime and tms_cutime values for all waited-for terminated children. The tms_cstime field contains the sum of the tms_stime and tms_cstime values for all waited-for terminated children.All times reported are in clock ticks.
RETURN VALUE
times() returns the number of clock ticks that have elapsed since an arbitrary point in the past.
需要注意的有:這個結構沒有包含牆上時鐘時間的任何測量值,函數返回牆上時鐘時間作為其函數值,此值為相對過去的某一時刻測量的,所以不能用其絕對值,而必須使用其相對值。有系統命令time,可以直接查看進程啟動並執行時間。
五、訊號屏蔽字、訊號集、鬧鐘
注意:在子進程中未處理的訊號集設定為空白集、未處理的鬧鐘被清除
1)訊號概念
訊號提供了一種處理非同步事件的方法,產生訊號的事件對於進程而言是隨機的出現的,進程不能簡單地測試一個變數來判斷是否產生了一個訊號,而是必須告訴核心“在此訊號出現時請執行下列操作”,可以要求核心在某個訊號出現時候按照下列三種方式之一來處理:1.忽略此訊號,但是有兩個訊號不能忽略SIGKILL和SIGSTOP,這種訊號不能忽略的原因是他們向超級使用者提供了是進程終止或者停止的可靠方法。2.捕捉訊號,為了做到這一點要通知核心在某種訊號發生時候調用一個使用者函數。3.執行預設動作,大多數的訊號預設動作是終止該進程。
每個進程都有一個訊號屏蔽字(signal mask),它規定了當前要阻塞遞送到該進程的訊號集。對於每種可能的訊號,該屏蔽子都有一位與之對應。對於某種訊號,若其對應位已設定,則它當前是被阻塞的。進程可以調用sigprocmask來檢測和更改當前訊號屏蔽字。訊號數量可能會超過整型包含的二進位位元(這種用每一位來表示一個特徵的用法在linux中相當普遍),因此POSIX定義了一個新資料類型sigset_t,用於儲存一個訊號集合。
2)訊號與進程
1.當一個程式啟動的時候,所有訊號狀態都是系統預設或忽略,通常所有訊號都被設定為她們的預設動作,除非調用exec函數,確切的講,exec函數將原先設定為要捕捉訊號都改為他們的預設動作,其他訊號的狀態則不改變,對於一個進程原先要捕捉的訊號,當執行一個程式後,自然不能在捕捉它了,因為訊號捕捉函數的地址很可能在所執行的新程式中已無意義。一個具體的例子就是當shell在後台執行一個進程的時候,shell自動將進程對中斷和退出訊號的處理方式設定為忽略,於是當使用者按中斷鍵的時候不會影響到後台進程。
2.fork的時候
前面其實都是鋪墊,當一個進程調用fork的時候,其子進程繼承父進程的訊號處理方式,因為子進程在開始時複製了父進程的儲存映像,所以訊號捕捉函數的地址在子進程中是有意義的。
3.alarm
使用alarm函數可以設定一個計時器,在將來某個指定的時間該計時器會逾時。當計時器逾時時,產生SIGALRM訊號。如果不忽略或不捕捉此訊號,則其預設動作是終止調用該alarm函數的進程。每個進程只能有一個鬧鐘時鐘。
六、檔案鎖(記錄鎖)
記錄鎖(record locking)的功能是:當一個進程正在讀或修改檔案的某個部分時,它可以阻止其他進程修改同一檔案區。對於UNIX系統而言,“記錄”這個詞是一種誤用,因為UNIX系統核心根本沒有使用檔案記錄這種概念。更適合的術語是位元組範圍鎖(byte-range locking),因為它鎖定的只是檔案中的一個地區(也可能是整個檔案)。
記錄鎖不僅可以方式多個進程多於同一個檔案做修改,還可以建立單一的進程(類似單例模式),不能啟動兩個同樣的進程。當進程建立的時候先去鎖定一個目標檔案(一般檔案名稱為進程的名字),當第二個相同進程建立的時候也要去鎖定同樣的檔案,因為第一個進程已經建立,鎖定失敗,進程退出。
七、環境
這個部分應該是進程啟動並執行時候的程式映像中高地址部分的內容,和命令列參數一起。有HOME(使用者工作的目錄)、PATH、SHELL、USER、LOGNAME。
八、運行狀態
Linux 的進程有五種狀態,同樣在sched.h標頭檔中定義
在檔案的前面定義了進程的運行狀態
#define TASK_RUNNING 0 可運行狀態,相當於進程三種狀態的執行和就緒狀態
#define TASK_INTERRUPTIBLE 1 中斷等待狀態。處於這種狀態喚醒的原因可能是訊號或定時中斷,或者I\O就緒
#define TASK_UNINTERRUPTIBLE 2 不可中斷等待狀態,主要是等待I\O
#define TASK_ZOMBIE 3 僵死狀態,進程已經結束已經釋放除了PCB以外的部分系統資源,等待父進程wait()讀取結束狀態
#define TASK_STOPPED 4 進程已經停止
注意:這是0.11的核心,在1.0的核心以上就多了一種狀態,在1.0核心的sched.h中有定義
#define TASK_SWAPPING 5 交換狀態,進程的頁面也可以從記憶體轉換到外存,以空出記憶體空間
參考:
apue
linux 核心0.11完全註解