2018-1-30
一、UNIX、C語言以及Linux的歷史回顧
1. UNIX簡史、C語言的誕生
1969年,貝爾實驗室的Ken Thompson首次實現了UNIX系統。
1973年,C語言步入成熟期,人們以其重寫了幾乎整個UNIX內核。
2. UNIX兩大分支:BSD、System V
1969~1979年間,UNIX歷經了多個版本,其中從第七版起,UNIX分裂為兩大分支:BSD和System V。
加州大學伯克利分校為UNIX開發了許多新特性,然后發布了屬於自己的UNIX發布版——BSD。
AT&T公司被獲准銷售UNIX,這催生出了另一種UNIX的變種——System V。
3. Linux簡史:GNU項目、Linux內核
GNU項目開發了一套幾乎完備且可以自由分發的UNIX實現,但獨缺一顆能夠有效運作的內核。
1991年,Linus Torvalds開發出Linux內核,隨后許多程序員也加入到了改進內核的行列中。
4. POSIX標准:關於操作系統接口方面
二、一系列與Linux系統編程相關的基本概念
1. 內核:操作系統的核心
指管理和分配計算機資源的核心層軟件。即其為管理計算機的有限資源提供了相關的軟件層。
內核的職責包括:進程調度、內存管理、提供文件系統、創建和終止進程、訪問設備、提供應用編程接口等。
內核態&用戶態:處理器架構允許CPU在用戶態或核心態下運行。
2. shell:命令解釋器
一個具有特殊用途的程序,用於讀取用戶輸入的命令,並執行相應的程序以響應命令。
“login shell”指的是:用戶剛登陸系統時,由系統創建,用以運行shell的進程。
3. 文件描述符:指代打開的文件
獲取的方法:調用open()
由shell啟動的進程會繼承3個已打開的文件描述符:0為標准輸入,指代為進程提供輸入的文件;1為標准輸出,指代供進程寫入輸出的文件;2為標准錯誤,指代供進程寫入錯誤信息或異常通告的文件。在交互式shell或程序中,上述三者指向終端。
4. 程序
兩種形式:源碼形式、二進制機器語言指令
5. 進程
內存布局:程序的指令、數據(程序使用的靜態變量)、堆、棧
6. 內存映射
7. 靜態庫和目標庫:一種文件,供其他應用程序調用
8. 信號:軟件中斷
進程收到信號,就意味着某一事件或異常情況的發生。
進程間通信的方式之一。
9. 線程:類似於共享同一虛擬內存及一干其他屬性的進程
執行相同的程序代碼,共享同一數據區域和堆,但每個線程都擁有屬於自己的棧(裝載本地變量和函數調用鏈接信息)。
10. /proc文件系統:一種虛擬文件系統
由一組目錄和文件組成,以文件系統目錄和文件形式,提供一個指向內核數據結構的接口。
2018-1-31
三、掌握系統編程的先決條件
1. 系統調用:內核入口
借助該機制,進程可以請求內核(以自己的名義)去執行某些動作。
幕后步驟:進程通過調用C語言函數庫中的外殼函數發起系統調用,外殼函數執行一條中斷機器指令,並執行該中斷向量所指向的代碼。
小結:進程向內核請求服務,與用戶空間的函數調用相比,系統調用會產生顯著的開銷,因為系統需要臨時性地切換到核心態,此外,內核還需驗證系統調用的參數、用戶內存和內核內存之間也有數據需要傳遞。
2. 庫函數:組成C語言標准函數庫
部分庫函數不會使用任何系統調用,其他庫函數則構建於系統調用層之上。
設計庫函數是為了提供比底層系統調用更為方便的調用接口。
小結:標准的C語言函數庫提供了大量的庫函數,有些庫函數會利用系統調用來完成工作,而另一些庫函數則完全在用戶空間內執行任務。
3. GNU C語言函數庫(glibc):Linux上最常用的實現
獲取glibc版本
4. 關於來自系統調用的錯誤
系統調用失敗(由函數的返回值來表明)后,會將全局整形變量errno設置為一個正值,以標識具體的錯誤。
根據errno的值打印錯誤消息:perror()、strerror()
小結:大多數系統調用和庫函數都會返回一個狀態值,以表明調用成功與否。
5. 常用的頭文件及錯誤診斷函數
| 頭文件 | 功能說明 |
| stdio.h | 標准I/O函數 |
| stdlib.h | 常用的庫函數原型,加上常量EXIT_SUCCESS和EXIT_FAILURE |
| sys/types.h | 許多程序使用到的類型定義 |
| unistd.h | 許多系統調用的原型 |
| errno.h | 聲明errno,定義error常量 |
| string.h | 常用的字符串處理函數 |
常用的錯誤診斷函數:errMsg、errExit、err_exit、errExitEN
6. 系統數據類型:降低不同UNIX系統間相互移植的難度
每種類型的定義均使用C語言的typedef特性,如pid_t數據類型用以表示進程ID。
大多數命名均以_t結尾,許多都聲明於頭文件<sys/types.h>中。
應用程序應采用這些類型定義來聲明其使用的變量,才能保證可移植性。如聲明“pid_t mypid;”將允許應用程序在任何系統上正確表示進程ID。
常用系統數據類型的描述見P51。
打印系統數據類型:強制轉換為某一類型。
2018-2-8
四、文件I/O
1. 文件描述符
指代打開的文件,包括管道(PIPE)、FIFO、socket、終端設備和普通文件。
針對每個進程,文件描述符都自成一套。
大多數程序都使用的3種標准的文件描述符:0、1、2。細節:在程序運行之前,shell代表程序打開這3個文件描述符。更確切地說,程序繼承了shell文件描述符的副本——在shell的日常操作中,這3個文件描述符始終是打開的。(在交互式shell中,這個文件描述符通常指向shell運行所在的終端。)如果命令行指定對輸入/輸出進行重定向操作,那么shell會對文件描述符做適當修改,然后再啟動程序。
2. 文件I/O操作的幾個主要系統調用:open()、creat()、read()、write()、close()
fd = open(pathname, flags, mode)
numread = read(fd, buffer, count)
numwritten = write(fd, buffer, count)
status = close(fd)
3. 打開一個文件:open()
函數原型:int open(const char *pathname, int flags, mode_t mode);
功能:open()調用既能打開一個文件,也能創建並打開一個新文件。
返回:文件描述符;失敗則返回-1,並將errno置為相應的錯誤標志。
參數:flags用於指定文件的訪問模式,而mode則指定了文件的訪問權限。如果open()並未指定O_CREAT標志,則可以省略mode參數。
補充:flags參數值可參考P60,調用發生錯誤時的一些errno值的設置可參考P63。
4. 讀取文件內容:read()
函數原型:ssize_t read(int fd, void *buffer, size_t count);
功能:從文件描述符fd所指代的打開文件中讀取數據。
返回:實際讀取的字節數,遇到文件結束(EOF)則返回0;失敗則返回-1。
參數:count指定最多能讀取的字節數,而buffer則提供用來存放數據的內存緩沖區地址(緩沖區至少應有count個字節)。
補充:系統調用不會分配內存緩沖區用以返回信息給調用者,故需預先分配大小合適的緩沖區並將緩沖區指針傳遞給系統調用。
5. 數據寫入文件:write()
函數原型:ssize_t write(int fd, void *buffer, size_t count);
功能:將數據寫入一個已打開的文件中。
返回:實際寫入文件的字節數。
參數:文件描述符fd指代要寫入的文件,而buffer則為要寫入文件中數據的內存地址,count參數為欲從buffer寫入文件的數據字節數。
補充:對磁盤文件執行I/O操作時,write()調用成功並不能保證數據已經寫入磁盤。因為為了減少磁盤活動量和加快write()系統調用,內核會緩存磁盤的I/O操作。
6. 關閉文件:close()
函數原型:int close(int fd);
功能:關閉一個打開的文件描述符,並將其釋放回調用進程,供該進程繼續使用。
返回:失敗則返回-1,錯誤類型有:企圖關閉一個未打開的文件描述符,或者兩次關閉同一文件描述符等。
7. 改變文件偏移量:lseek()
函數原型:off_t lseek(int fd, off_t offset, int whence);
功能:依照offset和whence參數值調整該文件的偏移量。
返回:新的文件偏移量。
參數:參數offset的單位為字節;參數whence表明參考基點,可設置為:SEEK_SET、SEEK_CUR、SEEK_END。
2018-2-9
五、深入文件I/O
1. 原子操作
文件I/O操作存在兩種競爭狀態,可通過正確使用open()的標志位,來保證操作的原子性。
2. 文件控制操作:fcntl()
函數原型:int fcntl(int fd, int cmd, ...);
功能:對(一個打開的)文件描述符執行一系列控制操作。包括修改打開文件的狀態標志、復制文件描述符。
參數:cmd參數所支持的操作范圍很廣;第三個參數以省略號來表示,意味着可以將其設置為不同的類型,或者加以省略。
3. fcntl()的用途——獲取或修改一個已打開的文件的訪問模式和狀態標志
方法:要獲取這些設置,應將cmd參數設置為F_GETFL;而要設置文件的狀態標志,則cmd應為F_SETFL。
4. 文件描述符和打開文件的關系
文件描述符標志為進程和文件描述符所私有,對這一標志的修改不會影響同一進程或不同進程中的其他文件描述符。
5. 復制文件描述符:dup()、dup2()
用途:通過復制文件描述符,達到實現重定向操作的目的。
6. 其他調用:pread()、pwrite()、readv()、writev()、truncate()、ftruncate()
7. 非阻塞I/O:在打開文件時指定O_NONBLOCK標志
說明:若open調用未能立即打開文件,則返回錯誤,而非陷入阻塞。
8. /dev/fd目錄
對每個進程,內核都提供有一個特殊的虛擬目錄/dev/fd。該目錄中包含“/dev/fd/n”形式的文件名,n是與進程中的打開文件描述符相對應的編號。例如,/dev/fd/0對應於進程的標准輸入。
打開/dev/fd目錄中的一個文件等同於復制相應的文件描述符。
用途:用於shell中,將其作為文件名參數傳遞給shell命令。
六、進程之結構篇——虛擬內存的布局及內容
1. 進程的定義:由內核定義的抽象的實體,並為該實體分配用以執行程序的各項系統資源。
關鍵點:可以用一個程序來創建許多進程,也即許多進程運行的可以是同一程序。
2. 進程號
數據類型:pid_t
補充:低數值的進程號為系統進程和守護進程所長期占用。
父進程:每個進程都由其父進程創建,父進程終止后,子進程就會變為“孤兒”,隨即由init進程收養。
3. 進程內存布局:進程分配到的內存的布局
每個進程分配到的內存的組成部分:文本段、數據段、棧、堆
文本段:包含了進程運行的程序機器語言指令,其是只讀、可共享的。因為多個進程可同時運行同一程序。
初始化數據段:包含顯式初始化的全局變量和靜態變量。
未初始化數據段:包含了未進行顯式初始化的全局變量和靜態變量。
棧:一個動態增長和收縮的段,由棧幀組成。系統會為每個當前調用的函數分配一個棧幀,棧幀中存儲了函數的局部變量、實參和返回值。
堆:可在運行時(為變量)動態進行內存分配的一塊區域。
2018-2-10
4. 虛擬內存管理:進程內存布局存在於虛擬內存中
虛擬內存:利用程序的訪問局部性這一特性,使得程序僅有部分地址空間存在於RAM中。
實現:將每個程序使用的內存切割成小型的、固定大小的“頁”單元,相應地將RAM划分成一系列與虛存頁尺寸相同的頁幀。任一時刻,每個程序僅有部分頁駐留在物理內存頁幀中,程序未使用的頁拷貝保存在交換區(磁盤空間中的保留區域)內,作為RAM的補充——僅在需要時才會載入物理內存。
頁表:內核為每個進程維護一張頁表,其描述了每頁在進程的虛擬地址空間中的位置。
進程的虛擬地址空間:進程駐留在RAM中的部分 + 進程保存在交換區的部分
優點:因為需要駐留在內存中的僅是程序的一部分,所以程序的加載和運行都很快,而且一個進程所占用的內存(即虛擬內存大小)能夠超過RAM容量。同時,每個進程使用的RAM減少了,RAM中同時可以容納的進程數量就增多了。
5. 環境:每個進程都有的稱之為環境列表的字符串數組,每個字符串都以名稱=值形式定義
環境:“名稱-值”的成對集合,可存儲任何信息。而將列表中的名稱稱為環境變量。
從程序中訪問環境:通過全局變量environ
修改環境的系統調用:getenv()、putenv()、setenv()、unsetenv()、clearenv()
6. 非局部跳轉:setjmp()、longjmp()
能夠從甲函數跳轉到乙函數,但這將是程序難於閱讀和維護,應盡量避免使用。
七、內存分配——在堆或堆棧上分配內存
1. 在堆上分配內存:進程可通過增加堆的大小來分配內存
堆:一段長度可變的連續虛擬內存,始於進程的未初始化數據段末尾,隨着內存的分配和釋放而增減。
program break:堆的當前內存邊界,最初其正好位於未初始化數據段末尾之后。
調整program break的系統調用:brk()和sbrk()
2. 在堆上進行內存分配的系統調用:malloc()、free()
函數原型:void *malloc(size_t size);
參數:size為分配的字節數;返回類型void*表示可以將返回值賦給任意類型的C指針。
函數原型:void free(void *ptr);
功能:釋放ptr所指向的內存塊,將這塊內存填加到空閑內存列表中。
補充:進程終止時,其占用的所有內存都會返還給操作系統,包括在堆內存中由malloc函數包內一系列函數所分配的內存。
3. 在堆上分配內存的其他方法:calloc()、realloc()
函數原型:void *calloc(size_t numitems, size_t size);
功能:給一組相同對象分配內存,並將已分配的內存初始化為0(而malloc()則不會進行初始化)。
參數:參數numitems指定分配對象的數量;size指定每個對象的大小。
函數原型:void realloc(void *ptr, size_t size);
功能:調整一塊內存的大小,此塊內存應是之前由malloc包中函數所分配的。
4. 在堆棧上分配內存:alloca()
通過增加棧幀的大小從堆棧上分配,該類內存會在調用alloca()的函數返回時自動釋放。
八、用戶和組
1. 用於定義用戶和組的系統文件
密碼文件:/etc/passwd
shadow密碼文件:/etc/shadow
組文件:/etc/group
2. 用於從這些系統文件中獲取信息的庫函數
這些庫函數的功能包括:從密碼文件、shadow文件和組文件中獲取單條記錄,掃描上述各個文件中的所有記錄
庫函數:getpwnam()、getpuid()、getgrnam()、getgrgid()等
3. crypt()函數:用於加密和認證登錄密碼
描述:UNIX系統采用單向加密算法對密碼進行加密,故驗證候選密碼的唯一方法是使用同一算法對其進行加密,並將加密結果與存儲於/etc/shadow中的密碼進行匹配。
加密算法:封裝於crypt()函數之中
函數原型:char *crypt(const char *key, const char *salt);
好處:對需要認證用戶的程序來說極為有用。
