歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux基礎 >> 關於Linux

《Linux系統編程》第一章筆記

簡介和主要概念

1.1 系統編程

系統編程和應用層編程的區別與聯系

系統編程最突出的特點是要求系統程序員必須對他工作的系統的硬件和操作系統有深入和全面的了解。
盡管目前越來越多的語言、框架在遠離系統級編程,向高層業務靠攏,但系統級編程不會消亡,普通程序員也能從系統級編程取得收益。而Linux下的編程大多數都屬於系統級別的編程。

系統編程的分支

通常包括內核開發(或者至少有設備驅動的內容);用戶空間系統級別編程;網絡編程。
系統編程三大基石:系統調用C庫編譯器

系統調用

系統調用是為了從操作系統獲得服務或資源而從用戶空間向內核發起的調用。

基於系統安全和穩定的考慮,用戶空間應用程序不能直接執行內核代碼或操作內核數據,內核必須提供一個機制——用戶空間程序能夠發送信號通知內核它希望請求一個系統調用1。
更多Linux架構的資料。

C庫

在現代Linux系統中,C庫是由GNU libc提供,簡稱glibc。除了標准的C庫,glibc還提供了系統調用封裝,線程支持和基本應用工具。

庫和系統調用的關系:庫為應用程序提供系統調用的封裝,並將系統調用映射成進入內核所需要的源語【《Linux網絡編程》2.1】。系統調用功能非常基礎且性能代價昂貴(用戶空間到內核空間切換的開銷),直接使用系統調用的話會造成系統調用次數增加(無I/O緩沖的read(),每次讀取文件時都是一次系統調用,對應的是有用戶空間緩沖的fread()庫函數)、開發工作量大等弊端,此外系統調用是操作系統相關的,因此一般沒有跨操作系統的可移植性。因此庫提供了一些符合標准的庫函數,其實質是系統調用的封裝。更多庫調用和系統調用的資料。
可以執行libc的動態庫來查看glibc的版本,例如/lib64/libc.so.6,glibc的動態庫不一定總叫同一個名字,可以對可執行程序使用ldd命令,查看glibc的位置。
程序在編譯期可以使用__GLIBC____GLIBC_MINOR__兩個宏來確認glibc的版本,2.12版本中第一個宏定義為2,第二個宏定義為12。在運行期可以使用gnu_get_libc_version()或者confstr(_CS_GNU_LIBC_VERSION)來確定版本。

編譯器

在Linux中,GCC是GNU編譯器工具集2。

gcc和g++:gcc和g++都是the GNU Compiler Collection工具套件的一部分。gcc和g++的區別在於:

對於不同擴展名的文件處理方式不同,g++會將.c和.cpp的源文件當做C++源碼編譯,gcc會將.c當做C源碼編譯,對於C++源碼的編譯行為兩者一致 gcc無法默認和C++的庫鏈接,需要在編譯選項中指定

以上的不同可以通過編譯選項來去除,對於編譯C++源碼,gcc -xc++ -lstdc++ -shared-libgcc等價於g++。詳見gcc和g++有何不同。

gmake和make:

gmake是GNU Make的縮寫。 Linux系統環境下的make就是GNU Make,之所以有gmake,是因為在別的平台上,make一般被占用,GNU make只好叫gmake了。 比如在安裝二進制文件進行編譯時要使用make命令,但如果在Solaris或其他非GNU系統中運行,必須使用GNU make,而不是使用系統自帶的make版本,這時要用gmake代替make進行編譯。
GNU gmake調用的C編譯器名稱為 gcc,C++編譯器的名稱為 g++
(非Linux平台下)make調用的C編譯器名稱為cc,C++編譯器名稱為CC

除了默認的編譯工具不一致外,兩者的編譯選項也有差距。因此在非Linux平台下使用gcc/g++時,需要指定gmake。

特性測試宏
有時在編譯時只想要編譯器提供符合某個標准版本的函數,確保所寫程序符合某個標准版本,可以在包含頭文件之前定義特性測試宏。編譯器根據定義的宏來暴露符合條件的函數。features.hman 7 feature_test_macros提供了詳細的信息。

1.2 API和ABI

在系統級別,影響可移植性的因素主要包括兩個: API 和 ABI ,兩個都定義和描述了不同模塊間的接口。

API(應用程序接口)定義了模塊之間在源碼層交互的接口,以函數的方式進行抽象,例如某個API函數可能是一系列函數的組合。遵循API的源碼能夠確保在提供相同API的系統上成功編譯。API通過標准來定義,通過庫來實現。

ABI(應用程序二進制接口)定義了模塊之間的二進制接口,主要關注的問題有調用約定、字節序、寄存器使用、庫行為等。其確保二進制代碼兼容,即一段目標代碼能夠在任何相同ABI的系統上運行而不需要重新編譯。在Linux系統中,不同的架構有不同的ABI,一般以架構名稱命名,例如alphax86-64

1.3 標准

Linux盡力與POXIS(Portable Operating System Interface,可移植操作系統接口)和SUS(Single UNIX Specification,單一UNIX規范)兩個標准保持兼容。其中SUS標准包含了POSIX標准。 Linux不同的版本之間遵循LSB(Linux Standards Base,Linux基本規范)標准,該標准擴展了POSIX和SUS,包括嘗試提供二進制標准,允許目標代碼無需修改即可在符合標准的系統上運行。 美國標准化學院(ANSI)制定的標准化C語言稱為ANSI C,不同的編譯器在ANSI C的基礎上自行擴展新特性,gcc編譯器提供的新特性被稱為GNU C。

1.4 Linux編程概念

文件和文件系統

一切皆是文件,Linux的很多交互工作是通過讀寫文件來完成。文件只有被打開才能被訪問,一個打開的文件通過唯一的文件描述符進行引用,Linux中用一個整數表示,簡寫為fd。

對於每個進程來說,文件描述符0,1,2已經被占用,分別代表標准輸入、標准輸出和標准錯誤,使用STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO來替代具體的數值。因此每個進程能打開的fd從3開始。
以下代碼測試文件描述符的分配策略:

#include 
#include 
#include 
using namespace std;
int main()
{
    int fd1 = open("test.txt", O_RDWR|O_CREAT );
    int fd2 =  open("test.txt", O_RDWR|O_CREAT );
    cout << "fd1-" << fd1 << " fd2-" << fd2 << endl;//輸出3 4,一個進程多次打開同一個文件,fd不同
    close(fd1);//釋放fd1
    int fd3 = open("test.txt", O_RDWR|O_CREAT );
    cout << "fd1-" << fd1 << " fd2-" << fd2 << " fd3-" << fd3 <

整個系統打開的文件數量有上限,通過sysctl -a | grep fs.file-max查看,單個進程打開文件的上限通過ulimit -n來查看。
可以通過讀取/proc/PID/fdinfo文件獲取PID對應的進程的文件描述符使用情況。

普通文件

即通常意義上的文件,以線性字節數組方式組織數據,通常也被稱為字節流。在系統級別,Linux沒有要求文件有特定結構。
文件的讀寫操作基於文件偏移量,內核記錄每個描述符的文件偏移量,第一次打開時偏移量為0,隨著讀寫操作內核將改變該值。偏移量可也手動設置超過文件大小,每次對超過文件大小的位置寫入,會導致中間部分充填為0,充填為0的部分不占據硬盤空間,只在真正寫入數據時才占據空間,這種文件叫做稀疏文件。偏移量最大值只受變量類型大小所限。
同一個文件可以被多次打開,每次打開後系統會分配一個描述符。內核沒有對並發讀寫做限制,用戶空間程序需要自行同步。
Linux系統通過inode節點訪問文件,inode節點存儲在硬盤上,保存有修改時間戳、所有者、長度、權限等信息,inode節點對應唯一的inode編號,Linux系統通過inode編號來訪問文件,而不是通過文件名。

目錄

目錄將易讀的名字與inode映射,名字與inode的配對叫做鏈接。一個目錄可以被視為普通的文件,但是其保存名字和inode的映射。
目錄本身也有inode節點,目錄內的鏈接能夠指向其他目錄,因此實現了目錄層次。

鏈接

多個文件名可以解析到同一個inode節點上,這樣的鏈接叫做硬鏈接。inode節點記錄的有鏈接數,表示有多少個文件名與該inode節點關聯,硬鏈接無法創建在目錄上。當刪除一個硬鏈接(即刪除文件)時,鏈接數會減一,當鏈接數為0時,系統會回收對應inode編號。Linux通過ln link_name target_file命令創建硬鏈接,其中target_file為已經存在的文件路徑。由於inode在跨文件系統時沒有意義,因此硬鏈接只能作用於一個文件系統。 不同的inode節點可以標記同一份文件數據,這樣的鏈接叫做軟鏈接(符號鏈接),軟鏈接可以指向任何位置,包括不同的文件系統甚至不存在的文件。軟鏈接其實是一個新的inode節點,可以有自己的文件屬性、權限控制,通過ln -s link_name target_file命令創建。

特殊文件

Linux的特殊文件是指以文件方式表示的內核對象,四種特殊文件:塊設備文件、字符設備文件、命名管道、套接字。

可以通過ls –lll命令查看是否是特殊文件,對應關系如下:

權限控制 文件類型 -rw- 普通文件 brw- 塊設備 crw- 字符設備 srwx socket設備 prwx 管道文件 lrwx 軟鏈接 drwx 目錄

字符設備
訪問字符型設備與訪問線性隊列類似,用戶空間程序按照字節寫入順序進行讀取,當沒有更多字符需要讀取時,設備返回EOF。鍵盤就是典型的字符設備,其他還有打印機和終端。/dev/null是特殊的字符設備,所有對該設備的輸出都會忽略。

塊設備
塊設備以字節數組的方式進行訪問,用戶空間程序可以以任意順序訪問。塊設備通常是存儲設備,如硬盤、CD-ROM驅動器等。

以前新增或刪除字符、塊設備都需要手動配置,現在可以通過命令service kudzu start使Linux自動添加或刪除設備。

命名管道
通常也叫FIFOs,先進先出的簡稱,是一種進程間通信(IPC)機制,通過一個特殊文件進行訪問,該文件保存在文件系統中,因此能做到無親屬關系的線程相互通信。對應的普通管道在內存中創建且不在任何文件系統中存在,因此只能用於父子進程或兄弟進程間通信。

套接字
可以跨主機、跨進程進行網絡通信。
套接字是一個很大的話題,留待專門講解。

文件系統和名字空間

Linux提供了一個全局統一的名字空間,所有文件都在同一個名字空間下,區別於windows的各個盤符
文件系統是以合法層次結構組織的文件和目錄的集合

文件系統能從全局的名字空間中添加(掛載)和移除(卸載),每個文件系統都掛載在名字空間中特定的位置(掛載點)。第一個被掛載的文件系統位於名字空間的根部/,稱為根文件系統。

塊設備最小訪問地址單位是扇區,扇區是設備的物理單位,一般是2的n次方,通常為512字節。文件系統中最小邏輯地址單位是塊,塊是文件系統中的抽象,通常是扇區大小乘以2的n次方,必須小於頁(最小可訪問的內存管理單元)的大小。

進程和線程

進程

進程是執行中的目標代碼,除了目標代碼,進程還包括數據、資源、狀態以及虛擬化的計算機(指進程之間相互隔絕,每個進程都認為自己獨占計算機)

Linux下最常用的可執行代碼的格式為ELF,包括元數據、多個代碼和數據段。段是加載到線性內存塊的線性目標代碼塊,主要包括:
* 代碼段——只讀數據(如常量)和可執行代碼
* 數據段——已初始化的數據(包括全局變量、靜態變量)
* bss段——未初始化的全局數據,加載時清0
* 堆——程序運行時動態申請內存的區域
* 棧——隨函數調用、返回而增減的內存,用於為局部變量和函數調用信息分配空間

線程

一個進程包含一個或多個線程,線程共享進程的資源和地址空間,但有每個線程獨享的棧、處理器狀態和目標代碼當前位置。

由於線程之間有共享的資源,因此多線程的一個重要問題就是線程同步:通過約束線程的行為來保護共享資源;通過某種機制來實現線程間通知,避免類似死循環等待資源的操作來占用CPU等等。
查看線程和進程資料和線程資料。

進程體系

每個進程都有一個正整數的唯一標識(pid),第一個進程的pid是1。進程有層次結構,即進程樹,其第一個進程是init進程。
線程也通過ID號來識別,但是線程的ID號只在其屬於的進程內有效,在別的進程中無意義,進程可以通過線程ID來管理線程。

init進程是所有進程的根,新進程通過fork()系統調用創建。fork()復制了進程,原進程是父進程,新進程是子進程。子進程啟動後一般采用exec()系列函數加載另一個程序,此時從父進程繼承的文本段、數據段、棧和堆的數據全部被替換。進程結束的方式有兩種,一種是調用_exit()請求退出(main()函數return後自動會調用類似函數),另一種是向進程傳遞信號使其退出。若父進程在子進程之前終止,子進程的父進程改為init。進程終止時,內核會保留進程的部分內容,並允許父進程查詢終止狀態,此時稱為終止進程等待。父進程確認子進程終止後,子進程完全刪除;但一個子進程終止後父進程未獲取它的狀態,此時稱為僵屍進程,危害在於內核會為僵屍進程保留pid和進程信息,占用系統資源。init進程會等待所有子進程,因此父進程先於子進程終止的話,不會產生僵屍進程。
無論是調用_exit()相關函數還是由於收到信號,進程結束時會指定進程終止狀態,大多數shell將前一個進程的終止狀態寫在變量中。

進程組和會話

進程組
大多數shell提供了名為任務控制的交互特性,用戶可以同時執行並操縱多條命令和管道,支持任務控制的shell會將管道內的所有進程放在一個進程組中,所有進程的進程組號一樣,都為組長進程的進程id,組長進程是組內第一個啟動進程。使用fork()系統調用時,父子進程也構成了一個進程組。內核可以對進程組內所有成員執行動作,例如信號的傳遞等。

#include 
 pid_t getpgrp(void);//獲取本進程的進程組

 #include 
 #include 
 int setpgid(pid_t pid, pid_t pgid);//設置指定進程的進程組,只能為本進程或子進程設置

會話
Linux通過進程組對進程做統一操作,通過會話對進程組做統一操作。會話中的所有進程都有相同的會話標識符,會話首進程是指創建會話的進程,其進程id是會話id,最常見的會話首進程是shell。
控制終端是會話首進程打開時使用的終端,最多只能成為一個會話的控制終端。打開控制終端會使會話首進程成為控制終端的控制進程,若與終端的連接斷開,控制進程會受到SIGHUP信號。
任意時間點,會話中總有一個前台進程組來從終端中讀取輸入,向終端發送輸出。用戶在終端按下Ctrl+cCtrl+z時,前台進程組會受到終止或掛起信號。一個會話同時可以有多個後台進程組,後台進程由&結尾的命令創建。

用戶和組

Linux通過用戶和組進行認證,每個進程與一個用戶id關聯,標識啟動線程的用戶。其被稱作真實uid。超級用戶root的uid是0。

每個用戶都有唯一的正整數標識,用戶名和密碼保存在/etc/passwd中。查找有多少賬戶使用命令cat /etc/passwd | wc -l。passwd文件格式類似於root:x:0:0:root:/root:/bin/bash,其個字段含義為——注冊名:口令:用戶標識號:組標識號:注釋:用戶主目錄:命令解釋程序
口令使用x代替,真正保存在/etc/shadow中,如果該字段是*,代表賬戶被禁用。口令可以通過passwd命令管理。
用戶ID 0是管理員,1-499系統保留,一般賬號從500開始。
組標識號是用戶的缺省工作組,使用usermod -g設置。
用戶主目錄標識用戶登錄系統後自動進入哪個目錄。root用戶的主目錄在/root下,其他用戶在/home/路徑下,與用戶名相同。使用usermod -d設置。
進程和用戶id
每個進程都一系列有與之對應的用戶id和組id:
真實用戶/組id——進程所屬的用戶id和組id。
有效用戶/組id——進程訪問受保護的資源時用其確認權限,初始與真實用戶/組id一致。
補充組id——標識進程所屬的額外組。

權限

每個文件都有一個所有者、所屬組合權限位集。權限位集描述了所有者、所屬組合其他人對文件的讀、寫、執行的權限,文件所有者和權限信息保存在inode中。

權限位集采用9位八進制保存,每個八進制數分別代表擁有者權限、擁有者所屬組權限、其他用戶權限。各個位表示是否擁有讀、寫、執行三個權限。700即111,000,000,代表僅所有者有讀寫執行權限。744即111,100,100,代表除所有者有讀寫執行權限外,別的用戶還有讀權限。讀寫權限對root用戶來說無意義,root用戶對所有文件都有讀寫權限,但當文件沒有可執行權限時root用戶也不能直接執行(root用戶可以使用chmod命令添加可執行權限)。
目錄的讀權限指能否列出目錄內容,寫權限指能否修改文件,執行權限指能否對目錄下的文件做訪問。

信號

信號是一種單向一部通信機制,信號可能是內核發送到進程,也可能是進程發送到另一個進程,或者是進程發送給自己。信號一般用於通知進程發送某些事件,例如段錯誤或用戶輸入Ctrl+C

內核信號由一個數字常量和文本名表示,除了SIGKILL(進程中斷)和SIGSTOP(進程停止)外,進程能根據收到的信號做默認處理、忽略或自定義處理。在收到有處理函數的信號時,控制權會交給信號處理函數,在信號處理函數返回後中斷的指令繼續執行。
信號的來源主要有硬件來源和軟件來源3。

內核態和用戶態

現代架構的CPU一般至少在兩個狀態下運行:用戶態和內核態,使用硬件指令在兩種狀態下切換。同樣虛擬內存也被標記為用戶態和內核態。

在用戶態下運行時,CPU只能訪問被標記為用戶態的內存區域,超過用戶態區域的訪問會引發硬件異常。而在內核態下的進程可以訪問內核態和用戶態的內存區域。
此外一些指令也只能在內核態下執行,例如關閉系統、訪問內存硬件、設備I/O操作等。將這些指令放在內核態執行,保證了用戶不會直接訪問內核指令和數據結構,也無法直接執行不利於系統的操作。[《Linux/Unix系統編程手冊》 第二章]

進程間通訊

進程間通訊機制包括管道、命名管道、信號、信號量、文件、消息隊列、共享內存和套接字。

頭文件

內核和glibc為系統編程提供頭文件,包括標准C頭文件和Linux平台相關的頭文件。

錯誤處理

系統編程中錯誤通常通過函數返回值表示,通過errno變量來描述。返回值一般表達了發送哪種錯誤,而errno變量保存了錯誤的原因。

errno變量在中定義為int類型。由於很多調用都可以設置errno值,因此該值只有在函數調用出錯後的短時間內有意義,否則不一定代表預期的函數調用出錯原因;同時GCC保證了errno的線程安全,不同的線程擁有各自本地的errno值。
errno.herrno-base.h兩個頭文件定義了各種錯誤原因和對應的錯誤碼,glibc提供了一些函數將errno值轉為對應的文本信息:

#include
//向標准錯誤輸出stderr打印出"str:當前errno描述"的信息。
void perror ( const char * str );
#include
//獲取錯誤碼對應的描述,不可修改,非線程安全
char* strerror(int errnum);
//獲取錯誤碼對應的描述(最大為n個字符),保存到buf中,線程安全
int strerror_r(int errnum, char *buf, size_t n);

對於有些函數,其返回值在一定范圍內都是合法的,此時如果想判斷函數調用是否成功,可以在調用之前將errno清0,調用後檢測errno取值來判斷函數調用是否成功,例如strtoul類似的轉換函數。


  • 應用程序通過軟中斷陷入內核,使用寄存器通知內核執行哪個系統調用以及帶什麼參數。通過數字系統調用標注,從0開始,例如open()的指令為5。此外參數傳遞也使用寄存器進行。 1
  • gcc原來是GNU版本的cc,C編譯器。後來更多的語言支持被加入,gcc成為GNU編譯器家族的代名詞(the GNU Compiler Collection)。 2
  • 最常發出信號的系統函數有kill, raise, alarm、setitimer、sigqueue以及非法運算。 3
Copyright © Linux教程網 All Rights Reserved