引言:
? ? ? ? 所有的操作系統都為運行在其上的程序提供服務,比如:執行新程序、打開文件、讀寫文件、分配存儲區、獲得系統當前時間等等
1. UNIX體系結構
? ? ? ? 從嚴格意義上來說,操作系統可被定義為一種軟件,它控制計算機硬件資源,提供程序運行的環境。我們通常將這種軟件稱為內核(kernel),因為它相對較小,而且位于環境的核心。圖1-1 顯示了 UNIX 操作系統的體系結構。
? ? ? ? ?內核的接口被稱為系統調用(system call),公用函數庫構建在系統調用接口之上,應用程序可以使用公用函數庫提供的接口,也可以使用內核提供的接口(系統調用)。shell 是一個特殊的應用程序,為運行其他應用程序提供了一個接口。從廣義上說,操作系統包括內核和一些其他軟件,這些軟件使得計算機能夠發揮作用,并使計算機具有自己的特性。這里所說的其他軟件包括系統實用程序(system utility)、shell、公用函數庫以及應用程序等。Linux 是 GUN 操作系統使用的內核,一些人將這種操作系統稱為 GUN/Linux 操作系統,但是更常見的是簡單地稱其為 Linux。
2. 登錄
2.1 用戶名
? ? ? ? 用戶在登錄 UNIX 系統時,輸入用戶名,然后輸入用戶密碼。系統將在其口令文件(通常是 /etc/passwd 文件)中查看登錄名。口令文件中的登錄項由 7 個以冒號(:) 分隔的字段組成,依次是【登錄名:加密口令:用戶id:用戶組id:注釋字段:用戶登錄進入的起始工作目錄:shell 程序路徑】
在 shell 終端下輸入以下命令查看 /etc/passwd 文件的內容進行驗證:
cat /etc/passwd
用戶名:root
加密口令:x
用戶id:0
用戶組id:0
注釋字段:root
用戶登錄進入的起始工作目錄:/root
用戶的 shell 程序路徑:/bin/bash
? ? ? ? 加密口令:x 是一個占位符,較早期的 UNIX 系統版本中,該字段存放加密口令字。將加密口令字存放在一個人人可讀的文件中是一個安全漏洞,所以現在將加密口令字存放在另一個文件中。第6章將說明這種文件以及訪問它們的函數。
?2.2 shell
? ? ? ? 用戶登錄后,系統通常先顯示一些系統信息,然后用戶就可以向 shell 程序輸入命令。shell 是一個命令行解釋器,它讀取用戶輸入,然后執行命令。shell 的用戶輸入通常來自終端(交互式 shell),有時則來自于文件(稱為 shell 腳本)。
3. 文件和目錄
3.1 文件系統
? ? ? ? UNIX 文件系統是目錄和文件的一種層次結構,所有東西的起點是根目錄(root),這個目錄的名稱是一個字符 “/”。
? ? ? ? 目錄是一個包含目錄項的文件。在邏輯上,可以認為每個目錄項都包含一個文件名,同時還包含說明該文件屬性的信息。文件屬性是指文件類型(普通文件還是目錄等)、文件大小,文件所有者、文件權限(其他用戶是否有訪問該文件的權限)以及文件的最后修改時間等。
3.2 文件名
? ? ? ? 文件的名字稱為文件名(filename),只有斜線(/)和 空字符這兩個字符不能出現在文件名中。斜線用來分隔構成路徑名的,空字符則用來終止一個路徑名。
? ? ? ? 創建新目錄時會自動創建兩個文件名:. (點)和 ..(點點) 。點指向當前目錄,點點指向父目錄。最高層次的根目錄中,點和點點都指向當前目錄。
3.3 路徑名
? ? ? ? 由斜線分隔的一個或多個文件名的序列(也可以斜線開頭)構成路徑名(pathname)。以斜線開頭的路徑名稱為絕對路徑(absolute pathname),否則稱為相對路徑(relative pathname),相對路徑名指向相對于當前目錄的文件。文件系統根的名字(/)是一個特殊絕對路徑名,它不包含文件名。
以下代碼功能:列出一個目錄中所有文件的名字,ls 命令的簡要實現
err.h
#ifndef ERR_H_
#define ERR_H_#include <stdarg.h>#define MAX_BUF 4096// __attribute__((noreturn)) 屬性,告訴編譯器這個函數永遠不會有返回值,避免當函數有返回值,在某種條件下未能執行到返回值代碼處,編譯器警告或報錯
__attribute__((noreturn)) void err_quit(const char *format, ...);
__attribute__((noreturn)) void err_sys(const char *format, ...);
void err_ret(const char *format, ...);#endif /*ERR_H_*/
err.c
#include "err.h"#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>static void err_doit(const int errno_flag, const int errorno, const char *format, va_list ap);static void err_doit(const int errno_flag, const int errorno, const char *format, va_list ap)
{char buf[MAX_BUF] = "";vsnprintf(buf, MAX_BUF-1, format, ap); // 將可變參數列表格式化到字符串中if (errno_flag)snprintf(buf+strlen(buf), MAX_BUF-strlen(buf)-1, ": %s", strerror(errorno));strcat(buf, "\n"); // 將參數字符串 "\n" 拷貝到參數 buf 所指的字符串尾。buf 要有足夠的空間來容納要拷貝的字符串。fflush(stdout); // 刷新 stdoutfputs(buf, stderr); // 將 buf 所指的字符串寫入到 strerrfflush(NULL); // 參數為 NULL, fflush() 會刷新所有 stdio
}void err_quit(const char *format, ...)
{va_list ap;va_start(ap, format); // 獲取可變參數列表的第一個參數的地址err_doit(0, 0, format, ap);va_end(ap); // 清空va_list可變參數列表exit(1);
}void err_sys(const char *format, ...)
{va_list ap;va_start(ap, format);err_doit(1, errno, format, ap);va_end(ap);exit(1);
}void err_ret(const char *format, ...)
{va_list ap;va_start(ap, format);err_doit(1, errno, format, ap);va_end(ap);
}
list_dir_filename.c
#include "../common/err.h"#include <dirent.h>
#include <stdio.h>int main(int argc, char *argv[])
{DIR *dirp = NULL;struct dirent *direntp = NULL;if (argc != 2)err_quit("ls dir_name");/* 打開目錄 */if ((dirp = opendir(argv[1])) == NULL)err_sys("can't open %s", argv[1]);/* 逐一讀取目錄條目 */while ((direntp = readdir(dirp)) != NULL)printf("%s\n", direntp->d_name);/* 關閉目錄 */closedir(dirp);return 0;
}
3.4 工作目錄
? ? ? ? 每個進程都有一個工作目錄(working directory),有時稱為當前工作目錄(current working directory)。所有相對路徑名都從工作目錄開始解釋的。進程可以用 chdir() 函數更改其工作目錄。
? ? ? ? 例如,相對路徑名 unix_env_program/chapter_one/test 指的是當前工作目錄中的 unix_env_program 目錄中的 chapter_one 目錄中的 test 文件(或目錄)。從路徑名可以看出?unix_env_program 和 chapter_one 是目錄,但是不能分辨 test 是文件還是目錄。/etc/passwd 是一個絕對路徑名,它指的是根目錄下的 etc 目錄中的 passwd 文件(或目錄)
3.5 起始目錄
? ? ? ? 登錄時,工作目錄設置為起始目錄(home directory),起始目錄從口令文件(/etc/passwd)中相應用戶的登錄項中獲得。
4. 輸入和輸出
4.1 文件描述符
? ? ? ? 文件描述符(file descriptor)通常是一個小的非負整數,內核用以標識一個特定進程正在訪問的文件。當內核打開一個現有的文件或創建一個新文件時,它都會返回一個文件描述符。在讀寫這個文件時,可以使用這個文件描述符。
4.2 標準輸入、標準輸出和標準錯誤
? ? ? ? 每當運行一個新程序時,所有的 shell 都為其打開 3 個文件描述符,即標準輸入(standard input)、標準輸出(standard output)和標準錯誤(standard error),STDIN_FILENO(0)、?STDOUT_FILENO(1)和 STDERR_FILENO(2)為 3 個常量,定義在 #include <unistd.h> 頭文件中,它們指向了標準輸入、標準輸出和標準出錯的文件描述符。如果不做特殊處理,例如就像簡單的命令 ls,則這 3 個文件描述符都鏈接向終端。大多數 shell 都提供一種方法,使其中任何一個或所有這 3 個文件描述符都能重新定向到某個文件,例如:
ls > file.list // 執行 ls 命令,將其標準輸出重新定向到名為 file.list 的文件中。
4.3 不帶緩沖的 I/O
? ? ? ? ?函數 open()、read()、write()、lseek() 以及 close() 提供了不帶緩沖的 I/O,這些函數操作的都是文件描述符。
以下代碼功能:從標準輸入中讀,并向標準輸出中寫,用于復制任一個 UNIX 普通文件
stdin_r_stdout_w.c
#include "../common/err.h"#include <unistd.h>#define BUF_SIZE 4096int main(int argc, char *argv[])
{int nread = 0;char buf[BUF_SIZE] = "";while ((nread = read(STDIN_FILENO, buf, BUF_SIZE)) > 0) // 從標準輸入文件描述符中讀{if (write(STDOUT_FILENO, buf, nread) != nread) // 寫入標準輸出文件描述符中err_sys("write error");}if (nread < 0)err_sys("read error");return 0;
}
將程序編譯成 stdin_r_stdout_w 可執行文件,在 shell 終端輸入以下命令
$./stdin_r_stdout_w > data.stdout // 標準輸入是終端,標準輸出重定向到 data.stdout 文件,標準出錯也是終端
鍵盤輸入:111,再換行,再輸入:222,再換行,再按下文件結束符(ctrl +d),將終止本次復制。
在 shell 終端輸入以下命令
$ ./stdin_r_stdout_w < data.stdout > newdata.stdout
// 標準輸入重定向為 data.stdout 文件,標準輸出重定向為 newdata.stdout 文件,標準出錯是終端
此時,是將 data.stdout 文件復制一份,并命名為 newdata.stdout
?4.4 標準 I/O
? ? ? ? 標準 I/O 函數為那些不帶緩沖的 I/O 函數提供了一個帶緩沖的接口。使用標準 I/O 函數無需擔心如何選取最佳的緩沖區大小。使用標準的 I/O 函數還簡化了對輸入行的處理。例如,fgets() 函數讀取一個完整的行,而 read() 函數讀取指定的字節數。我們最熟悉的標準 I/O 函數是 printf(),在調用 printf() 函數的程序中,總是要 #include <stdio.h> ,該頭文件包括了所有標準 I/O 函數的原型。
? ? ? ? 標準 I/O 操作的對象是文件指針:FILE*
? ? ? ? 特殊的文件指針:stdin、stdout、stderr
以下代碼功能:從標準輸入復制到標準輸出,也是用于復制任一個 UNIX 普通文件
stdin_copy_to_stdout.c
#include "../common/err.h"#include <stdio.h>int main(int argc, char *argv[])
{int c = 0;while ((c = getc(stdin)) != EOF) // 一次讀取一個字符,直到讀入最后的一個字節時,返回 EOF{if (putc(c, stdout) == EOF) // 將字符寫入到標準輸出err_sys("output error");}if (ferror(stdin))err_sys("input error");return 0;
}
常量 EOF ,標準 I/O 常量 標準輸入(stdin)和 標準輸出(stdout)都在 #include <stdio.h> 頭文件中定義。
5. 程序和進程
5.1 程序
? ? ? ? 程序(program)是一個存儲在磁盤上某個目錄中的可執行文件。內核使用 exec() 函數,將程序讀入內存,并執行程序。
5.2 進程和進程ID
? ? ? ? 程序的執行實例被稱為進程(progress)。UNIX 系統確保每個進程都有一個唯一的數字標識符,稱為進程ID(progress ID)。進程ID 總是為一個非負整數。
以下代碼功能:打印進程ID
print_pid.c
#include <unistd.h>
#include <stdio.h>int main(int argc, char *argv[])
{printf("pid = %ld\n", getpid());return 0;
}
5.3 進程控制
? ? ? ? fork()、exec() 和 waitpid() 是 3 個主要用于進程控制的主要函數。
? ? ? ? 以下代碼功能:從標準輸入中讀取命令,然后執行這些命令,類似于 shell 程序的基本實施部分。
execlp_demo.c
#include "../common/err.h"#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>
#include <stdlib.h>int main(int argc, char *argv[])
{char buf[MAX_BUF] = "";pid_t pid = 0;int status = 0;printf("%%");while ((fgets(buf, MAX_BUF, stdin)) != NULL) // 從標準輸入 stdin 中讀取一行{if (buf[strlen(buf) - 1] == '\n') // 最后一個是換行符時buf[strlen(buf) - 1] = 0; // 替換成 NULL,因為 execlp() 函數的參數要求字符串必須要以字符串結束符('\0')結尾if ((pid = fork()) < 0) {err_sys("fork error");} else if (pid == 0) { // 子進程/*int execlp(const char *file, const char *arg, ...);(1)execlp 函數名中 l 是 list,說明這個函數的參數是可變參數列表的意思(2)execlp 函數名中 p 是環境變量 $PATH 的意思,說明,這個函數的第一個參數可以是一個文件名,這時函數會從環境變量中去搜索這個文件的完整路徑名;這個函數的第一個參數也可以是絕對路徑或相對路徑,這時函數不會從環境變量中去搜索這個文件的完整路徑名(3)第二個參數 arg 是命令行起始地址(4) 傳遞給這個函數的最后一個參數必須為*/ execlp(buf, buf, (char *)NULL); // 執行讀取的命令err_ret("can't exec: %s", buf); // 如果程序能跑到這里,說明執行 execlp() 函數出錯了exit(127); // 子進程退出}/*pid_t waitpid(pid_t pid, int *status, int options);默認情況下 (當 options=0 時 ),waitpid掛起調用進程的執行,直到它的等待集合 (wait set) 中的一個子進程終止。如果等待集合中的一個進程在剛調用的時刻就已經 終止了,那么 waitpid 就立即返回 。在這兩種情況中,waitpid返回導致 waitpid 返回的已終止子進程的PID此時,已終止的子進程已經被回收,內核會從系統中刪除掉它的所有痕跡。*/// 父進程if ((pid = waitpid(pid, &status, 0)) < 0) // 子進程退出時,父進程回收子進程的資源,options=0,父進程被掛起,直到子進程退出,waitpid 立即返回err_sys("waitpid error");printf("%%");}return 0;
}
5.4 線程和線程ID?
????????通常,一個進程只有一個控制線程(thread)--?某一個時刻執行的一組機器指令。對于某些問題,如果有多個控制線程分別作用于它的不同部分,那么解決起來就容易的多。另外,多個控制線程也可以充分利用多核處理器的并行執行任務的能力。
? ? ? ? 一個進程內的所有線程共享同一個地址空間、文件描述符、棧以及與進程相關的屬性。因為它們能訪問同一個存儲區,所以各線程在訪問共享數據時需要采取同步措施以避免數據不一致的問題。
? ? ? ? 和進程相同,線程也有 ID 標識。但是,線程ID 只在它所屬的進程內起作用。一個進程的線程ID 在另一個進程中沒有意義。當在一個進程中對某個特定線程進行處理時,我們可以使用該線程的 ID 引用它。
6. 出錯處理
6.1 errno 以及出錯打印函數 strerror() 和 perror()
? ? ? ? 當 UNIX 系統函數出錯時,通常會返回一個負值,而且整型變量 errno 通常被設置為具有特定出錯信息的值。例如,open() 函數如果成功執行則返回一個非負文件描述符,如果出錯則返回 -1,errno 將會被設置,通過調用 char *str?= strerror(errno); 獲取到出錯信息。而有些函數出錯則使用另一種約定而不是返回一個負值。例如,大多數返回指向對象指針的函數,在出錯時會返回一個 NULL 指針。
? ? ? ? 頭文件 <errno.h> 中定義了 errno 以及賦予它的各種常量。這些常量都以字符 E 開頭。在 linux 操作系統中,出錯常量在 man 3 errno 手冊頁查看
? ? ? ? POSIX 和 ISO C 將 errno 定義為一個符號,它擴展成為一個可修改的整型左值(lvalue),它可以是一個包含出錯編號的整數,也可以是一個返回出錯編號指針的函數。以前使用的定義是:extern int errno;
但在支持線程的環境中,多個線程共享進程地址空間,每個線程都有屬于它自己的局部 errno,以避免一個線程干擾另一個線程。linux 操作系統支持多線程存取 errno,將其定義為:
extern int* __errno_location(void);
#define errno *(__errno_location())
對于 errno 應當注意兩條規則。
(1)如果沒有出錯,其值不會被例程清除,因此,僅當函數的返回值指明出錯時,才檢驗其值。
(2)任何函數都不會將 errno 的值設置為 0,而且在 <errno.h> 中定義的所有常量都不為 0。
C 標準定義了兩個函數,用于打印出錯信息
#include <string.h>
char* strerror(int errnum); // 返回值指向出錯信息字符串的指針
strerror() 函數將 errnum(通常就是一個 errno 值)映射為一個出錯信息字符串,并且返回此出錯信息字符串的指針。
#include <stdio.h>
void perror(const char *msg);
perror 函數基于 errno 的當前值,在標準錯誤上參生一條出錯信息,然后返回。它輸出的格式為:
參數 msg 指向的字符串,然后是一個冒號,一個空格,接著是 errno 值對應的出錯信息字符串,最后是一個換行符。
以下代碼功能:展示 strerror() 和 perror() 函數的使用方法
strerror_perror.c
#include <stdio.h>
#include <errno.h>
#include <string.h>int main(int argc, char *argv[])
{fprintf(stderr, "EACCES: %s\n", strerror(EACCES));errno = ENOENT;perror(argv[0]);return 0;
}
?注意:我們將程序名(argv[0])作為參數傳遞給 perror()?函數,這是一個標準的 UNIX 慣例,使用這種方法,在程序作為管道的一部分執行時,例如:
prog1 < inputfile | prog2 | prog3 > outputfile
我們就能夠分清 3 個程序中哪一個產生了一條特定的出錯信息。
6.2?出錯恢復
? ? ? ? 可將在? <errno.h> 中定義的各種出錯分成兩類:致命性的和非致命性的。
? ? ? ? 對于致命性的錯誤,無法執行恢復動作。最多能做的是在用戶屏幕上打印一條出錯消息或將一條出錯消息寫入日志文件中,然后退出。
? ? ? ? 對于非致命性出錯,有時可以較為妥善地進行處理。大多數非致命性出錯是短暫的(如資源短缺),當系統中活動較少時,這種出錯很可能不會發生。
? ? ? ? 與資源相關的非致命性出錯包括:
????????EAGAIN 資源暫時不可用(可能與 EWOULDBLOCK?的值相同)
????????ENFILE 系統中打開的文件太多
????????ENOBUFS 沒有可用的緩沖空間
????????ENOLCK 沒有可用的鎖
????????ENOSPC 設備上沒有剩余空間
????????EWOULDBLOCK 操作將阻塞(可能與 EAGAIN 的值相同)
????????ENOMEM 沒有足夠的空間。有時也是非致命性出錯。
????????EBUSY 設備或資源忙。當指明共享資源正在使用時,也可將它作為非致命性的出錯處理。
????????EINTR 中斷函數調用。當中斷一個慢速系統調用時,也可將它作為非致命性出錯處理。
? ? ? ? 對于資源相關的非致命性出錯的典型恢復操作是延遲一段時間,然后重試。這種技術可應用于其他情況,例如,假設出錯表明一個網絡連接不再起作用,那么應用程序可以采取這種方法,在短時間延遲后,嘗試重新連接。一些應用使用指數補償算法,在每次迭代中等待更長時間。
? ? ? ? 最終,由應用的開發者決定在哪些情況下應用程序可以從出錯中恢復。如果能夠采用一種合理的恢復策略,那么可以避免應用程序異常終止,進而就能改善應用程序的健壯性。
7. 用戶標識
7.1 用戶ID
? ? ? ? 口令文件登錄項中的用戶ID(user ID)是一個數值,系統用它來標識各個不同的用戶。系統管理員在確定一個用戶的登錄名的同時,確認其用戶ID。用戶不能更改其用戶ID。通常每個用戶有一個唯一的用戶ID。下面將介紹內核如何使用用戶ID 來檢驗該用戶是否有執行某些操作的權限。
? ? ? ? 用戶ID為 0 的用戶為根用戶(root)或超級用戶(superuser)。在口令文件中,有一個登錄項,其登錄名為 root,我們稱這種用戶的特權為超級用戶特權。如果一個進程具有超級用戶特權,則大多數文件權限檢查都不再進行。
7.2 用戶的組ID
? ? ? ? 口令文件登錄項也包括用戶的組ID(group ID),它是一個數值。組ID 也是由系統管理員在指定用戶登錄名時分配的。一般來說,在口令文件中有多個登錄項具有相同的組ID。組被用于將一個或多個用戶集合到項目或部門中去,這種機制允許同組的各種成員之間共享資源(如文件)。組文件將組名映射為數值的組ID。組文件通常是 /etc/group。
? ? ? ? 使用數值的用戶ID 和數值的組ID 設置權限是歷史上形成的。對于磁盤上的每個文件,文件系統都存儲該文件所有者的用戶ID 和組ID。存儲這兩個值只需 4 個字節(假定每個都以雙字節的整形數存放)。如果使用完整 ASCII 登錄名和組名,則需要更多的磁盤空間。另外,在檢驗權限期間,比較字符串比比較整形數更消耗時間。
? ? ? ? 但是,對于用戶而言,使用名字比使用數值方便,所以口令文件中包含了登錄名和用戶名ID 直接的映射關系,而組文件中包含了組名跟組ID 之間的映射關系。例如,ls -l 命令使用口令文件將數值的用戶ID映射為登錄名,從而打印出文件所有者的登錄名。
以下程序功能:打印用戶ID 和組ID
print_uid_gid.c
#include <stdio.h>
#include <unistd.h>int main(int argc, char *argv[])
{printf("uid = %d, gid = %d\n", getuid(), getgid());return 0;
}
7.3 附屬組ID
? ? ? ? 除了口令文件中對一個登錄名指定一個組ID外,大多數 UNIX 系統版本還允許一個用戶屬于另外一些組,登錄時,讀文件 /etc/group,尋找列有該用戶作為其成員的前 16 個記錄項就可以得到該用戶的附屬組ID(supplementary group ID)。POSIX 要求系統至少支持 8個附屬組,實際上大多數 UNIX 系統至少支持 16 個附屬組。
8. 信號?
? ? ? ? 信號(signal)用于通知進程發生某種情況。例如,某一進程執行除法操作,其除數為 0,則將名為 SIGFPE(浮點異常)的信號發送給該進程。
? ? ? ? 進程有以下 3 種處理信號的方式。
? ? ? ? (1)忽略信號。有些信號表示硬件異常,例如,除以 0 或訪問進程地址空間以外的存儲單元等,因為這些異常產生的后果不確定,所以不推薦使用這種處理方式。
? ? ? ? (2)按系統默認方式處理。對于除數為 0,系統默認處理方式是終止該進程。
? ? ? ? (3)提供一個函數,信號發生時調用該函數,這被稱為捕捉該信號,通過提供自編的處理函數,我們就能知道什么時候產生了信號,并按期望的方式處理它。
? ? ? ? 很多情況都會產生信號。終端鍵盤上有兩種產生信號的方法:
????????(1)中斷鍵(interrupt key,通常是 delete 鍵或 ctrl+c)和退出鍵(quit key,通常是 ctrl+\),它們被用于中斷當前運行的進程。
? ? ? ? (2)另一種產生信號的方法是調用 kill 函數,在一個進程中調用 kill 函數就可以向另一個進程發送一個信號,當然這樣做也是有限制的,當向一個進程發送信號時,我們必須是那個進程的所有者或超級用戶。
以下代碼功能:測試捕捉 SIGINT 信號,即捕捉按下中斷鍵產生的信號
signal_int_demo.c
#include "../common/err.h"#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
#include <signal.h>
#include <string.h>
#include <stdlib.h>static void sig_interrupt(int signo);int main(int argc, char *argv[])
{char buf[MAX_BUF] = "";int status = 0;pid_t pid = 0;if (signal(SIGINT, sig_interrupt) == SIG_ERR)err_sys("signal error");printf("%% ");while (fgets(buf, MAX_BUF, stdin) != NULL){if (buf[strlen(buf)-1] == '\n')buf[strlen(buf)-1] = 0;if ((pid = fork()) < 0)err_sys("fork error");else if (pid == 0) // 子進程{execlp(buf, buf, (char*)NULL);err_ret("can't exec: %s", buf);exit(127);}// 父進程if ((pid = waitpid(pid, &status, 0)) < 0) // 父進程回收子進程資源err_sys("waitpid error");printf("%% ");}return 0;
}void sig_interrupt(int signo)
{printf("interrupt\n%% ");
}
9. 時間值
? ? ? ? 歷史上,UNIX 系統使用過兩種不同的時間值。
? ? ? ? (1)日歷時間。該值是自協調世界時(Coordinated Universal Time,UTC),自 1970年1月1日 00:00:00 這個特定時間以來所經過的秒數累計值(早期的手冊稱 UTC 為格林尼治標準時間)。這些時間值可用于記錄文件最近一次的修改時間等。系統基本數據類型 time_t 用于保存這種時間值。
? ? ? ? (2)進程時間,也被稱為 CPU時間,用以度量進程使用的中央處理器資源。進程時間以時鐘滴答計算,每秒鐘曾經取為50、60或100個時鐘滴答。系統基本數據類型 clock_t 保存這種時間值,用 sysconf 函數可以得到每秒的時鐘滴答數。
? ? ? ? 當度量一個進程的執行時間時,UNIX 系統為一個進程維護了3個進程時間值:
- 時鐘時間
- 用戶CPU時間
- 系統CPU時間
? ? ? ? 時鐘時間又稱為墻上時鐘時間(wall clock time),它是進程運行的時間總量,其值與系統中同時運行的進程數有關。
? ? ? ? 用戶CPU時間是執行用戶指令所用的時間量。系統CPU時間是為該進程執行內核程序所經歷的時間。例如,每當一個進程執行一個系統服務時,如 read()?或 write(),在內核內執行該服務所花費的時間就計入該進程的系統CPU時間。用戶CPU時間和系統CPU時間之和常被稱為 CPU時間。
? ? ? ? 要取得任一進程的時鐘時間、用戶CPU時間和系統CPU時間是很容易的,只要執行命令 time,其參數是要度量其執行時間的命令,例如:
$ cd /usr/include
$ time -p grep _POSIX_SOURCE */*.h > /dev/null
?10. 系統調用和庫函數
????????
11. 習題
(1)在系統上驗證,除根目錄外,目錄. 和目錄.. 是不同的
ls 命令的下面有兩個參數:
-i 打印文件或目錄的 i 節點編號。
-d 僅打印目錄信息,而不是打印目錄中所有文件的信息。
(2)分析以下打印進程ID的程序,說明進程ID為 852 和 853 的進程發生了什么?
?因為 UNIX 系統是多任務操作系統,在第一次執行程序打印了該程序的進程ID 后,系統中有其它兩個新的進程被執行,所以占用了進程ID 為 852 和 853 的進程ID 了,我們再執行程序打印進程ID 時,此時的進程ID 則為 854
(3)perror() 函數的參數是用 const 修飾定義的,而 strerror() 函數的整型參數沒有用 const 修飾定義,為什么?
????????因為 perror() 函數的 msg 參數是一個指針,perror() 函數內部就可以改變這個指針指向的字符串,用 const 修飾后,說明這個指針指向的內容是常量,perror() 函數內部就不能修改這個指針指向的字符串。
? ? ? ? 而 strerror() 函數的 errnum 參數是一個整數類型,是按值傳遞,因此即使 strerror() 函數內部對這個參數進行修改,對于函數外是無效的,也就沒必要使用 const 屬性。
(4)若日歷時間存放在帶符號的 32 位整型數中,那么到哪一年它會溢出?可以用什么方法擴展溢出浮點數?采用的策略是否與現有的應用相兼容?
? ? ? ? 2^31 / 365*24*60*60 約等于 68,而日歷時間是從1970年1月1日 00:00:00 開始的,所以 2038年會溢出。將 time_t 定義為 64 位整型數就可以了。如果它現在是 32 位整型數,修改為 64 位整型數后,要保證應用程序正常工作,應當對其重新編譯。但是還有一個更糟糕的地方,某些文件系統及備份介質是 32 位整型數存放時間的,對于這些同樣需要更新,但又需要能兼容舊的格式。
(5)若進程時間存放在帶符號的 32 位整形數中,而且每秒為 100 時間滴答,那么經過多少天后,該時間會溢出?
? ? ? ? 2^31 / 24*60*60*100 約等于 248 天
注:本文大部分內容摘抄自《UNIX環境高級編程》(第3版),少部分內容是自己的操作驗證以及寫的代碼實例,本文只作為學習筆記