《UNIX环境高级编程 3rd》笔记(1 / 21):UNIX基础知识
文章目錄
- 引言
- UNIX體系結構
- 登錄
- 登錄名
- shell
- 文件和目錄
- 文件系統
- 文件名
- 路徑名
- 工作目錄
- 起始目錄
- 輸入和輸出
- 文件描述符
- 標準輸入、標準輸出和標準錯誤
- 不帶緩沖的IO
- 標準IO
- 程序和進程
- 程序
- 進程和進程ID
- 進程控制
- 線程和線程ID
- 出錯處理
- 出錯恢復
- 用戶標識
- 用戶ID
- 組ID
- 附屬組ID
- 信號
- 時間值
- 系統調用和庫函數
- 概述
- 區別一
- 例一
- 例二
- 小結
- 區別二
引言
所有操作系統都為它們所運行的程序提供服務。典型的服務包括:
- 執行新程序、
- 打開文件、
- 讀文件、
- 分配存儲區
- 獲得當前時間,
- …
本書集中闡述不同版本的UNIX操作系統所提供的服務。
(MyNote:將專注于Linux的。)
本章從程序員的角度快速瀏覽UNIX,對書中引用的一些術語和概念進行簡要的說明并給出實例。在以后各章中,將對這些概念做更詳細的說明。
對于初涉UNIX環境的程序員,本章還簡要介紹了UNIX提供的各種服務。
UNIX體系結構
從嚴格意義上說,可將操作系統定義為一種軟件,它控制計算機硬件資源,提供程序運行環境。我們通常將這種軟件稱為內核(kernel),因為它相對較小,而且位于環境的核心。下圖顯示了UNIX 系統的體系結構。
- 內核的接口被稱為系統調用(system call,圖中的陰影部分)。
- 公用函數庫構建在系統調用接口之上,應用程序既可使用公用函數庫,也可使用系統調用。
- shell是一個特殊的應用程序,為運行其他應用程序提供了一個接口。
從廣義上說,操作系統包括了內核和一些其他軟件,這些軟件使得計算機能夠發揮作用,并使計算機具有自己的特性。這里所說的其他軟件包括系統實用程序(system utility)、應用程序、shell 以及公用函數庫等。
例如,Linux是GNU操作系統使用的內核。一些人將這種操作系統稱為GNU/Linux操作系統,但是,更常見的是簡單地稱其為Linux。雖然這種表達方法在嚴格意義上講并不正確,但鑒于“操作系統”這個詞的雙重含義,這種叫法還是可以理解的(這樣的叫法更簡潔)。
登錄
登錄名
用戶在登錄UNIX系統時,先鍵入登錄名,然后鍵入口令。
系統在其口令文件(通常是/etc/passwd文件)中查看登錄名。
口令文件中的登錄項由7個以冒號分隔的字段組成,依次是:
目前,所有的系統已將加密口令移到另一個文件中。第6章將說明這種文件以及訪問它們的函數。
etc:Editable Text Configuration
shell
用戶登錄后,系統通常先顯示一些系統信息,然后用戶就可以向shell程序鍵入命令。(當用戶登錄時,某些系統啟動一個視窗管理程序,但最終總會有一個shell程序運行在一個視窗中)。
shell是一個命令行解釋器,它讀取用戶輸入,然后執行命令。shell 的用戶輸入通常來自于終端(交互式shell),有時則來自于文件(稱為shell腳本)。
下表總結了UNIX系統中常見的shell。
| Bourne shell | /bin/sh | ? |
| Bourne-again shell | /bin/bash | ? |
| C shell | /bin/csh | optional |
| Korn shell | /bin/ksh | optional |
| TENEX C shell | /bin/tcsh | optional |
文件和目錄
文件系統
UNIX文件系統是目錄和文件的一種層次結構,所有東西的起點是稱為根(root)的目錄,這個目錄的名稱是一個字符“/”。
目錄(directory)是一個包含目錄項的文件。在邏輯上,可以認為每個目錄項都包含:
-
一個文件名,
-
說明該文件屬性的信息
-
文件類型(是普通文件還是目錄等)、
-
文件大小、
-
文件所有者、
-
文件權限(其他用戶能否訪問該文件)
-
文件最后的修改時間
-
…
stat和fstat函數返回包含所有文件屬性的一個信息結構。
第4章將詳細說明文件的各種屬性。
文件名
目錄中的各個名字稱為文件名(filename)。只有斜線(/)和空字符這兩個字符不能出現在文件名中。斜線用來分隔構成路徑名的各文件名,空字符則用來終止一個路徑名。盡管如此,好的習慣還是只使用常用印刷字符的一個子集作為文件名字符(如果在文件名中使用了某些shell的特殊字符,則必須使用shell的引號機制來引用文件名,這會帶來很多麻煩)。
事實上,為了可移植性,POSIX.1推薦將文件名限制在以下字符集之內:字母(a ~ z、A ~ Z)、數字(0 ~ 9)、句點(.)、短橫線(-)和下劃線(_)。
創建新目錄時會自動創建了兩個文件名:
在最高層次的根目錄中,點點與點相同。
注意:空字符(‘\0’)和空格符(’ ')是分別不同的字符。
路徑名
由斜線分隔的一個或多個文件名組成的序列(也可以斜線開頭)構成路徑名(pathname),以斜線開頭的路徑名稱為絕對路徑名(absolute pathname)否則稱為相對路徑名(relative pathname)。相對路徑名指向相對于當前目錄的文件。
文件系統根的名字(/)是一個特殊的絕對路徑名,它不包含文件名。
實例:ls命令的簡要實現
#include "apue.h"//1. #include <dirent.h>//2. 為了使用opendir和readdir的函數原型,以及 dirent 結構的定義int main(int argc, char *argv[])//3.4. {DIR *dp;//6.struct dirent *dirp;if (argc != 2)err_quit("usage: ls directory_name");if ((dp = opendir(argv[1])) == NULL)//5.6.err_sys("can't open %s", argv[1]);//7.while ((dirp = readdir(dp)) != NULL)//5.6.printf("%s\n", dirp->d_name);closedir(dp);//5.exit(0);//8. }這個程序中,有很多細節需要考慮:
首先,其中包含了一個頭文件apue.h。本書中幾乎每一個程序都包含此頭文件。它包含了某些標準系統頭文件,定義了許多常量及函數原型,這些都將用于本書的各個實例中,附錄B列出了這一頭文件源碼。
接下來,我們包含了一個系統頭文件dirent.h,以便使用opendir和readdir的函數原型,以及 dirent 結構的定義。在其他一些系統里,這些定義被分成多個頭文件。比如,在Ubuntu 12.04中,/usr/include/dirent.h聲明了函數原型,并且包含bits/dirent.h,后者定義了dirent結構(真正存放在/usrlinclude/x86_64-linux-gnu/bits 下)。(MyNote:CentOS的存在/usr/include/dirent.h)
main函數的聲明使用了ISO C標準所使用的風格(下一章將對ISO C標準進行更多說明)。
程序獲取命令行的第1個參數argv[1]作為要列出其各個目錄項的目錄名。第7章將說
明main函數如何被調用,程序如何存取命令行參數和環境變量。
因為各種不同UNIX系統目錄項的實際格式是不一樣的,所以使用函數opendir、readdir和closedir對目錄進行處理。
opendir函數返回指向DIR結構的指針,我們將該指針傳送給readdir函數。我們并不關心DIR結構中包含了什么。然后,在循環中調用readdir來讀每個目錄項。它返回一個指向dirent結構的指針,而當目錄中己無目錄項可讀時則返回null指針。在dirent 結構中取出的只是每個目錄項的名字(d_name)。使用該名字,此后就可調用stat函數(見第4章)以獲得該文件的所有屬性。
程序調用了兩個自編的函數對錯誤進行處理:err_sys 和err_quit。如果用戶無權限訪問該目錄或目錄不存在,err_sys 函數打印一條消息(“Permission denied”或“Not a directory”),說明遇到了什么類型的錯誤。這兩個出錯處理函數在附錄B中說明,隨后將更多地敘述出錯處理。
當程序將結束時,它以參數0調用函數exit。函數exit終止程序。按慣例,參數0的意思是正常結束,參數值1~255則表示出錯。第8章將說明一個程序(如 shell 或我們所編寫的程序)如何獲得它所執行的另一個程序的exit狀態。
工作目錄
每個進程都有一個工作目錄(working directory),有時稱其為當前工作目錄(current working directory)。所有相對路徑名都從工作目錄開始解釋。進程可以用chdir函數更改其工作目錄。
例如,
-
相對路徑名doc/memo/joe指的是當前工作目錄中的doc目錄中的memo目錄中的文件(或目錄)joe。從該路徑名可以看出,doc和memo都應當是目錄,但是卻不能分辨joe是文件還是目錄。
-
路徑名/urs/lib/lint是一個絕對路徑名,它指的是根目錄中的usr目錄中的lib目錄中的文件(或目錄)lint.
起始目錄
登錄時,工作目錄設置為起始目錄(home directory),該起始目錄從口令文件(/etc/passwd)中相應用戶的登錄項中取得。
輸入和輸出
文件描述符
文件描述符(file descriptor)通常是一個小的非負整數,內核用以標識一個特定進程正在訪問的文件。當內核打開一個現有文件或創建一個新文件時,它都返回一個文件描述符。在讀、寫文件時,可以使用這個文件描述符。
標準輸入、標準輸出和標準錯誤
按慣例,每當運行一個新程序時,所有的shell都為其打開3個文件描述符,即標準輸入(standard input)、標準輸出(standard output)以及標準錯誤(standard error)。
如果不做特殊處理,例如就像簡單的命令ls,則這3個描述符都鏈接向終端。大多數shell都提供一種方法,使其中任何一個或所有這3個描述符都能重新定向到某個文件。例如:
ls > file.list執行ls命令,其標準輸出重新定向到名為file.list的文件。
不帶緩沖的IO
函數open、read、write、lseek 以及close提供了不帶緩沖的IO。這些函數都使用文件描述符。
實例:如果愿意從標準輸入讀,并向標準輸出寫,下面程序可用于復制任一UNIX普通文件。
#include "apue.h"//1.#define BUFFSIZE 4096int main(void) {int n;char buf[BUFFSIZE];//3.while ((n = read(STDIN_FILENO, buf, BUFFSIZE)) > 0)//1.2.3if (write(STDOUT_FILENO, buf, n) != n)//1.2.err_sys("write error");if (n < 0)err_sys("read error");exit(0); }頭文件<unistd.h> (apue.h中包含了此頭文件)及兩個常量STDIN_FILENO和STDOUT.FILENO是 POSTX標準的一部分(下一章將對此做更多的說明)。頭文件<unistd.h>包含了很多UNIX系統服務的函數原型,例如上例程序中調用的read和write。
兩個常量STDIN_FILENO和STDOUT_FILENO定義在<unistd.h>頭文件中,它們指定了標準輸入和標準輸出的文件描述符。在POSIX標準中,它們的值分別是0和1,但是考慮到可讀性,我們將使用這些名字來表示這些常量。
第3章將詳細討論BUFFSIZE常量,說明它的各種不同值將如何影響程序的效率。但是不管該常量的值如何,此程序總能復制任一UNIX普通文件。
read函數返回讀取的字節數,此值用作要寫的字節數。當到達輸入文件的尾端時,read返回0,程序停止執行。如果發生了一個讀錯誤,read返回-1。出錯時大多數系統函數返回-1。
如果將該程序編譯成標準名稱的a.out文件,并以下列方式執行它:
./a.out > data那么標準輸入是終端,標準輸出則重新定向至文件 data,標準錯誤也是終端。如果此輸出文件并不存在,則shell 會創建它。該程序將用戶鍵入的各行復制到標準輸出,鍵入文件結束符(通常是Ctrl+D)時,將終止本次復制。
若以下列方式執行該程序:
./ a.out < infile > outfile會將名為infile文件的內容復制到名為outfile的文件中。
第3章將更詳細地說明不帶緩沖的IO函數。
標準IO
標準IO函數為那些不帶緩沖的IO函數提供了一個帶緩沖的接口。使用標準I/O函數無需擔心如何選取最佳的緩沖區大小。使用標準IO函數還簡化了對輸入行的處理(常常發生在UNIX的應用程序中)。例如,fgets函數讀取一個完整的行,而read函數讀取指定字節數。
在第5章中我們將了解到,標準IO函數庫提供了使我們能夠控制該庫所使用的緩沖風格的函數。
我們最熟悉的標準IO函數是printf。在調用printf 的程序中,總是包含<stdio.h>(在本書中,該頭文件包含在apue.h中),該頭文件包括了所有標準IO函數的原型。
實例:下面程序的功能類似于前一個調用了read和 write的程序,第5章將對此程序進行更詳細的說明。它將標準輸入復制到標準輸出,也就能復制任一UNIX普通文件。
#include "apue.h"int main(void) {int c;while ((c = getc(stdin)) != EOF)if (putc(c, stdout) == EOF)err_sys("output error");if (ferror(stdin))err_sys("input error");exit(0); }函數getc一次讀取一個字符,然后函數putc將此字符寫到標準輸出。讀到輸入的最后一個字節時,getc返回常量EOF(該常量在<stdio.h>中定義)。標準IO常量stdin和 stdout也在頭文件<stdio.h>中定義,它們分別表示標準輸入和標準輸出。
程序和進程
程序
程序( program)是一個存儲在磁盤上某個目錄中的可執行文件。內核使用exec函數(7個exec函數之一),將程序讀入內存,并執行程序。第8章將說明這些exec函數。
進程和進程ID
程序的執行實例被稱為進程( process)。本書的每一頁幾乎都會使用這一術語。某些操作系統用任務( task)表示正在被執行的程序。
UNIX系統確保每個進程都有一個唯一的數字標識符,稱為進程ID(process ID)。進程ID總是一個非負整數。
#include "apue.h"int main(void) {printf("hello world from process ID %ld\n", (long)getpid());exit(0); }此程序運行時,它調用函數 getpid 得到其進程ID。我們將會在后面看到,getpid返回一個pid_t數據類型。我們不知道它的大小,僅知道的是標準會保證它能保存在一個長整型中。因為我們必須在printf函數中指定需要打印的每一個變量的大小,所以我們必須把它的值強制轉換為它可能會用到的最大的數據類型(這里是長整型)。
雖然大多數進程ID可以用整型表示,但用長整型可以提高可移植性。
進程控制
有3個用于進程控制的主要函數:fork、exec和waitpid。(exec函數有7種變體,但經常把它們統稱為exec函數。)
實例:UNIX系統的進程控制功能可以用一個簡單的程序說明。該程序從標準輸入讀取命令,然后執行這些命令。它類似于shell程序的基本實施部分。
#include "apue.h" #include <sys/wait.h>int main(void) {char buf[MAXLINE]; /* from apue.h */pid_t pid;int status;printf("%% "); /* print prompt (printf requires %% to print %) */while (fgets(buf, MAXLINE, stdin) != NULL) {//1.if (buf[strlen(buf) - 1] == '\n')//2.buf[strlen(buf) - 1] = 0; /* replace newline with null */if ((pid = fork()) < 0) {//3.err_sys("fork error");} else if (pid == 0) { /* child */execlp(buf, buf, (char *)0);//4.err_ret("couldn't execute: %s", buf);exit(127);}/* parent */if ((pid = waitpid(pid, &status, 0)) < 0)//5.err_sys("waitpid error");printf("%% ");}exit(0); }實驗過程:
[jallen@localhost intro]$ ./shell1 % date 2021年 05月 10日 星期一 12:21:54 PDT % ls getcputc hello.c Makefile shell1 shell2.c uidgid getcputc.c ls1 mycat shell1.c testerror uidgid.c hello ls1.c mycat.c shell2 testerror.c % pwd /home/jallen/Desktop/apue.3e/intro % who jallen tty1 2021-05-10 07:19 (:0) jallen pts/0 2021-05-10 07:58 (:0.0) % [jallen@localhost intro]$ ^C [jallen@localhost intro]$用標準IO函數fgets從標準輸入一次讀取一行。當鍵入文件結束符(通常是Ctrl+D)作為行的第一個字符時,fgets返回一個null 指針,于是循環停止,進程也就終止。第18章將說明所有特殊的終端字符(文件結束、退格字符、整行擦除等),以及如何改變它們。
因為fgets返回的每一行都以換行符終止,后隨一個null字節,因此用標準C函數strlen計算此字符串的長度,然后用一個null字節替換換行符。這樣做是因為execlp函數要求的參數是以null結束的而不是以換行符結束的。
調用fork創建一個新進程。新進程是調用進程的一個副本,我們稱調用進程為父進程,新創建的進程為子進程。fork對父進程返回新的子進程的進程ID(一個非負整數),對子進程則返回0。因為fork創建一個新進程,所以說它被調用一次(由父進程),但返回兩次(分別在父進程中和在子進程中)。
在子進程中,調用execlp以執行從標準輸入讀入的命令。這就用新的程序文件替換了子進程原先執行的程序文件。fork和跟隨其后的exec兩者的組合就是某些操作系統所稱的產生(spawn)一個新進程。在 UNIX系統中,這兩部分分離成兩個獨立的函數。第8章將對這些函數進行更多說明。
子進程調用execlp執行新程序文件,而父進程希望等待子進程終止,這是通過調用waitpid 實現的,其參數指定要等待的進程(即pid參數是子進程 ID)。waitpid函數返回子進程的終止狀態(status變量)。在我們這個簡單的程序中,沒有使用該值。如果需要,可以用此值準確地判定子進程是如何終止的。
該程序的最主要限制是不能向所執行的命令傳遞參數。例如不能指定要列出目錄項的目錄名,只能對當前工作目錄執行ls命令。為了傳遞參數,先要分析輸入行,然后用某種約定把參數分開(可能使用空格或制表符),再將分隔后的各個參數傳遞給execlp函數。盡管如此,此程序仍可用來說明UNIX系統的進程控制功能。
線程和線程ID
通常,一個進程只有一個控制線程(thread)——某一時刻執行的一組機器指令。對于某些問題,如果有多個控制線程分別作用于它的不同部分,那么解決起來就容易得多。另外,多個控制線程也可以充分利用多處理器系統的并行能力(MyNote:多線程優勢)。
一個進程內的所有線程共享同一地址空間、文件描述符、棧以及與進程相關的屬性。因為它們能訪問同一存儲區,所以各線程在訪問共享數據時需要采取同步措施以避免不一致性(MyNote:利用多線程優勢注意事項)。
與進程相同,線程也用ID標識。但是,線程ID只在它所屬的進程內起作用。一個進程中的線程ID在另一個進程中沒有意義。當在一進程中對某個特定線程進行處理時,我們可以使用該線程的ID引用它。
控制線程的函數與控制進程的函數類似,但另有一套。線程模型是在進程模型建立很久之后才被引入到UNIX系統中的,然而這兩種模型之間存在復雜的交互,在第12章中,我們會對此進行說明。
出錯處理
當UNIX系統函數出錯時,通常會返回一個負值,而且整型變量errno通常被設置為具有特定信息的值。例如,open 函數如果成功執行則返回一個非負文件描述符,如出錯則返回-1。
在open出錯時,有大約15種不同的errno值(文件不存在、權限問題等)。而有些函數對于出錯則使用另一種約定而不是返回負值。例如,大多數返回指向對象指針的函數,在出錯時會返回一個null指針。
文件<errno.h>中定義了errno以及可以賦與它的各種常量。這些常量都以字符E開頭。
man errnoPOSIX和ISO C將errno定義為一個符號,它擴展成為一個可修改的整形左值(lvalue)。它可以是一個包含出錯編號的整數,也可以是一個返回出錯編號指針的函數。以前使用的定義是:
extern int errno;但是在支持線程的環境中,多個線程共享進程地址空間,每個線程都有屬于它自己的局部errno以避免一個線程干擾另一個線程。例如,Linux支持多線程存取errno,將其定義為:
extern int *__errno_location (void); #define errno (*_errno_1ocation())(MyNote:不懂具體怎么存取。)
對于errno應當注意兩條規則:
C標準定義了兩個函數,它們用于打印出錯信息。
#include <string.h> char *strerror(int errnum); //返回值:指向消息字符串的指針strerror函數將errnum(通常就是errno值)映射為一個出錯消息字符串,并且返回此字符串的指針。
perror函數基于errno 的當前值,在標準錯誤上產生一條出錯消息,然后返回。
#include <stdio.h> void perror(const char *msg);它首先輸出由msg指向的字符串,然后是一個冒號,一個空格,接著是對應于errno值的出錯消息,最后是一個換行符。
實例:顯示了這兩個出錯函數的使用方法。
#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); }輸出結果
[jallen@localhost intro]$ ./testerror EACCES: Permission denied ./testerror: No such file or directory注意,我們將程序名(argv[0],其值是. /a.out)作為參數傳遞給perror。這是一個標準的UNIX慣例。使用這種方法,在程序作為管道的一部分執行時,例如:
prog1 < inputfile | prog2 | prog3 > outputfile我們就能分清3個程序中的哪一個產生了一條特定的出錯消息。
本書中的所有實例基本上都不直接調用strerror或perror,而是使用附錄B中的出錯函數。該附錄中的出錯函數使我們只用一條C語句就可利用ISOC的可變參數表功能處理出錯情況。
出錯恢復
可將在<errno.h>中定義的各種出錯分成兩類:
與資源相關的非致命性出錯包括:
- EAGAIN、
- ENFILE、
- ENOBUFS、
- ENOLCK、
- ENOSPC、
- EWOULDBLOCK,
- 有時ENOMEM也是非致命性出錯。
- 當EBUSY指明共享資源正在使用時,也可將它作為非致命性出錯處理。
- 當EINTR中斷一個慢速系統調用時,可將它作為非致命性出錯處理(第10章內容)。
對于資源相關的非致命性出錯的典型恢復操作是延遲一段時間,然后重試。這種技術可應用于其他情況。例如,假設出錯表明一個網絡連接不再起作用,那么應用程序可以采用這種方法,在短時間延遲后,嘗試重建該連接。一些應用使用指數補償算法,在每次迭代中等待更長時間。
最終,由應用的開發者決定在哪些情況下應用程序可以從出錯中恢復。如果能夠采用一種合理的恢復策略,那么可以避免應用程序異常終止,進而就能改善應用程序的健壯性。
用戶標識
用戶ID
口令文件登錄項中的用戶ID (user ID)是一個數值,它向系統標識各個不同的用戶。系統管理員在確定一個用戶的登錄名的同時,確定其用戶ID。用戶不能更改其用戶ID。通常每個用戶有一個唯一的用戶ID。
下面將介紹內核如何使用用戶ID來檢驗該用戶是否有執行某些操作的權限。
用戶ID為0的用戶為根用戶(root)或超級用戶(superuser)。在口令文件中,通常有一個登錄項,其登錄名為root,我們稱這種用戶的特權為超級用戶特權。我們將在第4章中看到,如果一個進程具有超級用戶特權,則大多數文件權限檢查都不再進行。
某些操作系統功能只向超級用戶提供,超級用戶對系統有自由的支配權。
組ID
口令文件登錄項也包括用戶的組ID(group ID),它是一個數值。組ID也是由系統管理員在指定用戶登錄名時分配的。一般來說,在口令文件中有多個登錄項具有相同的組ID。組被用于將若干用戶集合到項目或部門中去。這種機制允許同組的各個成員之間共享資源(如文件)。第4章將介紹可以通過設置文件的權限使組內所有成員都能訪問該文件,而組外用戶不能訪問。
組文件將組名映射為數值的組ID。組文件通常是/etc/group。
使用數值的用戶ID和數值的組ID設置權限是歷史上形成的。對于磁盤上的每個文件,文件系統都存儲該文件所有者的用戶ID和組ID。存儲這兩個值只需4個字節(假定每個都以雙字節的整型值存放)。如果使用完整ASCII登錄名和組名,則需更多的磁盤空間。另外,在檢驗權限期間,比較字符串較之比較整型數更消耗時間。
但是對于用戶而言,使用名字比使用數值方便,所以口令文件包含了登錄名和用戶ID之間的映射關系,而組文件則包含了組名和組D之間的映射關系。例如,ls -l命令使用口令文件將數值的用戶ID映射為登錄名,從而打印出文件所有者的登錄名。
實例:打印用戶ID和組ID。
#include "apue.h"int main(void) {printf("uid = %d, gid = %d\n", getuid(), getgid());exit(0); }附屬組ID
除了在口令文件中對一個登錄名指定一個組ID外,大多數UNIX系統版本還允許一個用戶屬于另外一些組。
這一功能是從4.2BSD開始的,它允許一個用戶屬于多至16個其他的組。登錄時,讀文件/etc/group,尋找列有該用戶作為其成員的前16個記錄項就可以得到該用戶的附屬組ID (supplementary group ID)。
在下一章將說明,POSIX要求系統至少應支持8個附屬組,實際上大多數系統至少支持16個附屬組。
信號
信號(signal)用于通知進程發生了某種情況。例如,若某一進程執行除法操作,其除數為0則將名為SIGFPE(浮點異常)的信號發送給該進程。進程有以下3種處理信號的方式:
很多情況都會產生信號。終端鍵盤上有兩種產生信號的方法,它們被用于中斷當前運行的進程:分別稱為
另一種產生信號的方法是調用kill函數。在一個進程中調用此函數就可向另一個進程發送一個信號。
當然這樣做也有些限制:當向一個進程發送信號時,我們必須是那個進程的所有者或者是超級用戶。
實例:
回憶一下基本的shell實例(在進程控制章節)。如果調用此程序,然后按下中斷鍵,則執行此程序的進程終止。產生這種后果的原因是:對于此信號(SIGINT)的系統默認動作是終止進程。該進程沒有告訴系統內核應該如何處理此信號,所以系統按默認方式終止該進程。
為了能捕捉到此信號,程序需要調用signal函數,其中指定了當產生STGINT信號時要調用的函數的名字。函數名為sig_int,當其被調用時,只是打印一條消息,然后打印一個新提示符。
#include "apue.h" #include <sys/wait.h>static void sig_int(int); /* our signal-catching function */int main(void) {char buf[MAXLINE]; /* from apue.h */pid_t pid;int status;if (signal(SIGINT, sig_int) == SIG_ERR)err_sys("signal error");printf("%% "); /* print prompt (printf requires %% to print %) */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) { /* child */execlp(buf, buf, (char *)0);err_ret("couldn't execute: %s", buf);exit(127);}/* parent */if ((pid = waitpid(pid, &status, 0)) < 0)err_sys("waitpid error");printf("%% ");}exit(0); }void sig_int(int signo) {printf("interrupt\n%% "); }實驗過程:
[jallen@localhost intro]$ ./shell2 % ls getcputc hello.c Makefile shell1 shell2.c uidgid getcputc.c ls1 mycat shell1.c testerror uidgid.c hello ls1.c mycat.c shell2 testerror.c % date 2021年 05月 10日 星期一 12:25:06 PDT % ^Cinterrupt ^C% interrupt ^C% interrupt date 2021年 05月 10日 星期一 12:25:17 PDT % % ls getcputc hello.c Makefile shell1 shell2.c uidgid getcputc.c ls1 mycat shell1.c testerror uidgid.c hello ls1.c mycat.c shell2 testerror.c %運行程序后,鍵盤輸入Ctrl + D,便會打印“interrupt”,但程序并未結束,還能運行。
在函數的返回類型前加上static,就是靜態函數。其特性如下:
- 靜態函數只能在聲明它的文件中可見,其他文件不能引用該函數
- 不同的文件可以使用相同名字的靜態函數,互不影響
Link
時間值
歷史上,UNIX系統使用過兩種不同的時間值。
日歷時間。該值是自協調世界時(Coordinated Universal Time,UTC)1970年1月1日00:00:00這個特定時間以來所經過的秒數累計值(早期的手冊稱UTC為格林尼治標準時間)。這些時間值可用于記錄文件最近一次的修改時間等。系統基本數據類型time_t用于保存這種時間值。
進程時間。也被稱為CPU時間,用以度量進程使用的中央處理器資源。進程時間以時鐘滴答計算。每秒鐘曾經取為50、60或100個時鐘滴答。系統基本數據類型clock_t保存這種時間值。第2章將說明如何用sysconf函數得到每秒的時鐘滴答數。
當度量一個進程的執行時間時(第3章),UNIX系統為一個進程維護了3個進程時間值:
- 時鐘時間,又稱為墻上時鐘時間(wall clock time),它是進程運行的時間總量,其值與系統中同時運行的進程數有關。每當在本書中提到時鐘時間時,都是在系統中沒有其他活動時進行度量的。
- 用戶CPU時間,是執行用戶指令所用的時間量。
- 系統CPU時間,是為該進程執行內核程序所經歷的時間。例如,每當一個進程執行一個系統服務時,如read或write,在內核內執行該服務所花費的時間就計入該進程的系統CPU時間。
用戶CPU時間和系統CPU時間之和常被稱為CPU時間(進程時間)。
要取得任一進程的時鐘時間、用戶時間和系統時間是很容易的——只要執行命令time(1),其參數是要度量其執行時間的命令,例如:
$ cd /usr/include $ time -p grep _POSIX_SOURCE */*.h > /dev/null real 0m0.81s user 0m0.11s sys 0m0.07stime命令的輸出格式與所使用的shell有關,其原因是某些shell并不運行/usr/bin/time,而是使用一個內置函數測量命令運行所使用的時間。
第8章將說明一個運行進程如何取得這3個時間。關于時間和日期的一般說明見第6章。
系統調用和庫函數
概述
所有的操作系統都提供多種服務的入口點,由此程序向內核請求服務。各種版本的UNIX實現都提供良好定義、數量有限、直接進入內核的入口點,這些入口點被稱為系統調用(system call)。Linux 3.2.0提供了380個系統調用,FreeBSD 8.0提供的系統調用超過450個。
系統調用接口總是在《UNIX程序員手冊》的第2部分中說明,是用C語言定義的,與具體系統如何調用一個系統調用的實現技術無關。這與很多早期的操作系統不同,那些系統按傳統方式用機器的匯編語言定義內核入口點。
UNIX所使用的技術是為每個系統調用在標準C庫中設置一個具有同樣名字的函數。用戶進程用標準C調用序列來調用這些函數,然后,函數又用系統所要求的技術調用相應的內核服務。例如,函數可將一個或多個C參數送入通用寄存器,然后執行某個產生軟中斷進入內核的機器指令。從應用角度考慮,可將系統調用視為C函數。
《UNIX程序員手冊》的第3部分定義了程序員可以使用的通用庫函數。雖然這些函數可能會調用一個或多個內核的系統調用,但是它們并不是內核的入口點。例如,printf 函數會調用write系統調用以輸出一個字符串,但函數strcpy(復制一個字符串)和atoi(將ASCII轉換為整數)并不使用任何內核的系統調用。(MyNote:庫函數可能不使用任何內核的系統調用。)
區別一
從實現者的角度來看,系統調用和庫函數之間有根本的區別,但從用戶角度來看,其區別并不重要。在本書中,系統調用和庫函數都以C函數的形式出現,兩者都為應用程序提供服務。但是,我們應當理解,如果希望的話,我們可以替換庫函數,但是系統調用通常是不能被替換的。
例一
以存儲空間分配函數malloc為例。有多種方法可以進行存儲空間分配及與其相關的無用空間回收操作(最佳適應、首次適應等),并不存在對所有程序都最優的一種技術。
UNIX系統調用中處理存儲空間分配的是sbrk,它不是一個通用的存儲器管理器。它按指定字節數增加或減少進程地址空間。如何管理該地址空間卻取決于進程。
存儲空間分配函數malloc實現一種特定類型的分配。如果我們不喜歡其操作方式,則可以定義自己的malloc函數,它很可能將使用sbrk 系統調用。(malloc可以自定義)
事實上,有很多軟件包,它們使用sbrk系統調用實現自己的存儲空間分配算法。下圖顯示了應用程序、malloc函數以及sbrk系統調用之間的關系。
(MyNote:個人理解庫函數是應用程序與系統調用之間中間層,可有更多操作空間。)
從中可見,兩者職責不同,內核中的系統調用分配一塊空間給進程,而庫函數malloc則在用戶層次管理這一空間。
例二
另一個可說明系統調用和庫函數之間差別的例子是,UNIX系統提供的判斷當前時間和日期的接口。
一些操作系統分別提供了一個返回時間的系統調用和另一個返回日期的系統調用。任何特殊的處理,例如正常時制和夏令時之間的轉換,由內核處理或要求人為干預。UNIX系統則不同,它只提供一個系統調用,該系統調用返回自協調世界時1970年1月1日零時這個特定時間以來所經過的秒數。
對該值的任何解釋,例如將其變換成人們可讀的、適用于本地時區的時間和日期,都留給用戶進程進行處理。在標準C庫中,提供了若干例程以處理大多數情況。這些庫函數處理各種細節,如各種夏令時算法等。
小結
應用程序既可以調用系統調用也可以調用庫函數。很多庫函數則會調用系統調用。下圖顯示這種差別。
區別二
系統調用和庫函數之間的另一個差別是:系統調用通常提供一種最小接口,而庫函數通常提供比較復雜的功能。我們從sbrk系統調用和malloc庫函數之間的差別中可以看到這一點。當我們比較不帶緩沖的I/O函數(見第3章)和標準IO函數(見第5章)時,還將看到這種差別。
進程控制系統調用(fork、exec和 wait)通常由用戶應用程序直接調用。但是為了簡化某些常見的情況,UNIX系統也提供了一些庫函數,如 system和popen。第8章將說明system函數的一種實現,它使用基本的進程控制系統調用。第10章還將強化這一實例以正確地處理信號。
(MyNote:系統調用簡單少功能,庫函數復雜多功能。)
為使讀者了解大多數程序員應用的UNIX系統接口,我們不得不既說明系統調用,又介紹某些庫函數。例如,若只描述sbrk系統調用,那么就會忽略很多應用程序使用的malloc庫函數。本書除了必須要區分兩者時,對系統調用和庫函數都使用函數(function)這一術語來表示。
總結
以上是生活随笔為你收集整理的《UNIX环境高级编程 3rd》笔记(1 / 21):UNIX基础知识的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java中int[]与Integer[]
- 下一篇: 如何保证接口的幂等性