APUE_Chapter01_Introduction_筆記總結

            ***PAY A TRIBUTE TO W.Richard Stevens***

Chapter01: Unix預覽

1.1 簡介

什麼是操作系統,操作系統其實就是爲程序的運行提供服務的平臺. 其中包括比如打開文件,讀寫文件,分配內存,獲取時間等等. Unix是很神奇也很深奧的系統,這章節帶大家語言Unix中都有些什麼,我們需要學習寫什麼. 所有在本章涉及到的知識點,在之後的章節中都會詳細解釋.


1.2 Unix整體架構

嚴格意義上來說,操作系統就是控制電腦硬件資源和分配環境供上層應用執行的軟件. 我們就稱其爲kernel. 也就是核心的意思. 一個Linux內核大概就是六七十M的樣子. 在內核外圍緊緊包裹着的就是system call.想必大家也聽過,這個就是調用內核的入口,這都是一系列的函數接口. 再外層一些就是library routines, 這是比如C庫函數,等等通用的函數庫. 相鄰的是shell. 到底什麼是shell,我們在後面會介紹,你其實可以簡單的把它當做是提供運行上層應用的特殊應用程序,學過鳥哥的私房菜的知道Linux是一堆命令,所以你現在也可以姑且當做是命令解釋器. 再外層就是應用程序了,應用程序可以直接訪問shell, libray routines 和system calls.
這裏寫圖片描述
大家可以man man看看具體的信息,這裏就會顯示出不同的manual page的功能:
man 1: 可執行文件或者是shell命令
man 2: 系統調用(內核提供的函數)
man 3: 庫函數調用(程序代碼庫中的函數)
man 4: 特殊的文件(一般是/dev下的文件,比如設備文件,塊兒文件等)
man 5: 一些文件的格式和規定行使說明(/etc/passwd這文件格式是這麼定義的等)
man 6: 遊戲(這一般用不到)
man 7: 各種各樣的雜情況(包括宏等)
man 8: 系統管理員命令(一般意義上是root)
man 9: kernel的東西

這裏寫圖片描述


1.3 登錄

當我們登錄進系統的時候,首先碰到的就是輸入用戶名和密碼,這時候,系統會去密碼文件中檢索匹配與否, 在以前這個文件是/etc/passwd, 但是現在已經加密後將密文等移動到了/etc/shadow等其他地方. 我們先看/etc/passwd:
sar:x:205:105:Stephen Rago:/home/sar:/bin/ksh
每條記錄都是七列: 登錄名,加密後的密碼,數字化的UID,數字化的GID,備註信息, 家目錄,使用的是什麼shell.
用戶輸入給shell的東西一般都是從終端(本身就是一個可交互的shell)或從一些文件中(這些文件就是shell腳本文件).
大家可以自行在查看本機支持什麼shell:
Ubuntu:
這裏寫圖片描述
NetBSD:
這裏寫圖片描述
這裏寫圖片描述
Bourne Shell: sh是存在於絕大多數的Unix系統中的.
Bourne-again shell: GNUshell,幾乎所有的linux都在用這個shell. 按照POSIX規則進行的編寫, 兼容sh, 並且支持csh和ksh的特點.
C shell: System V Release 4(SVR4)這種Unix種用着Cshell, 其中有一些sh沒有的特點: 任務控制, 歷史機制, 和命令行的編輯等.
Korn shell: sh的繼承者,開始用於SVR4, 向上兼容sh並且結合了Cshell多出來的那些功能.
不懂的系統默認使用不同的shell, 比如BSD系列一般是默認sh, Linux好多都是bash, 但是像Debian用的就死BSD的sh的替代品dash(Debian Almquist shell).


1.4 文件和目錄

Unix中的文件是樹的層級狀的
這裏寫圖片描述
一切都是起始於一個/(根目錄). 在Unix中,目錄也是文件,具體到文件到底包括什麼,在後面會講到. 只不過在目錄的結構體中是包含目錄內的文件名和描述文件的屬性的結構體. 每個文件(包括目錄)的屬性信息:
這裏寫圖片描述
文件類型(是常規文件, 目錄, 設備文件等), 文件大小,文件所有者,文件權限,最近修改時間. stat(2), fstat(2)是可以查到具體的結構體信息的.這些結構體中的屬性就是我們ls -al命令的時候顯示的文件的那些屬性.
但是對於大多數的Unix文件系統來說,內部文件的屬性是不放置在目錄塊中的,因爲如果一個文件有硬鏈接的時候很難去追蹤同步,所以一般都是放出來的,這些內容後面會詳解釋的.
這裏寫圖片描述
這就是目錄結構體中存放的信息
當我們在命名文件的時候,不能使用/和空格, 因爲在Unix中,/表示路徑名稱的分割,空格表示文件名命名的結束. 在不同的文件系統中對一些字符也是不進行識別的,都會以亂碼的形式輸出, 大家可以看看ls命令中的-q這個參數. 所以POSIX.1建議了文件名只能使用letters, numbers, period(.), dash(-), and underscore(_).老版的Unix System V限制文件名只能14字符,但是BSD就擴展到了255個字符.
每當我們創建一個新的目錄,裏面總會出來.和..兩個東西,.表示的就是當期那目錄, cd .你會發現沒有變化, ..表示的就是上級目錄, cd ..你會發現回到了你剛纔進入前的目錄中. 只有在/目錄下.和..一樣,因爲一切開始於/嘛!
如果一個路徑開始於/,那麼說明是絕對路徑,否則就是相對路徑, 相對路徑就是指相對於當前目錄的路徑.
接下來這段程序就是簡單列出目錄下的文件:
(參考: opendir(3), readdir(3), perror(3))

#include <stdio.h>
#include <sys/types.h>
#include <dirent.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[])
{
    DIR *dp;
    struct dirent *dirp;

    if(argc != 2){
        perror("usage: ls dir_name");
    }
    if((dp = opendir(argv[1])) == NULL)
    {
        perror("Can't open this directory");
    }
    /*struct dirent *readdir(DIR *dirp);*/
    while((dirp = readdir(dp)) != NULL)
    {
        /*
        struct dirent {
               ino_t          d_ino;       /* inode number
               off_t          d_off;       /* not an offset; see NOTES
               unsigned short d_reclen;    /* length of this record
               unsigned char  d_type;      /* type of file; not supported
                                              by all filesystem types
               char           d_name[256]; /* filename
           };
        */
        printf("%s\n", dirp->d_name);
    }
    closedir(dp);
    exit(0);//0 means ok
}

cc是C的編譯器,
這裏寫圖片描述
但是像GNU系統中C編譯器一般是gcc, 即使用cc也是指向了gcc
這裏寫圖片描述
我們可以從/usr/inclulde/下面找到所有庫函數的頭文件.
每個進程都有自己的工作目錄, 我們可以用chdir(2)來改變當前路徑. 家目錄就是我們剛進入系統的目錄,pwd可以
查詢當前目錄, 這個家目錄就是根據我們用戶登錄時, /etc/passwd中的文件.


1.5 輸入和輸出

文件描述符就是一些常規的非負整數,文件描述符是內核用來識別進程中使用到的文件的. 當打開一個已經存在的文件或者新建一個文件時,內核都會給用戶層返回文件描述符以供用戶對這個文件操作.
一般情況下,所有的shell都會默認打開三個文件描述符. 標準輸入(0),標準輸出(1),和標準錯誤(2). 如果沒有特殊的指定的話, 比如執行ls這三個描述符就會指向終端. 除非用一些>進行重定向的符號.
函數像open, read, write, lseek和close都是使用無緩衝的IO,所以它們都是直接操作fd的.
讓我們實現各簡單的從標準輸入讀入數據輸出到標準輸出中:
(參考: read(3), write(3))

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#define BUFFSIZE 4096

int main(void)
{
    int n;
    char buf[BUFFSIZE];
    /*ssize_t read(int fildes, void *buf, size_t nbyte);*/
    while((n = read(STDIN_FILENO, buf, BUFFSIZE)) > 0)
    {
    /*ssize_t write(int fildes, const void *buf, size_t nbyte);*/
        if(write(STDOUT_FILENO, buf, n) != n)
        {
            perror("write err");
        }
    }
    if(n < 0)
    {
        perror("read err");
    }
    exit(0);
}

read函數返回了讀到的字節數,這些字節數就是要write的字節數,當文件讀取結束時,read返回0, 並且程序結束. 如果read出錯了,就返回-1,對於絕大部分的系統函數而言,-1就是錯誤的標誌.
當我們執行./io > data的時候,標準輸入就是終端, 標準輸出就變成了data文件.並且標準錯誤也是指向了終端.當我們執行./io < infile >outfile, 文件名爲infile將會拷貝到outfile. 相比於無緩衝的I/O, 標準I/O可以緩解我們對bufersize的優化問題.比如,fgets就是針對整行進行讀取, getc是一個符號一個符號讀取,碰到EOF結束,而read函數則是需要對指定字節數進行讀取. 我們最常見的標準I/O就是printf. 我們來一個stdin,stdout但是用標準I/O的代碼:
(參考: getc(3), putc(3), ferror(3))
這裏稍微提一下fgetc和getc的區別:
分別查看manuage和stdio.h
這裏寫圖片描述
這裏寫圖片描述
發現man中說不同支出在於getc有可能是個宏,但是在最新的stdio.h中已經做了優化,所以兩者是一樣的使用的. 然後再man中有這樣以及話,”returns it as an unsigned char cast to an int”就是所將收到的char轉換成了int類型,這樣我們putc直接用就可以了.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
    int c;
/*int getc(FILE *stream);*/
    while((c = getc(stdin)) != EOF)
    {
        if(putc(c, stdout) == EOF)
        {
            perror("out_put err");
        }
    }
/*clearerr, feof, ferror, fileno - check and reset stream status*/
/*int ferror(FILE *stream);*/
    if(ferror(stdin))
    {
        perror("input err");
    }
    exit(0);
}

在這段代碼中,你肯定有疑問說,getc(FILE *stream)爲什麼傳入個stdin. 那麼這些東西都可以在stdio.h中找到
這裏寫圖片描述
這裏寫圖片描述
我們可以找到這些根源問題.


1.6 程序和進程

程序是放在硬盤上的東西,加載到內存中然後由內核去執行. 一個正在執行的程序就是進程. Unix表示每個進程都有獨一無二的進程ID.
走個程序:
(參考: getpid(2))

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>

int main(void)
{
    printf("hell world from process ID %ld\n", (long)getpid());
    exit(0);
}

這裏寫圖片描述
我們可以發現每次執行都不同,是因爲每次執行一個程序,都是一個單獨的進程. 然後在我們的程序中爲什麼要強轉long呢?是因爲getpid這個函數返回的是pid_t數據類型, 我們並不知道它的類型,我們知道的是保證適應long類型. 其實也可以是int 但是爲了更爲的保險. 這樣一說,進程就是相當於程序運行的平臺的了, 總共有三個主要的函數進行進程控制: fork(2), exec(3), waitpid(2). 其中exec是個家族函數,有7中不同的exec,但是我們一般統稱爲exec.
接下來的一段程序帶領大家從input輸入一條命令然後執行,這裏涉及到子父進程和回收問題,在之後都會詳解講解:
(參考: fgets(3), strlen(3), fork(2), execlp(3), waitpid(2))

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#define MAXLINE 4096

int main(void)
{
    char buf[MAXLINE];
    int status;
    pid_t pid;
    /*print prompt(printf requires %% to print %)*/
    printf("%%");
    /*char *fgets(char *s, int size, FILE *stream);*/
    while(fgets(buf, MAXLINE, stdin) != NULL)
    {
        if(buf[strlen(buf) - 1] == '\n')
        {
            //cut off
            buf[strlen(buf) - 1] = 0;
        }
        /*pid_t fork(void);*/
        if((pid = fork()) < 0)
        {
            perror("fork err");
        }
        else if(pid == 0)
        {
    /*
        int execlp(const char *file, const char *arg, ...
                      (char  *) NULL );
    */
            execlp(buf, buf, (char *)0);
            perror("couldn't execute\n");
            exit(127);
        }
        /*parent*/
        /*pid_t waitpid(pid_t pid, int *status, int options);*/
        if((pid = waitpid(pid, &status, 0) < 0))
        {
            perror("waitpid err");
        }
        printf("%%");
    }
    exit(0);
}

這個代碼中涉及到的知識點還是不少的:
1. 我們用標準I/O函數fgets讀取一次讀取一行.
2. 因爲execlp執行命令的時候的參數是要以空結尾的文件,但是我們標準輸入的時候,最後
敲入的是回車鍵,在Linux中就是\n, 所以我們要用C標準庫函數的strlen將最後一個字符
換成0, 表示null結束.
3. 我們通過fork來創建出子進程. pid爲0表示子進程. fork()函數是典型的一次調用返回兩次
的函數.
4. 在子進程中,用execlp執行來自標準輸入的命令. 就在這時就會有一個執行命令的進程取代
了孩子進程, 這種fork和exec的結合叫做在一些操作系統上生產新的進程.
5. 因爲兒子在執行程序,父親就等待孩子結束. 這就是調用waitpid,參數中有status表示返回
孩子的狀態碼,但是這個簡單程序中我們沒有接這個狀態.
這裏寫圖片描述
在一個進程中的所有線程共享地址空間, 文件描述符,棧和進程相關的屬性.每個線程在他們自己的棧上執行, 雖然在同一進程中有些線程是可以訪問其他線程的棧. 因爲他們能夠訪問同樣的內存,線程需要同步操作來共享數據來避免不一致性.這裏有些抽象,但是後面線程章節會着重處理.操作線程的函數與操作進程的函數是等價的.因爲線程加入Unix系統是在進程模型建立後很久才實行的.


1.7 錯誤處理

這裏寫圖片描述
大家可以看到這種RETURN VALUE在Unix的函數中是很常見的. -1表示發生了錯誤,並且errno中設置了具體的錯誤值,在這裏能找到具體的出錯原因.在errno.h中定義了errno是int類型,在Linux中errno(3)我們能夠看到左右的errno的宏定義, 在Unix中我們可以通過intro(2)來看到列舉, 但是NetBSD7.x中也可以從errno(3)中列舉出來.
這裏寫圖片描述
這裏寫圖片描述
但是在多線程的系統中,進程地址空間被多個線程共享,爲了線程間不被幹擾,所以每個線程都有自己的errno拷貝. 所以linux中對訪問errno定義爲:
extern int *__errno_location(void)
define errno(*__errno_location())
我們只應該在函數返回錯誤發生的時候去檢測errno的值,否則正常情況下它的值是不會被清除的,所以有可能這個函數執行的時候,即使成功,errno目前還是上個函數的錯誤狀態. 在Unix中沒有任何一個函數設置errno是0, 所以它不可能是0的.
我們想獲取到準確的errno的描述string,就通過C標準定義的strerror(3):
這裏寫圖片描述
這個函數返回了一個指向errno的message的指針.
在我們上面的代碼中,我將錯誤信息全部都用perror(3)進行輸出, 這個函數完整的輸出情況:參數msg: (根據errno)err_message 換行
來我們看一下上面提到的perror(3)和strerror(3):

#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    fprintf(stderr, "EACCES: %s\n", strerror(EACCES));
    errno = ENOENT;
    perror(argv[0]);
    exit(0);
}

這裏寫圖片描述
錯誤也是分着致命錯誤和非致命錯誤的. 致命錯誤就是無法恢復的錯誤, 頂多也就是打印到用戶界面或者記錄到日誌文件中. 非致命錯誤, 可以處理.絕大多數數的非致命錯誤都是暫時的,比如資源存儲報警, 甚至在一些系統上都不可能發生.
與資源相關的非致命錯誤是:
EAGAIN: Resource temporarily unavailable (may be the same value as EWOULDBLOCK) (POSIX.1)
資源暫時不可獲得.
EWOULDBLOCK: Operation would block (may be same value as EAGAIN) (POSIX.1)
操作被阻了
NFILE: Too many open files in system (POSIX.1); on Linux, this is probably a result of encountering
the /proc/sys/fs/file-max limit (see proc(5)).
打開太多文件了,也即是文件描述符佔用的太多了,默認是1024個fd,但是你可以重新設置.
ENOBUFS: No buffer space available (POSIX.1 (XSI STREAMS option))
沒有緩存空間了
ENOLCK:No locks available (POSIX.1)
鎖不夠了
ENOSPC: No space left on device (POSIX.1)
設備上空間不足了
ENOMEM: Not enough space (POSIX.1)
空間不做
EBUSY: Device or resource busy (POSIX.1)
當一個資源正在使用的情況下,這個錯誤碼也可以是非致命的

處理非致命錯誤的方式通常就是延遲或稍後重新嘗試.這些等待的參數完全取決於開發者的設定


1.8 用戶識別

用戶ID在我們的密碼文件中就是一個簡單的數值,系統用這個數值來識別用戶. 當我們用戶創建之後,系統會自動分配我們是改變它的.用戶ID是0的表示是root或者超級管理員.如果一個進程有超管權限,那麼幾乎所有的文件權限都是可訪問的.(free rein完全自由)形容root.
但是對於一些系統,比如MacOS客戶版本超管是不能用的.同樣的,在我們的passwd文件中也是有一列是防止組ID的,用戶組就是將多個用戶聚集在一起共享文件等一些資源組的單獨文件在/etc/group中. 在Unix系統中的任何一個文件系統都通過用戶ID和組ID識別文件的所有者, 存儲這些值分別是4字節的整數(舊版本的Unix是2字節). 這樣省空間,並且在登錄的時候驗證字串比驗證Integer要浪費資源.自從4.2BSD開始,用戶不進能夠擁有創建用戶的時候同時創建的組之外,還能另外加入最大16個組中.這就是傳說中的SGID(Supplementary Group IDs),這個是通過登錄時讀取/etc/group來獲取到的.


1.9 信號

信號是用來通知某個進程某些狀況的發生.比如一個進程進行了除以0的操作,那麼就會產生SIGFPE的信號(浮點數異常).進程有三種處理的方式:
1. 忽略.這不是個推薦的方法.
2. 讓默認的動作發生. 比如上面的除以0的操作,默認的操作就是終止進程.
3. 當信號產生的時候調用提供的處理函數.這就是叫做捕獲信號.通過捕獲,我們就能知道什麼時候信號發生了,並且按照我們
的意願進行處理.
在我們日常的操作中經常會產生信號操作.比如當我們敲入CTRL+C或者DELETE鍵的時候就產生了中斷信號,CTRL+BACKSLASH就產生了退出信號,這倆信號會中斷或者退出當前的進程.另外一種常見的產生信號的方式是kill(2)方法.我們也能夠用這個函數從這進程發送信號到另外的進程, 前提是我們必須是那個進程的owner.


1.10 時間

縱觀歷史,Unix維護這兩種不同的時間:
1. 日曆時間: 這個時間就是從紀元開始(1970.1.1 00:00:00 UTC),到現在的秒數,協調的通用時間(UTC). 這些時間值是用來記錄文件修改
的時間的.比如一種數據類型time_t就是存儲這些時間值的.
這裏寫圖片描述
我們可以看到,在bits/types.h中第139行有定義time_h這個數據類型.
這裏寫圖片描述
在bits/typesizes.h中對其__TIME_T_TYPE進行了定義.
2. 進程時間
這個就是傳說中的CPU時間, 並且通過一個進程測試CPU. 進程時間用時鐘週期進行測量.一般都是每秒鐘50,60或者100次.clock_t就是進程時間的數據類型.
當我們測試準確的進程時間的時候,我們會在Unix上得到三個時間:
·clock時間
表示這個進程執行了的時間,這個取決於有多少個進程在系統上正在跑.
·user CPU時間
表示這個進程在用戶空間手中掌控的時間
·系統CPU時間
表示這個進程在內核手中掌控的時間
所以一般情況下user CPU + system CPU就是CPU時間.
這裏寫圖片描述


1.11 系統調用和庫函數

任何的系統都會有指向調用內核的函數入口. Unix中就提供了定義規範的進入內核的入口點,這就是系統調用(system call)。具體系統調用函數
的數量會隨着內核版本不同而不同.比如4.4BSD就有110個,SVR4就有120個, Linux3.2有380個, FreeBSD8.0有450個.
在Unix上系統調用函數都有對應同名的C標準函數(這裏說的並不是man3, 而是直接說道man2), 用戶進程調用這些C函數,然後這些函數再通過一些機制喚醒內核服務. 比如,某個函數能 將一個或多個C參數放到通用寄存器中然後執行一些機器指令生成一個軟中斷給內核.
man3是標準的C函數庫,大部分是不能調用內核函數的,但是有一些可以,比如printf就是直接調用內核的write系統調用來輸出string.
這裏舉個例子,我們都清楚malloc(3)這個數是用來分配內存的,但是對這個函數目前沒有任何優化. 但是Unix內核處理內存分配是用的sbrk(2)
,這個函數通過增加或減少進程地址空間通過指定的字節數. 但是malloc只是實現了一種類型的內存管理,如果我們不想用這個函數,我們完全可以直接使用sbrk系統調用.大量的軟件其實都是用sbrk實現他們自己的內存非配算法的.
這裏寫圖片描述
類似的當前時間問題,Unix內核進行的工作是獲取到UTC從紀元開始到現在的秒數,然後具體根據時區等轉換成可讀性的時間是扔給了用戶
空間.
用戶空間是可以直接調用系統調用的,但是絕大部分的系統調用還是被庫函數調用.比如fork, exec, waitpid
這裏寫圖片描述


聯繫方式: [email protected]

發佈了19 篇原創文章 · 獲贊 2 · 訪問量 4153
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章