[單刷 APUE 系列]第一章——Unix 基礎知識[1]

原文來自靜雅齋,轉載請註明出處。javascript

文章系列緣由

Unix系統標準做爲目前開發最重要的系統標準,應當是必須懂得的,不管是服務端C++開發仍是安卓、iOS開發,實際上都和Unix環境開發密切相關,一個Unix環境開發者在入手安卓和iOS開發,是歷來不會去詢問很基礎的一些問題,好比動態庫靜態庫搜索路徑、編譯選項、環境變量的問題。目前筆者認爲《Unix環境高級編程》ver3是最好的一部Unix環境開發入手著做,可是因爲很多朋友在學習的時候都會碰到很多問題,因此筆者將本身的學習記錄從新整理成博客。java

Unix簡介

操做系統的狹義定義,是將操做系統定義爲一種控制計算機資源,提供程序運行環境的軟件,一般咱們稱之爲內核,內核提供接口供上層應用調用,也叫作System Call(系統調用)。同時,爲了方便應用程序使用內核,一般都會有公用函數庫,應用程序既可使用系統調用,也可使用公用函數庫。系統調用和公用函數庫實際上並非同一個東西,可是對於開發者來講,能夠看成同一個層,均可以使用C函數來調用。再向上,就是shell終端,做爲人機交互部分,最外層則是應用程序。
而從廣義上來講,操做系統就是一個包含了內核和必備系統軟件的集合,這些軟件是支持一個系統正常運轉使用、人機交互的最小要求。
目前來講,已經不存在真正的Unix系統了,由於自從AT&T公司封閉Unix源代碼,Unix的變種分支就出現了不少,其中學院派BSD,商業SystemV,和開源的Linux最重走到了最後。其中,BSD原先是基於AT&T開放代碼構建,後來Unix實驗室被賣給了Novell,Novell受權BSD開發Unix,可是去除了源自AT&T的源代碼,最終造成了BSD-4.4 Lite,也是目前不少類Unix操做系統的基石。商業SystemV則是AT&T聯合許多公司,用於解決Unix混亂的商業版本,因此後來的不少商業Unix都是基於SystemV release4版本,然後Unix被賣給Novell,最終到了X/OPEN Consortium,即後來的Open Group。
也就是說,只要實現了Unix環境的標準,就是一個Unix系統,筆者就是在Mac OS X系統下進行操做。程序員

Unix文件和目錄

Unix有一個哲學——一切皆是文件,文件在Unix環境中是很是重要的東西,Unix文件系統就是一個虛擬層次結構,全部目錄都掛載於/根目錄,文件夾也能夠被認爲是一種文件,設備也是一種文件,重要的Socket套接字也是一種文件,APUE第一個實例就是寫一個相似ls命令的實現。shell

#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->d_name);

    closedir(dp);
    exit(0);
}複製代碼

而後運行編程

> cc myls.c複製代碼

結果報錯:vim

example.c:1:10: fatal error: 'apue.h' file not found
#include "apue.h"
         ^
1 error generated.複製代碼

很多朋友就很是鬱悶,我就是按照上面的打的代碼啊,怎麼錯了?其實,apue.h頭文件是這本書裏自行編寫的頭文件,目的在於減小#include的代碼量,讓讀者更加專一於實際代碼。讀者能夠從附錄B 其餘源代碼這一章中找到apue.h頭文件。或者從官方網站直接下載源代碼,複製出apue.h文件。
咱們把頭文件放到myls.c同一目錄下,再次編譯函數

Undefined symbols for architecture x86_64:
  "_err_quit", referenced from:
      _main in example-c42025.o
  "_err_sys", referenced from:
      _main in example-c42025.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)複製代碼

好像錯誤更多了啊,可是仔細查看錯誤信息,裏面提示缺乏err_quiterr_sys的二進制代碼。衆所周知,編譯器編譯成爲二進制代碼分爲四個步驟:預處理、編譯、彙編和連接
預處理過程只是展開宏定義,處理條件編譯,展開include指令來插入實際代碼和一些雜項工做。編譯就是將預處理完成的代碼進行詞法分析、語義分析和代碼優化,最終生成相應的彙編代碼文件,而後就是彙編過程,將其翻譯爲二進制代碼,而後就是連接工做,使用連接器將各個目標文件組裝到一塊兒,解決符號依賴等問題,最終造成一個二進制文件,其中還分爲連接動態庫和靜態庫的不一樣。這些階段均可以指定編譯參數來讓編譯器只執行到某一階段,好比:工具

> cc -E xxx.c #只執行預處理階段
> cc -S xxx.c #只執行到編譯階段
> cc -c xxx.c #只執行到彙編階段複製代碼

根據不一樣到編譯器參數可能有一點點不一樣,可是均可以經過man命令來查看具體的編譯器參數,好比Linux下就是gcc,FreeBSD和Mac OS X就是使用clang做爲編譯器。
實際上上述問題就是由於缺乏庫文件,而附錄B裏面就有關於錯誤處理的代碼,咱們能夠直接從書的源代碼裏面找出來,而後使用學習

cc -c error.c
cc -c errorlog.c複製代碼

將其編譯爲二進制文件,而後使用ar -r xxx.o xxx.o將其打包爲靜態庫文件,把liberror.a放到lib文件夾下,把apue.h放到include文件夾下,而後咱們把頭文件命令改成#include "include/apue.h",使用編譯命令cc -L./lib -lerror myls.c就可以正確的編譯出二進制文件了。
最終,編譯出的二進制文件就能方便的顯示目錄下的全部文件。大數據

《Unix程序員手冊》是Unix系統必備的工具書,裏面使用節(section)來組織內容的,包含了許許多多的內容例如咱們使用man ls的時候,能夠看到裏面有ls(1)這就是表明這個內容是存放在第一節裏,通常來講,Unix系統參考手冊內部是以下組織

  1. User Commands and Utilities
  2. System Calls
  3. C Library Functions
  4. File formats
  5. Headers,tables and macros
  6. Games and demos
  7. Device and Network Interfaces
  8. Maintance and Accounting commands
  9. Device driver interfaces

咱們可使用man -s [section] [content]的形式來查找內容,並且Man Page是咱們在作開發的時候的得力助手,Mac OS X的內部手冊是BSD的用戶手冊。

輸入和輸出

文件描述符在C語言內部是一個非負整數,內核用其來標示一個進程訪問的文件,每一個進程都維護本身的文件描述符,按照標準規定,當一個進程運行時,都默認打開三個文件描述符,即標準輸入、標準輸出和標準錯誤,正常狀況下,這三個文件都指向終端輸出,可是在終端可使用重定向的方式將這三個文件描述符指向不一樣的地方。咱們能夠查看一下系統頭文件

> vim /usr/include/unistd.h複製代碼

咱們能夠找到

#define  STDIN_FILENO   0       /* standard input file descriptor */
#define STDOUT_FILENO   1       /* standard output file descriptor */
#define STDERR_FILENO   2       /* standard error file descriptor */複製代碼

實際上0、一、2就默認已經被使用了,若是咱們在此基礎上新打開一個文件,其實是增長在3的位置,並且每一個進程都有0、一、2的文件描述符。

程序和進程

程序是一段放置於磁盤上的二進制代碼,內核使用exec函數族來說進程讀入內存,而且執行程序,在內存中運行的程序實例被稱爲進程,Unix標準要求每一個進程都有惟一表示符(process ID即pid),pid是一個非負整數。

#include "include/apue.h"

int main(int argc, char *argv[])
{
    printf("hello world from process ID %lu\n", (unsigned long)getpid());
    exit(0);
}複製代碼

最終程序會輸出運行進程的pid,咱們能夠經過一下命令

man 2 getpid複製代碼

獲得關於此係統調用的函數原型

pid_t getpid(void);複製代碼

pid_t是標示進程id的數據類型,咱們能夠經過查看/usr/include/unistd.h文件,發現它裏面有#include <sys/_types/_pid_t.h#include <_types.h>兩條命令,而後咱們再打開/usr/include/sys/_types/_pid_t.h/usr/include/_types.h,其中,_pid_t.h裏有typedef __darwin_pid_t pid_t;,而/usr/include/_types.h裏有一條語句#include <sys/_types.h>,咱們再次打開/usr/include/sys/_types.h,裏面有typedef __uint32_t __darwin_id_t;,那麼很是簡單,實際上pid_t就是__uint32_t,能夠看出,系統使用了32位無符號整形存儲了pid_t類型,實際上,標準並無規定其大小,而是保證它能保存在一個長整形中。因此咱們將其轉換爲它可能用到的最大數據類型。

進程控制有主要三個函數:fork、exec和waitpid。(exec函數有7種變體,可是通常統稱exec函數)

#include "include/apue.h"
#include <sys/wait.h>

int main(int argc, char *argv[])
{
    char buf[MAXLINE];
    pid_t pid;

    printf("%% ");
    while (fgets(buf, MAXLINE, 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, NULL);
            err_ret("couldn't excute: %s", buf);
            exit(127);
        }
        if ((pid = waitpid(pid, NULL, 0)) < 0)
            err_sys("waitpid error");
        printf("%% ");
    }
    exit(0);
}複製代碼
  • 在這個程序裏使用了標準I/O函數fgets從標準輸入讀取一行,當輸入文件結束符Ctrl+D時候,fgets返回一個null指針,而後就會直接執行exit(0);讓進程退出
  • fgets每次讀取的一行都以換行符終止,因此buf最後兩個字符就是'\n'和'\0',可是execlp函數要求參數必須以'\0'結尾,不須要'\n'換行符,因此咱們使用'\0'字符先替換了'\n',讓execlp函數能順利執行
  • 調用fork函數建立一個新進程,新進程是父進程的副本,fork對父進程返回子進程的pid,對子進程則返回整數0,而且子進程是徹底複製父進程的當前內存空間,因此子進程一開始執行的代碼就是父進程正在執行的代碼,因此說fork函數被調用一次(在父進程調用),但返回兩次(父進程和子進程都獲得返回值)
  • 根據fork函數的返回值判斷當前進程是子進程仍是父進程,在子進程中,調用execlp執行命令,使用新的程序文件替換了原先子進程的程序文件。而父進程則使用waitpid等待子進程的終止,當一切執行完畢,則打印出新的提示符%

一般狀況下,一個進程只有一個線程運行,可是Unix實際上存在線程模型,可以讓咱們建立管理線程,從而更好的共享進程的內存,而且充分利用多處理器系統的並行能力。一個進程內的全部線程共享同一內存地址、文件描述符、棧和進程屬性,從而線程建立是很是輕量化的,開銷不多,可是因爲共享資源,因此各個線程在訪問資源的時候必須採起同步措施防止出現資源同時訪問等問題。與進程類似,線程能夠說是一個輕量化的進程,它也有本身的線程ID,可是線程ID只在本身的進程內起做用。

相關文章
相關標籤/搜索