一、UNIX體系結構
嚴格來說,操作系統可定義為一種軟件,它控制計算機硬件資源,提供程序運行環境。我們通常將這種軟件稱為內核。因為它小且位於計算機體系的核心。
下圖顯示了UNIX系統的體系結構:
上圖陰影部分為系統調用,所有的系統調用都會從用戶空間中匯聚到 0x80中斷點,同時保存具體的系統調用號。
C語言中的open()、read()等函數都是通過系統調用觸發中斷,進而調用驅動函數完成讀寫操作。
二、文件和目錄
UNIX文件系統是一直樹形層次結構,所有文件的起點是一個為根的目錄,它是“/”。在UNIX系統中本着是一切皆文件的思想,比如在命令行中執行如下命令,就會顯示/etc目錄的下的文件和目錄。
$ vi /etc
創建新目錄時會自動創建兩個文件夾:.(點)和..(點點)。.(點)表示當前目錄,..(點點)表示父目錄。有一個特殊的情況,那就是根目錄“/”下的.(點)和..(點點)是同一個路徑,都是“/”。
由斜線“/”開頭的路徑都是絕對路徑,反之則是相對路徑。
下面是一個用C實現的ls命令代碼:
1 #include <dirent.h> 2 #include <stdio.h> 3 4 int main(int argc, char const *argv[]) 5 { 6 if (argc != 2) 7 { 8 printf("usage: %s <directory>\n", argv[0]); 9 return -1; 10 } 11 12 DIR *dp; 13 struct dirent *dirp; 14 15 if (!(dp = opendir(argv[1]))) 16 { 17 printf("can't open %s\n", argv[0]); 18 return -2; 19 } 20 21 while ((dirp = readdir(dp))) 22 { 23 printf("%s\n", dirp->d_name); 24 } 25 26 return 0; 27 }
我們可以通過結果看到/etc目錄下有.(點)和..(點點)兩個目錄。(僅展示部分結果)
$ ./a.out /etc
.
..
bluetooth
dbus-1
rsyslog.conf
三、輸入和輸出
在UNIX系統中輸入和輸出是經過抽象的,所有的輸入和輸出都是通過文件來完成的。當我們讀寫時,是在對文件進行讀寫,而實際上該文件可能管理映射到硬件(如LED、按鍵等),也可能是一個socket套接字。
文件的抽象是通過文件描述符實現的,打開一個文件得到一個文件描述符,它通常是非負數的,我們使用read()、write()讀寫時,都是對文件描述符進行操作。
標准輸入、標准輸出和標准錯誤也是三個文件描述符,且一般情況下,它們被shell默認打開並默認被系統映射到硬件設備。我們可以使用“<”和“>”來重定向這三個文件描述符默認打開的設備。比如執行之前的命令:
$ ./a.out > /dev/null
此時我們會發現命令行中沒有輸出,因為此命令將輸出結果重定位到/dev/null空設備中。
四、程序和進程
程序是靜態的進程,而進程是運行着的程序。程序本質上是一個存在硬盤上的可執行文件。程序被加載到內存中之后就開始執行,此時程序變成一個動態的進程。每一個進程都有一個標識符,稱為進程ID,其是一個非負數,且在當前時刻是唯一的。
有3個可以用於控制進程的系統調用:fork、exec和waitpid。其中exec是一系列函數的統稱。
每一個進程都是一個獨立的個體。一個進程可以擁有多個線程。
通常,一個進程只有一個主線程,也就是main函數的線程。當我們需要同時處理多個任務時(比如我們一邊聽歌、一邊走路),就需要使用多線程。一個進程內的所有線程共享當前進程的內存空間、文件描述符、棧和進程相關的屬性。由於所有進程共享進程的內存空間,因此在訪問共享數據時需要采取同步措施以避免數據的不一致。
同進程類似,線程也有一個ID唯一標識每一個進程,但線程的ID只在進程內部有效,進程外部則無意義。
五、出錯處理
當UNIX系統調用函數出錯時,通常會返回一個負值。一般我們需要對出錯進行處理。
系統調用函數通常會將錯誤返回值賦給errno,errno變量看起來像是一個int類型的變量,但實際上並不是。
早期的時候,它被簡單的用int類型變量實現。但隨着多線程出現之后,一個進程的errno變量是被多個線程共享的,當某一個線程因為出錯而改變了errno變量之后,其他線程無法根據errno來判斷自己當前的狀態,造成了混淆,因此現在它通常被實現為一個函數調用。
extern int *__errno_location(void); #define errno (*__errno_location())
C標准定義了兩個函數,可用於打印出錯信息。
#include <string.h> char* strerror(int errnum); #include <stdio.h> void perror(const char *msg);
六、信號
信號(signal)用於通知進程發生了某種狀況(比如執行除數為0的除法操作),則系統會發送一條通知至該進程,進程收到信號通知后,有3種應對處理方法:
- 忽略信號。不進行處理。
- 按系統默認方式處理。
- 提供一個函數。此方式在收到信號之后,用我們提供的函數進行處理。
舉個例子,假設現在有一個程序,它有三種方式來處理用戶通過鍵盤Ctrl+C(對應信號的SIGINT)發出的中斷信號。
對於忽略信號,程序會忽略Ctrl+C,導致按Ctrl+C時沒有任何反應。
對於按系統默認方式處理,Ctrl+C在系統中默認是終止程序,則程序會被終止。
對於提供一個函數,程序使用我們提供的函數進行處理,我們可以在函數中執行printf()等操作。
七、時間值
UNIX中有兩種表示時間的方式。
一種是指日歷時間(比如現在是幾點幾分),該值是從1970年1月1日以來經過了多少秒的形式,該秒數使用time_t類型表示。在UNIX系統中,可以使用time()函數來返回當前日期和時間。
#include <time.h>
time_t time(time_t *__timer);
函數成功返回從1970年1月1日以來經過了多少秒,失敗返回-1。
如果__timer指針非空,那么__timer指針所指的對象也被設置為相應的值。
返回的time_t對於我們來說沒有意義,必須轉換為我們可讀的時間。UNIX系統提供了兩個函數用於從time_t轉換為年月日時分秒的時間結構類型,提供了一個逆向從年月日時分秒的時間結構類型轉到time_t的函數,還提供了兩個格式化輸出的函數。其函數聲明如下:
#include <time.h> struct tm *gmtime(const time_t *timer); /* 轉換為格林威治時間 */ struct tm *localtime(const time_t *timer); /* 轉換為本地時間 */ time_t mktime(struct tm *tp); size_t strftime(char* s, size_t maxsize, const char* format, const struct tm* tp); size_t strftime_l(char* s, size_t maxsize, const char* format, const struct tm* tp, locale_t loc);
對於前兩個函數,成功返回相應指針,失敗返回NULL;對於第三個函數,成功返回時間,失敗返回-1;對於最后兩個函數,成功返回字符數,失敗返回0。
上面函數所使用或返回的struct tm如下:
struct tm { int tm_sec; /* Seconds: 0-59 (K&R says 0-61?) */ int tm_min; /* Minutes: 0-59 */ int tm_hour; /* Hours since midnight: 0-23 */ int tm_mday; /* Day of the month: 1-31 */ int tm_mon; /* Months *since* january: 0-11 */ int tm_year; /* Years since 1900 */ int tm_wday; /* Days since Sunday (0-6) */ int tm_yday; /* Days since Jan. 1: 0-365 */ int tm_isdst; /* +1 Daylight Savings Time, 0 No DST, * -1 don't know */ };
示例代碼如下:
1 time_t sec; 2 struct tm *stm; 3 4 while (1) { 5 time(&sec); 6 stm = localtime(&sec); 7 printf("Time: %4d-%02d-%02d %02d:%02d:%02d\r", 1900 + stm->tm_year, 1 + stm->tm_mon, \ 8 stm->tm_mday, stm->tm_hour, stm->tm_min, stm->tm_sec); 9 Sleep(1); /* 休眠1秒 */ 10 }
另一種是指進程時間,用來表示進程執行的時間。
我們可以用time命令來得知一個程序執行所花費的時間:
$ time ./a.out > /dev/null
在程序中可以使用clock_gettime()函數用來計時,比如從當前程序開始執行開始到程序停止執行為止統計總計花費了多少時間。
此函數可用於獲取指定時鍾開始之后的秒數。函數聲明如下:
#include <time.h> int clock_gettime(clockid_t __clock_id, struct timespec *__tp);
函數成功時返回0,失敗返回-1。
八、系統調用和庫函數
所有的操作系統都提供多種服務的入口點,由此程序可以向內核請求服務。各種版本的UNIX實現都提供良好定義、數量有限、直接進入內核的入口點,這些入口點被稱為系統調用。
系統調用接口在man手冊的第二部分中說明,是使用C語言定義的。比如:
$ man 2 read
公用函數庫接口在man手冊的第三部分中說明,也是使用C語言定義的。它們不一定是內核的入口點,部分會間接使用一個或多個內核系統調用,而有些則完全不使用。
從實現角度看,系統調用和公用函數庫有着本質區別,系統調用是伴隨內核而產生的,在用戶空間是不可替換的。公用函數庫是編譯器廠商根據語言標准而實現的,可以更新和替換。
從用戶角度看,它們沒有太大區別。
下一章 第三章:文件I/O