1. 程式人生 > >第一章 UNIX 基礎知識

第一章 UNIX 基礎知識

ext ufs 系統管理員 一點 fine kernel pid_t 執行命令 國際

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 基礎知識