第一章 UNIX 基礎知識
1.1 Unix體系結構
OS定義為一種軟件,它控制計算機硬件資源,提供程序運行環境,一般稱其為內核(kernel),它體積小,位於環境中心。
內核的接口為系統調用(system call),共用函數庫構建在系統調用上,應用軟件既可以使用公用函數庫,也可以使用系統調用。shell是一種特殊的應用程序,它為運行其他應用程序提供一個接口。
下圖為 UNIX 體系結構:
廣義上,OS包括內核和一些軟件,例如 Linux 是 GNU 操作系統使用的內核,可以稱這種操作系統為 GNU/Linux,但是通常簡稱為 Linux。所以 Linux 本身有雙重含義,內核和操作系統。
1.2 登陸
(1)登錄名
用戶登陸 UNIX 系統,鍵入 登錄名,再鍵入 口令。系統在其口令文件(通常是/etc/passwd文件)中查看登錄名。
我文件中的內容:
口令文件中的登陸項由7個以冒號分隔的字段組成,他們分別是:
登陸名、加密口令、數值用戶ID、數值組、註釋字段、起始目錄、shell程序
其中,所有OS已將加密口令移到另一個文件中,第6章將說明這種文件以及訪問他們的函數。
(2)shell
用戶登陸後,用戶可以向shell程序鍵入命令,某些系統會啟動一個視窗管理程序,但 最終總會有一個shell程序運行在一個視窗中。
shell是一個命令解釋器,它讀取用戶輸入,然後執行命令。
用戶通常用終端(交互式shell),有時通過文件(shell腳本,shell script)向shell進行輸入。
下圖是常見的shell
Steve Bourne在貝爾實驗室開發的 Bourne shell。
Bourne-again shell 是GNU shell,所有Linux系統都提供這種shell,它被設計遵循 POSIX 的。
1.3 文件和目錄
(1)文件系統
UNIX文件系統是目錄和文件組成的一種層次結構,目錄的起點稱為根(root),名字是 / 。
目錄(directory)是一個包含許多目錄項的文件。
在邏輯上,可以認為每個目錄項都包含一個文件名,文件屬性信息(文件類型,文件大小...),stat 和 fstat 可以返回文件屬性的一個信息結構。
目錄項的邏輯視圖與實際存放在磁盤上的方式是不同的。UNIX 文件系統的大多數實現並不在目錄項中存放屬性,這是因為當一個文件具有多個硬鏈接時,很難保持多個屬性副本之間的同步。到第4章討論硬鏈接時,這個問題將很好理解。
(2)文件名
目錄中各個名字稱為文件名(filename)。
文件名中不能出現斜線(/)和空操作符(null)。因為謝賢用於分隔各文件名構成路徑名。空操作符用於終止一個路徑名。
創建新目錄時會自動創建兩個文件名:. 和 .. ,點指向當前目錄,點一點指向父目錄。在最高層次的根目錄中,點一點和點相同。
現在,所有的UNIX系統支持至少 255 各字符的文件名。
(3)路徑名
一個或多個斜線分隔的文件名序列構成路徑名(pathname),以斜線開頭的路徑稱為絕對路徑(absolute pathname),否則稱為相對路徑(relative pathname)。相對路徑名引用相對於當前目錄的文件。
// 列出一個目錄中所有文件 #include "apue.h" #include <dirent.h> int main(int argc, char **argv) { DIR *dp; struct dirent *dirp; if (argc != 2) err_quit("usage: ls directory_name"); if ((dp = opendir(argv[1])) == NULL) err_sys("Can‘t open %s", argv[1]); while ((dirp = readdir(dp)) != NULL) printf("%s\n", dirp->name); closedir(dp); exit(0); }
因為各種不同 UNIX 系統目錄項的實際格式是不一樣的,所以使用函數 opendir, readdir, closedir對目錄進行處理。
opendir 函數返回指向 DIR 結構的指針,將這個指針傳給 readdir 函數,我們不關心 DIR 結構中包含了什麽。然後,在循環中調用 readdir 來讀每個目錄項。
readdir 函數返回一個指向 dirent 結構的指針,而當目錄中已無可讀的目錄項時則返回 null 指針。在dirent 結構中取出的是每個目錄項的名字(d_name)。使用該名字,此後可調用 stat 函數以獲得該文件的所有屬性。
當程序將結束,它以參數0調用函數 exit,exit終止程序,按慣例,參數0表示正常結束,參數1-255表示出錯。
struct dirent 結構如下:
(4)工作目錄
每個進程 都有一個工作目錄(working directory),有時稱為當前工作目錄(current working directory)。所有相對路徑名都從工作目錄開始解釋。進程可以用chdir函數更改其工作目錄。
(5)起始目錄
登陸時,工作目錄設置為起始目錄(home directory),該起始目錄從口令文件中相應用戶的登陸項中取得。
1.4 輸入和輸出
(1)文件描述符
文件描述符(file descriptor)通常時一個小的非負整數,內核用它標識一個特定進程正在訪問的文件。當內核打開一個已有文件或創建一個新文件時,它返回一個文件描述符。在操作文件時,可以使用。
(2)標準輸入、標準輸出和標準出錯
按慣例,每當運行一個新程序時,所有shell都為其打開三個文件描述符:標準輸入(standard input)、標準輸出(standard output)以及標準出錯(standard error)。
如果項 ls 那樣沒有做什麽特殊處理,則這三個描述符都鏈向終端。
大多數 shell 都提供一種方法,使其中 任何一個或所有這三個描述符都能重定向到某個文件,如:
ls > file.list
(3)不用緩沖的 I/O
函數 open、read、write、lseek以及close提供了不用緩沖的 I/O。這些函數都使用文件描述符。
// 將標準輸入復制到標準輸出 #include "apue.h"
#define BUFFSIZE 1
int main(void) { int n; char buf[BUFFSIZE]; while ((n = read(STDIN_FILENO, buf, BUFFSIZE)) > 0) if (write(STDOUT_FILENO, buf, n) != n) err_sys("write error"); if (n < 0) err_sys("read error"); exit(0); }
由於鍵入值的傳遞是 FIFO結構的,所以無論 BUFFSIZE 設置為什麽值,程序都能正常執行,但是執行效率不同。
(2)標準 IO
標準 I/O 函數提供一種對不用緩沖 I/O 函數的帶緩沖接口。使用標準 I/O 函數無需擔心如何選取最佳的緩沖區大小,例如上面程序中的 BUFFSIZE 常量的大小。
使用標準 I/O 函數的另一個優點是簡化了對輸入行的處理。例如,fgets函數讀一完整的行,而read函數讀指定字節數。
在5.4節中,我們將了解到,標準 I/O 函數庫提供了使我們能夠控制該庫所使用的緩沖風格的函數。
// 用標準 I/O 將標準輸入復制到標準輸出 #include "apue.h" int main() { int c; while ((c = getc(stdin)) != EOF) if (putc(c, stdout) == EOF) err_sys("output error"); if (ferror(stdin)) err_sys("input_err"); exit(0); }
EOF是一個常量,在stdio.h 中定義,使用 ctrl + D鍵入。 標準輸入/標準輸出 stdin 和 stdout 定義在 stdio.h 中,表示標準輸入和標準輸出文件。
1.5 程序和進程
(1)程序
程序(program)是存放在磁盤上、處於某個目錄中的一個可執行文件。使用6個exec函數中的一個由內核將程序讀入存儲器,並使其執行。
(2)進程和進程ID
程序的執行實例被稱為進程(process)。某些操作系統用任務(task)表示正被執行的程序。
UNIX系統確保每個進程都有一個唯一的數字標識符,稱為進程ID(process ID)。進程ID總是一非負整數。
(3)進程控制
有三個用於進程控制的主要函數:fork、exec和waitpid。
// 從標準輸入讀命令並執行 #include "apue.h" #include <sys/wait.h> int main(void) { char buf[MAXLINE]; pid_t pid; int status; printf("%% "); while (fgets(buf, MAXLINE, stdin) != NULL) { if (buf[strlen(buf) - 1] == ‘\n‘) buf[strlen(buf) - 1] = 0; /* replace newline with null */ if ((pid = fork()) < 0) { err_sys("fork error"); } else if (pid == 0) { execlp(buf, buf, (char *)0); err_ret("couldn‘t execute: %s", buf); exit(127); } if ((pid = waitpid(pid, &status, 0)) < 0) err_sys("waitpid error"); printf("%% "); } exit(0); }
fgets從標準輸入一次讀一行,當鍵入文件按結束字符EOF(使用 ctrl + D)作為行的第一個字符時,fgets返回一個 null 指針,程序退出。
由於 execlp 函數要求參數以 null 結尾,而不是換行符,所以需要進行替換。
(4)線程和線程ID
通常,一個進程只有一個控制線程(thread),同一時刻只執行一組機器指令,對於某些問題,如果不同部分各使用一個控制線程,那麽可簡化問題解決。另外,多個控制線程能充分利用多處理器系統的並行性。
同一個進程的線程共享同一地址空間,所以各線程在訪問共享數據時需要采取同步措施以避免不一致性。
與進程相同,線程也用ID標識,但是線程ID只在它所屬的進程內起作用。
1.6 出錯處理
UNIX 函數時,通常返回一個負值,或者 null,而且整形變量 errno 通常被設置為函數有附加信息的一個值。
文件<errno.h>中定義了符號errno和可以賦予它的各種常量。
errno 以前的定義是:
extern int errno;
但是在支持線程的環境中,多個線程共享進程地址,每個線程都有屬於自己的局部errno以避免一個線程幹擾另一個線程。例如 Linux支持多線程存取 errno,將其定義為:
extern int * __errno_location(void); #define errno (*__errno_location())
對於errno應當知道兩條規則。
第一:如果沒有出錯,則其值不會被一個例程清楚。因此,僅當函數的返回值指明出錯時,才檢驗其值。
第二:任一函數都不會講errno值設置為0,在<errno.h>中定義的所有常量都不為0
C標準定義了兩個函數,它們幫助打印出錯信息。
#include <string.h> char *strerror(int errnum);
#include <stdio.h> void perror(const char *msg);
它首先輸出msg指向的字符串,然後一個冒號,一個空格,接著時errno值對應的出錯信息,最後是一個換行符。
// 示例strerror和perror #include "apue.h" #include <errno.h> int main(int argc, char **argv) { fprintf(stderr, "EACCES: %s\n", strerror(EACCES)); errno = ENOENT; perror(argv[0]); exit(0); }
出錯恢復:
可將<errno.h>中定義的各種出錯分成致命性和非致命性兩類。對於致命性錯誤,無法執行恢復動作,最多只能在用戶屏幕上打印一條出錯信息,或寫入日誌,然後終止。而對於非致命性出錯,可以進行處理,大多數非致命性出錯本質上是暫時的,如資源短缺。
與資源相關的非致命性出錯包括 EAGAIN、ENFILE、ENOBUFS、ENOLCK、ENOSPC、ENOSR、EWOULDBLOCK,有時 ENOMEM也是非致命性,當EBUSY指明共享資源正在使用時,可以將他作為非致命性出錯處理,當EINTR中斷一慢速系統調用時,可 將它作為非致命性出錯處理。
對於資源相關的非致命性出錯,一般恢復動作時延遲一些時間,然後再試。
1.7 用戶標識
(1)用戶ID
口令文件登陸項中的用戶ID(user ID)是個數值,它向系統標識各個不同的用戶。
系統管理員在確定一個用戶登陸名同時,確定用戶ID,用戶不能更改用戶ID。
用戶ID為0,是超級用戶。
(2)組ID
口令文件登陸項也包括用戶的組ID(group ID),它是一個數值。
組被用於將若幹用戶分到不同的項目組或者部門中去。這種機制允許同組各個成員之間共享資源,而組外用戶則不能。
組文件將組名映射為數字組ID,它通常是 /etc/group
使用數字ID是歷史上形成的,為的是節省磁盤空間,另外權限校驗也比字符串更省時。對於用戶而言使用字符串更方便,所以口令文件包含了登陸名和用戶ID之間的映射關系。
(3)附加組ID
大多數UNIX系統允許用戶屬於多個組。
1.8 信號
信號(signal)是通知進程已發生某種情況的一種技術。
進程對於信號有三種選擇:忽略,默認方式處理,捕捉。
1.9 時間值
UNIX系統一直使用兩種不同的時間值:
(1)日歷時間,該值是自 1970年1月1日00:00:00以來國際標準時間(UTC)所經過的秒數累計值(早期稱為格林尼治標準時間)。
系統基本數據類型 time_t 用於保存這種事件值。
(2)進程時間,也被稱為 CPU 時間,用於度量進程使用CPU資源。進程時間以時鐘滴答計算。歷史上有每秒50,60或100個滴答。
系統基本數據類型 clock_t 用於保存這種時間值。
當度量一個進程的執行時間時,UNIX系統使用三個進程時間值:
時鐘時間,用戶CPU時間,系統CPU時間。
時鐘時間:進程運行時間總量。
用戶CPU時間:執行用戶指令所用的時間。(進程在用戶空間的時間)
系統CPU時間:執行內核程序所經歷的時間。(進程在內核空間的時間)
要獲進程的三種時間,只需要執行命令 time(1)。
第一章 UNIX 基礎知識