1. 程式人生 > >進程的執行狀態

進程的執行狀態

create pid lur tro git 普通 word lose 內存空間

既然進程可以並發執行,那麽他們是在程序運行是什麽狀態呢?不同的系統可能會有不同的狀態,以下為大多數情況:

進程一般存在三種情況:

(1)就緒狀態:我已經準備好,給我處理器,我就可以執行,這時的進程狀態就是就緒狀態;

(2)執行狀態:我已經獲得資源,並且正在工作中,這時的進程狀態就是執行狀態;

(3)阻塞狀態:我剛剛正在工作,突然“斷電”了,我被迫停止,這時的進程狀態就是阻塞狀態。

但是,不是說我處於一種狀態就會不變,正在執行的總會做完,被迫停止的也可以重新開始。

關於三種狀態,舉個栗子:

在食堂買飯,有人已經買上飯菜了坐了正在吃,有人正在排隊買菜,有人買好了但是沒有座位,正在等待。

三、進程的控制

進程控制主要是負責進程的創建與撤銷,進程狀態之間的切換以及進程之間的通信等。當然這也是系統的基本功能,在內核中的相應程序中完成。

但是什麽是操作系統內核?

操作系統內核是指擴充計算機硬件的第一層,廣泛采用層次式結構,通常將一些與硬件密切相關的模塊,比如中斷處理程序,設備驅動程序,存儲器管理等安排在緊靠硬件的軟件層,並且讓他們常住在內存中,施以保護。內核在實現實現其基本功能時基本常采用原語操作。

什麽是原語?

可以簡單的看作是命令。主要介紹一下進程控制語言,主要包括進程的創建與撤銷、阻塞與喚醒、刮起與激活等六個原語。

(1)進程創建原語:進程通過調用進程創建原語來創建一個子進程,步驟為:申請空閑的PCB(進程控制模塊),為子進程獲得新的標識-->為子進程分配諸如內存空間的資源-->初始化進程控制模塊-->將新進程插入到PCB的就緒隊列中。創建ok。

(2)進程撤銷原語:撤銷原語在撤銷進程時,連同該進程的子孫進程一同撤銷。步驟為:根據被撤銷的進程的標識符從PCB檢索表中找到該進程的PCB,並獲得該進程的狀態-->若進程處於執行狀態,立即終止其執行,並且將其邏輯值重置;若進程不是執行狀態,直接將其從狀態隊列中刪去-->遞歸的處理該進程的子孫進程-->撤銷進程時,將所有資源歸位,註銷其資源描述清單-->釋放該進程的PCB。撤銷ok,但是如果其邏輯值為真,則會轉入進程調度程序。

(3)進程阻塞原語:當進程請求某個事件尚未出現時,進行步驟:終止調用者自身的執行-->該進程調用進程阻塞原語使其從執行狀態變為阻塞狀態-->把調用者進程的PCB插入到相應的阻塞隊列-->然後轉入進程調度程序。

(4)進程喚醒原語:執行的進程釋放某資源之後,調用進程喚醒原語將因等待該資源而阻塞的進程喚醒成就緒狀態。進行步驟:找出相應被喚醒的進程的內部標識-->把該標識從阻塞隊列中移去-->重設該狀態為就緒-->將該進程插入到就緒隊列中去。

Linux信息項目(The Linux Information)把進程定義為“程序的一個執行(即,運行)實例”。所以,要定義進程我們先要定義什麽是程序。再次根據Linux信息項目裏的定義,“程序是內存裏的一個可執行文件。”

所以,我們知道進程是正在運行的程序的一部分。這是否意味著進程一定是在運行中的?不一定。

進程狀態

為了弄明白正在運行的進程是什麽意思,我們需要知道進程的不同狀態。一個進程可以有幾個狀態(在Linux內核裏,進程有時候也叫做任務)。

下面的狀態在 fs/proc/array.c 文件裏定義:

C
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 /* * The task state array is a strange "bitmap" of * reasons to sleep. Thus "running" is zero, and * you can test for combinations of others with * simple bit tests. */ static const char * const task_state_array[] = { "R (running)", /* 0 */ "S (sleeping)", /* 1 */ "D (disk sleep)", /* 2 */ "T (stopped)", /* 4 */ "t (tracing stop)", /* 8 */ "X (dead)", /* 16 */ "Z (zombie)", /* 32 */ };

運行狀態(running)並不意味著進程一定在運行中,它表明進程要麽是在運行中要麽在運行隊列裏。睡眠狀態(sleeping)意味著進程在等待事件完成(這裏的睡眠有時候也叫做可中斷睡眠(interruptible sleep))。磁盤休眠狀態(Disk sleep)有時候也叫不可中斷睡眠狀態(uninterruptible sleep),在這個狀態的進程通常會等待IO的結束。

可以通過發送 SIGSTOP 信號給進程來停止(T)進程。這個被暫停的進程可以通過發送 SIGCONT 信號讓進程繼續運行。

例如,可以用下面的方法來停止或繼續運行進程:

Shell
1 2 kill -SIGSTOP <pid> kill -SIGCONT <pid>

可以使用gdb終止進程來實現跟蹤終止狀態。如果我沒有記錯的話,這個狀態和終止狀態基本上是一樣的。

死亡狀態是內核運行 kernel/exit.c 裏的 do_exit() 函數返回的狀態。這個狀態只是一個返回狀態,你不會在任務列表裏看到這個狀態。

僵死狀態(Zombies)是一個比較特殊的狀態。有些人認為這個狀態是在父進程死亡而子進程存活時產生的。實際上不是這樣的。父進程可能已經死了但子進程依然存活著,那個子進程的父進程將會成為init進程,pid 1。當進程退出並且父進程(使用wait()系統調用)沒有讀取到子進程退出的返回代碼時就會產生僵死進程。僵死進程會以終止狀態保持在進程表中,並且會一直在等待父進程讀取退出狀態代碼。

這裏有一個創建維持30秒的僵死進程例子:

C
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <stdio.h> #include <stdlib.h> /* * A program to create a 30s zombie * The parent spawns a process that isn‘t reaped until after 30s. * The process will be reaped after the parent is done with sleep. */ int main(int argc, char **argv[]) { int id = fork(); if ( id > 0 ) { printf("Parent is sleeping..n"); sleep(30); } if ( id == 0 ) printf("Child process is done.n"); exit(EXIT_SUCCESS); }

Linux進程狀態是一篇非常棒的文章,它使用代碼例子來講述進程狀態並使用 ptrace 來控制它。

進程包含了什麽信息?

我簡要地提過進程表,我將會在這解釋什麽是進程表。進程表是Linux內核的一種數據結構,它會被裝載到RAM裏並且包含著進程的信息。

每個進程都把它的信息放在 task_struct 這個數據結構裏,task_struct 包含了這些內容:

  • 狀態(任務狀態,退出代碼,退出信號。。。)
  • 優先級
  • 進程id(PID)
  • 父進程id(PPID)
  • 子進程
  • 使用情況(cpu時間,打開的文件。。。)
  • 跟蹤信息
  • 調度信息
  • 內存管理信息

保存進程信息的數據結構叫做 task_struct,並且可以在 include/linux/sched.h 裏找到它。所有運行在系統裏的進程都以 task_struct 鏈表的形式存在內核裏。

進程的信息可以通過 /proc 系統文件夾查看。要獲取PID為400的進程信息,你需要查看 /proc/400 這個文件夾。大多數進程信息同樣可以使用top和ps這些用戶級工具來獲取。

進程執行

當進程執行時,它會被裝載進虛擬內存,為程序變量分配空間,並把相關信息添到task_struct裏。

進程內存布局分為四個不同的段:

  • 文本段,包含程序的源指令。
  • 數據段,包含了靜態變量。
  • 堆,動態內存分區區域。
  • 棧,動態增長與收縮的段,保存本地變量。

這裏有兩種創建進程的方法,fork()和execve()。它們都是系統調用,但它們的運行方式有點不同。

要創建一個子進程可以執行fork()系統調用。然後子進程會得到父進程中數據段,棧段和堆區域的一份拷貝。子進程獨立可以修改這些內存段。但是文本段是父進程和子進程共享的內存段,不能被子進程修改。

如果使用execve()創建一個新進程。這個系統調用會銷毀所有的內存段去重新創建一個新的內存段。然而,execve()需要一個可執行文件或者腳本作為參數,這和fork()有所不同。

註意,execve()和fork()創建的進程都是運行進程的子進程。

進程執行還有很多其他的內容,比如進程調度,權限許可,資源限制,庫鏈接,內存映射… 然而這篇文章由於篇幅限制不可能都講述,以後訪問可能會加上

進程間通信(IPC)

為了進程間的通信,存在兩個解決方法,共享內存,消息傳遞。

在共享內存的方案裏,為了幾個進程間能夠通信創建了一個共享的區域。這個區域能被多個進程同時訪問。這種方法通常在使用線程時使用。這是實現IPC最快的形式,因為這種形式只涉及到內存的讀寫。 但是,這需要進程在訪問共享內存時受到的限制和訪問內核實現的其他進程內存一樣。

共享內存段的使用情況可以使用ipcs -m命令查看。

實現一個共享內存的服務器程序,代碼如下:

C
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 #include <stdlib.h> #include <stdio.h> #include <sys/ipc.h> #include <sys/shm.h> #define SEGMENT_SIZE 64 int main(int argc, char **argv[]) { int shmid; char *shmaddr; /* Create or get the shared memory segment */ if ((shmid = shmget(555, SEGMENT_SIZE, 0644 | IPC_CREAT)) == -1) { printf("Error: Could not get memory segmentn"); exit(EXIT_FAILURE); } /* Attach to the shared memory segment */ if ((shmaddr = shmat(shmid, NULL, 0)) == (char *) -1) { printf("Error: Could not attach to memory segmentn"); exit(EXIT_FAILURE); } /* Write a character to the shared memory segment */ *shmaddr = ‘a‘; /* Detach the shared memory segment */ if (shmdt(shmaddr) == -1) { printf("Error: Could not close memory segmentn"); exit(EXIT_FAILURE); } exit(EXIT_SUCCESS); }

通過把 *shmaddr = ‘a’; 替換為 printf(“Segment: %sn”, shmaddr) ,你將會得到一個客戶端程序並且能夠讀取共享內存段的數據。

運行 ipcs -m 將會輸出服務共享內存段的信息:

Shell
1 2 3 4 5 anton@shell:~$ ipcs -m ------ Shared Memory Segments -------- key shmid owner perms bytes nattch status 0x0000022b 0 anton 644 64 0

共享內存段可以使用 ipcrm 命令移除。要了解更多的共享內存實現IPC,可以閱讀Beej的共享內存段教程。

其他實現IPC的方法有文件,信號,套接字,消息隊列,管道,信號燈和消息傳遞。這些方法我不可能全部都深入講解,但我覺得信號和管道的方法我需要提供一些有趣的例子。

信號

介紹進程狀態時,我們已經看了一個使用kill命令的信號示例。信號是把事件或者異常的發生通知進程的軟件中斷。

每個信號都有一個整型標識,但通常使用 SIGXXX 來描述信號,例如 SIGSTOP 或者 SIGCONT 。內核使用信號來通知進程事件的發生,進程也可以使用kill()系統調用發送信號給進程。接收信號的進程可以忽略信號,被殺死,或者被掛起。可以使用信號處理器來處理信號並且在信號出現時任意處理信號。SIGKILL 這個特殊的信號不能被捕獲(處理器處理),要殺死一個掛起的進程時可以使用這個信號。不要把 SIGKILL 和 SIGTERM 混淆了,當使用 Ctrl+C 或者 kill <PID> 殺死進程時默認會發送 SIGKILL 信號。 SIGTERM 不會強制殺死進程並且它可以被捕獲,使用 SIGTERM 的進程通常可以被清理。

管道

管道用來把一個進程的輸出連接到另外一個進程的輸入。這是實現IPC最古老的方法之一。普通管道是單向通信的, 它有一個單向流。可以使用pipe() 創建一個管道,管道和Linux的其他對象一樣,都被看成文件對象。
通其他文件一樣,read()和write()操作都適用於管道。

命名管道是普通管道的增強版,它是雙向通信的並且可以實現管道的多進程讀寫。這都是普通管道不能實現的。無論有沒有進程對命名管道進行讀寫,它都會實際存在。命名管道在文件系統裏以特殊設備文件存在。在GNU/Linux裏,命名管道也被稱為FIFOs(先進先出,First In First Out)。

這裏有一個創建命名管道的例子:

C
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <stdlib.h> #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> int main(int argc, char **argv[]) { if (mknod("myfifo", S_IFIFO|0666, 0) == -1) { printf("Failed to mknodn"); exit(EXIT_FAILURE); } exit(EXIT_SUCCESS); }

在運行目錄裏,我們會看到myfifo文件。它的信息和下面的類似:

Shell
1 prw-rw-r-- 1 anton anton 0 Dec 16 16:14 myfifo

以上就是進程的基本介紹。寫得越多我就越意識到進程有太多東西要講了。從哪裏開始講進程和把不需要覆蓋的知識劃分出來,這是個很艱難的決定。共享內存段是我沒有很好地規劃好的一部分。回看進程間通信那部分是很有趣的。此外,因為有大量諸如Linux編程接口和操作系統概念的好資源,使我們更容易回歸概念思考。

進程的執行狀態