本系列文章是學習被譽爲UNIX編程聖經的《UNIX環境高級編程》的讀書筆記。《UNIX環境高級編程》的英文全稱爲《Advanced Programming in the UNIX Environment》,簡稱《APUE》,其作者是UNIX和網絡技術領域的知名專家W.Richard Stevens。
本書描述了UNIX系統的程序設計接口:系統調用接口和標準C庫提供的很多函數。與大多數操作系統一樣,UNIX爲程序運行提供了大量的服務--打開文件,讀文件,啓動一個新程序,分配存儲區,獲取當前時間等等。這些服務被稱爲系統調用接口(system call interface)。另外標準C庫提供了大量廣泛應用於C程序中的函數。
本書共分爲6個部分:
(1)對UNIX程序設計基本概念和術語的簡要描述,以及對各種UNIX標準化工作和不同UNIX實現的討論;
(2)I/O:不帶緩衝的I/O,文件和目錄,標準I/O庫和標準系統數據文件;
(3)進程:UNIX進程的環境,進程控制,進程之間的關係和信號;
(4)更多的I/0:終端I/O,高級I/O和守護進程;
(5)IPC:進程間通信;
(6)實例;
接下來就正式進入《UNIX環境高級編程》的學習了。本章將從程序設計人員的角度快速瀏覽UNIX,對書中引用的一些術語和概念進行簡要的說明,後續文章再對這些概念作更詳細的說明。
UNIX體系結構:
在嚴格意義上,可將操作系統定義爲一種軟件,它控制計算機硬件資源,提供程序運行環境。一般而言,我們稱這種軟件爲內核(kernel)。 內核的接口被稱爲系統調用(system call)。公用函數庫構建在系統調用接口之上。應用軟件既可以使用公用函數庫,也可以使用系統調用。
Shell是一種特殊的應用程序,它爲運行其他的應用程序提供了一個接口。
在廣義上,操作系統包括內核和一些其它軟件,這些軟件使得計算機能夠發揮作用,這些軟件包括系統實用程序(system utilities),應用軟件,shell以及公用函數庫等。
例如,Linux是GNU操作系統使用的內核,僅僅是GNU操作系統的關鍵組件之一,GNU操作系統還包括很多其它的自由軟件,例如bash,Tex,GNU C庫等等。所以嚴格意義上這些操作系統的發行版應該稱爲GNU/Linux,但是很多人將其簡稱爲Linux。雖然這種表達方法不正確,但是“操作系統”本省就具有雙重含義,所以這也是可以理解的。關於GNU和Linux的關係,可以參考:http://www.gnu.org/gnu/linux-and-gnu.en.html。
登錄:
登錄名:用戶在登錄UNIX系統時,需要輸入登錄名及相應的口令。系統在其口令文件(通常是/etc/passwd文件)中查看登錄名。
shell:
shell:shell是一個命令行解釋器,它讀取用戶輸入,然後執行命令。用戶通常通過終端(交互式shell),有時則通過文件(shell script)向shell進行輸入。用戶在登錄後,系統從口令文件中相應用戶登錄項的最後一個字段中瞭解到應該爲該登錄用戶執行哪一個shell。
文件和目錄:
文件系統:UNIX文件系統是目錄和文件組成的一種層次結構,目錄的起點稱爲根(root),寫爲"/"。目錄是一個包含許多目錄項的文件,邏輯上,可以認爲每個目錄項都包含一個文件名,同時還包含說明該文件屬性的信息。
文件名:目錄中的各個名字稱爲文件名,不能出現在文件名中的字符只有斜線("/")和空字符(null)。斜線用來分隔構成路徑名的各文件名,空字符則用來終止一個路徑名。
在創建新目錄時,會自動創建兩個文件名:.(稱爲點)和..(稱爲點-點)。點指當前目錄,點點指父目錄。在根目錄中,點和點點相同。
現如今,幾乎所有商品化的UNIX系統都支持至少255個字符的文件名。
路徑名:一個或多個斜線分隔的文件名序列(也可以斜線開頭)構成路徑名。以斜線開頭的路徑名稱爲絕對路徑,否則稱爲相對路徑。相對路徑引用相當於當前目錄的文件。
下列程序是ls(1)命令的簡要實現,用於列出某個目錄下的所有文件名。
/*
* Copyright (C) [email protected]
*/
#include <stdio.h>
#include <stdlib.h>
#include <dirent.h>
int
main(int argc, char *argv[])
{
if (argc != 2) {
printf("usage: ls directory_name\n");
exit(1);
}
DIR *dp;
struct dirent *dirp;
if ( (dp = opendir(argv[1])) == NULL) {
printf("can't open %s\n", argv[1]);
exit(2);
}
while ( (dirp = readdir(dp)) != NULL) {
printf("%s\n", dirp -> d_name);
}
closedir(dp);
exit(0);
}
ls(1)這種表示方法是UNIX系統的慣用方法,表示引用UNIX手冊集中的某個特定項。ls(1)表示引用第一部分中的ls項。各部分通常用數字1-8表示,每個部分中的各項則按字母順序排列。在我的CentOS上,這八個部分分別爲:
目前,大多數手冊都以電子文檔的形式提供。所以如果使用的是聯機手冊,可以使用如下命令查看ls命令手冊頁:man 1 ls 或 man -s1 ls。
由於不同的UNIX系統的目錄項的格式是不同的,因此上個程序採用opendir,readdir,closedir函數來對該目錄進行處理。關於該程序的更多細節將在後續文章進一步介紹。
工作目錄:每個進程都有一個工作目錄,有時將其稱爲當前工作目錄。所有相對路徑名都從工作目錄開始解釋。
起始目錄:登錄時,工作目錄設置爲起始目錄,起始目錄從口令文件中的相應用戶的登錄項中取得。
輸入與輸出:
文件描述符:文件描述符通常是一個小的非負整數,內核用它表示一個特定進程正在訪問的文件。當內核打一個已有文件或創建一個新文件時,它返回一個文件描述符。之後讀寫文件時,就可以使用該文件描述符。
標準輸入、標準輸出、標準出錯:按照慣例,每當運行一個新程序時,shell就爲其打開三個文件描述符:標準輸入、標準輸出、標準出錯。如果程序中沒有做什麼特殊處理,則這三個文件描述符都鏈向終端。大多數shell也提供相應的方法,讓這三個文件描述符重定向到某個文件。
不帶緩衝的I/O:函數open,read,write,lseek以及close都提供了不用緩衝的I/O,這些函數都使用文件描述符
下列程序將標準輸入複製到標準輸出:
/*
* Copyright (C) [email protected]
*/
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#define BUF_SIZE 4096
int
main(int argc, char *argv[])
{
int n;
char buffer[BUF_SIZE];
while ( (n = read(STDIN_FILENO, buffer, BUF_SIZE)) > 0) {
if (write(STDOUT_FILENO, buffer, n) != n) {
printf("write error\n");
exit(1);
}
}
if (n < 0) {
printf("read error\n");
exit(2);
}
exit(0);
}
頭文件<unistd.h>以及兩個常量STDIN_FILENO以及STDOUT_FILENO都是POSIX標準的一部分。該頭文件包含了許多UNIX系統服務的函數原型。STDIN_FILENO以及STDOUT_FILENO分別指定了標準輸入和標準輸出的文件描述符。它們的典型值是0和1,但是考慮到可移植性,最好還是使用標識符。
在shell中運行該程序時,通過重定向,可以用於複製任何一個UNIX普通文件。
標準I/O:標準I/O函數提供一種對不帶緩衝I/O函數的帶緩衝的接口。使用標準I/O函數可以無需擔心如何選取最佳的緩衝區大小,而且簡化了對輸入行的處理。
下列程序用標準I/O函數實現了將標準輸入複製到標準輸出:
/*
* Copyright (C) [email protected]
*/
#include <stdio.h>
#include <stdlib.h>
int
main(int argc, char *argv[])
{
int c;
while ( (c = getc(stdin)) != EOF) {
if (putc(c, stdout) == EOF) {
printf("put char error\n");
exit(1);
}
}
if(ferror(stdin)) {
printf("get char error\n");
exit(2);
}
exit(0);
}
getc一次讀取一個字符,putc將該字符寫到標準輸出。程序中的標準輸入常量 stdin,標準輸出常量stdout,以及文件結束符EOF,都定義在<stdio.h>中。
程序和進程
程序:程序是存在磁盤上、處於某個目錄中的可執行文件。使用6個exec函數中的一個由內核將該程序讀入存儲器,並使其執行。
進程和進程ID:程序的執行實例被稱爲進程。UNIX系統確保每個進程都有一個唯一的數字標識符,稱爲進程ID。進程ID總是一個非負整數。
下列程序通過調用getpid函數來獲取自己的進程ID:
/*
* Copyright (C) [email protected]
*/
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
printf("hello world from process ID : %d\n", getpid());
exit(0);
}
進程控制:有三個用於進程控制的主要函數:fork,exec,waitpid。(exec函數有6種變體,但把它們統稱爲exec函數)。
下列程序展示UNIX系統的進程控制功能,該程序從標準輸入中讀取命令,然後執行這些命令,是一個類似於shell的簡單實現:
/*
* Copyright (C) [email protected]
*/
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define MAX_LINE 32
int main(void)
{
char buf[MAX_LINE];
pid_t pid;
int status;
printf("%% ");
while (fgets(buf, MAX_LINE, stdin) != NULL) {
if (buf[strlen(buf) - 1] == '\n') {
buf[strlen(buf) - 1] = '\0';
}
if ( (pid = fork()) < 0) {
printf("fork error");
exit(1);
} else if (pid == 0) {
/* child process */
execlp(buf, buf, (char*)0);
printf("can't execute %s\n", buf);
exit(2);
}
if ( (pid = waitpid(pid, &status, 0)) < 0) {
printf("waitpid error\n");
exit(3);
}
printf("%% ");
}
exit(0);
}
該程序最主要的部分就是調用fork函數創建一個新進程。新進程是調用進程的複製品,因此調用進程稱爲父進程,新創建的進程爲子進程。fork函數向父進程返回子進程的進程ID(非負),向子進程返回0。所以fork函數是調用一次,返回兩次(分別在父進程和子進程中)。
在子進程中,調用execlp函數執行從標準輸入讀入的命令。這就用新的程序文件代替了子進程原先執行的程序文件。而父進程調用waitpid函數等待子進程終止。
關於該程序的更多細節將在後續文章進一步學習。
線程和線程ID:
通常,一個進程只有一個控制線程。但是對於某些問題,如果不同部分各使用一個控制線程,那麼解決問題會更加容易,而且多個控制線程也能充分地利用多處理器系統的並行性。
在一個進程內所有線程共享同一地址空間,文件描述符,以及相關的進程屬性。因爲所有線程都能訪問同一存儲區,所以各線程在訪問共享數據時需要採取同步措施以避免不一致性。
線程也用ID標識,但是線程ID只在它所屬的進程內起作用。一個進程中的線程ID在另一個進程中並無意義。
在進程模型建立很久之後,線程模型才被引入UNIX系統中,這兩個模型間存在複雜的相互作用。