作業系統:程式介紹
很久前我就想寫這篇文章了,但總是以各種理由來拖延。作業系統是我日常工作的主要部分,特別是GNU/Linux,這篇文章主要關注GUN/Linux。
程式是個大話題,我不確定如何才能覆蓋程式的所有知識點。這篇文章將會包含足夠多的程式碼讓你學會如何與程式互動。這些程式碼例子將會側重於GNU/Linux系統,因為這是我最熟悉的系統。
那麼,什麼是程式呢?Linux資訊專案(The Linux Information)把程式定義為“程式的一個執行(即,執行)例項”。所以,要定義程式我們先要定義什麼是程式。再次根據Linux資訊專案裡的定義,“程式是記憶體裡的一個可執行檔案。”
所以,我們知道程式是正在執行的程式的一部分。這是否意味著程式一定是在執行中的?不一定。
程式狀態
為了弄明白正在執行的程式是什麼意思,我們需要知道程式的不同狀態。一個程式可以有幾個狀態(在Linux核心裡,程式有時候也叫做任務)。
下面的狀態在 fs/proc/array.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 訊號讓程式繼續執行。
例如,可以用下面的方法來停止或繼續執行程式:
1 2 |
kill -SIGSTOP <pid> kill -SIGCONT <pid> |
可以使用gdb終止程式來實現跟蹤終止狀態。如果我沒有記錯的話,這個狀態和終止狀態基本上是一樣的。
死亡狀態是核心執行 kernel/exit.c 裡的 do_exit() 函式返回的狀態。這個狀態只是一個返回狀態,你不會在任務列表裡看到這個狀態。
僵死狀態(Zombies)是一個比較特殊的狀態。有些人認為這個狀態是在父程式死亡而子程式存活時產生的。實際上不是這樣的。父程式可能已經死了但子程式依然存活著,那個子程式的父程式將會成為init程式,pid 1。當程式退出並且父程式(使用wait()系統呼叫)沒有讀取到子程式退出的返回程式碼時就會產生僵死程式。僵死程式會以終止狀態保持在程式表中,並且會一直在等待父程式讀取退出狀態程式碼。
這裡有一個建立維持30秒的僵死程式例子:
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命令檢視。
實現一個共享記憶體的伺服器程式,程式碼如下:
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 將會輸出服務共享記憶體段的資訊:
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)。
這裡有一個建立命名管道的例子:
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檔案。它的資訊和下面的類似:
1 |
prw-rw-r-- 1 anton anton 0 Dec 16 16:14 myfifo |
以上就是程式的基本介紹。寫得越多我就越意識到程式有太多東西要講了。從哪裡開始講程式和把不需要覆蓋的知識劃分出來,這是個很艱難的決定。共享記憶體段是我沒有很好地規劃好的一部分。回看程式間通訊那部分是很有趣的。此外,因為有大量諸如Linux程式設計介面和作業系統概念的好資源,使我們更容易迴歸概念思考。
參考
下面的資源用來加深對這個領域知識的理解。如果你想學習關於作業系統的更多內容,一定要看看這些書,雖然書很厚但是值得你閱讀。