別出心裁的Linux系統調用學習法
操作系統與系統調用
操作系統(Operating System,簡稱OS)是計算機中最重要的系統軟件,是這樣的一組系統程序的集成:這些系統程序在用戶對計算機的使用中,即在用戶程序中和用戶操作中,負責完成所有與硬件因素相關的(硬件相關)和任何用戶共需的(應用無關)基本使用工作,並解決這些基本使用工作中的效率和安全問題,為使用戶(操作和上層程序)能方便、高效、安全地使用計算機系統,而從最底層統一提供所有通用的幫助和管理。
硬件相關:
- 涉及物理地址、設備接口寄存器、設備接口緩沖區
- 代碼量大,需硬件知識
- 需隨硬件的變化而變化
應用無關:
- 所有應用、用戶共需
- 工作過程雷同
- 與應用無直接關系
我把操作系統完成的「硬件相關、應用無關」的工作比喻成兩個角色:
- 管家婆
- 服務生
操作系統通過三抽象概念完成了「管家婆」的功能:
- 通過「文件」對I/O設備進行了抽象
- 通過「虛存」對主存和I/O設備進行了抽象
- 通過「進程」對CPU、主存和I/O設備進行了抽象
如果說概念是任何學習中的重中之重,那么《操作系統》課上最重要的概念就是「文件、虛存和進程」,這幾個概念沒學好,《操作系統》這門課就完了。當然通信、並發、並行、異步、調度、多道等概念在《操作系統》課程學習中也很重要。
一般來說,操作系統通過三個服務完成了「服務生」的概念:
- GUI:為小白用戶提供服務,你只會用鼠標就可以使用操作系統
- Shell: 為高級用戶提供服務,你要記憶系統命令,更多通過鍵盤使用操作系統
- 系統調用:為專業用戶程序員提供服務,你可以創建自己的工具讓大家更好的使用操作系統
系統調用(System Call)是操作系統為在用戶態運行的進程與硬件設備進行交互提供的一組接口。當用戶進程需要發生系統調用時,CPU 通過軟中斷切換到內核態開始執行內核系統調用函數。我們可以有三種方法使用系統調用:
- 通過 int 指令陷入:通過軟中斷指令int 0x80 來陷入內核態
- 使用 syscall 直接調用, glibc沒有封裝某個系統調用時可以
- 通過 glibc 提供的API調用,最方便的方法
下文我們主要使用第三種方法來使用Linux系統調用,並且不區分的使用「系統調用」和「API」。比如打開文件系統調用sys_open
對應的是 API是open
,我們會說open
是系統調用,也就是說manpages中的第二節的API我們就叫「系統調用」。所以系統調用你當成一般函數來學習就可以了,通過參數和返回值關注一下功能。
Linux下學習編程的3*3
Linux 系統調用學習方法
程序員技術練級攻略中提出程序員進階過程中通過實踐來學習系統編程。
如何學習命令我寫了一篇「別出心裁的Linux命令學習法」,核心是問題驅動,像使用Google/Baidu一樣使用man -k key1| grep key2| grep key3| ...
進行搜索,通過實踐不斷的學習新的命令。這個學習方法對你的英文要求有點高,建議用扇貝背單詞、DKY背單詞小組學習來提高自己的英文實用水平。
我們把這種方法推廣到Linux系統調用的學習,通過Linux系統來學習Linux編程。我們要充分利用好man pages。如下圖,man pages分為九節,每節的內容大家要熟悉:
我們想要了解系統調用的相關信息,我們搜一下看看:
intro(2)
和syscalls(2)
好像是我們要找的東東。我們用man
看看它們能提什么信息。我們先在shell中輸入man 2 intro
:
intro(2)
直接讓我們找syscalls(2)
,我們在shell中輸入man 2 syscalls
:
syscalls(2)
介紹了什么是系統調用,並提供了Linux系統調用的列表。從上圖我們看出Linux Kernel 3.1 大約有400個系統調用。當然,我們不需要掌握每一個系統調用,根據20/80定律,我們掌握大約80個核心系統調用就可以解決80%的問題了,文末會給出一些核心系統調用。
我們通過以下3個步驟來學習。
- 分析程序
首先分析現有的Linux命令如who/cp/ls/pwd...,這些命令都在/bin,/usr/bin,/usr/local/bin目錄中,通過「別出心裁的Linux命令學習法」中的方法了解它的功能及實現原理。能寫出實現該命令的偽代碼。 - 學習系統調用
看程序都用到哪些系統調用,以及每個系統調用的功能(參數,返回值,錯誤碼...)和使用方法(相關頭文件,庫文件,相關系統調用...)。 - 編程實現
利用學到的原理和系統調用,自己編程實現原來程序所實現的功能。
以上3步可以通過下面3個問題來實現:
- 它能做什么?
- 它是如何實現的?
- 能不能自己編寫一個?
系統調用學習示例1-who
我們通過解決三個問題來示范如何學習系統調用:
who
命令能做什么who
命令是如何實現的?- 能不能自己編寫一個
who
命令?
問題1. who
命令能做什么
一個Linux命令的功能我們可以通過whatis
或man -f
來查看,當然最好的方法是自己通過使用來體驗一下:
通過上圖我們看到who
命令用來查看誰登錄了系統(show who is logged on ),每一行代表一個巳經登錄的用戶,第1列是用戶名,第2列是終端名,第3列是登錄時間。
通過whatis who
或man -f who
直接運行命令,可以了解who的大致功能,要進一步了解who
的用法,需要借助聯機幫助manpages。我們輸入man 1 who
:
所Linux命令的manpages都有相同的基本格式,從第1行可以知道這是關於哪個命令的幫助,還可以知道這個幫助是位於哪一節的。在這個例子中,從第1行的內容who(l),可以知道這是who
命令的幫助,它的小節編號是1。Linux的manpages分為很多節,如第1小節中是關於Linux命令的幫助,第2小節中是關於系統調用的幫助,第5小節中是關於配置文件的幫助。
上面who
的在幫助文檔中:
-
名字(NAME)部分包含命令的名字以及對這個命令的簡短說明。我們可以通過
whatis who
或man -f who
來獲取這部分內容。 -
概要(SYNOPSIS)部分給出了命令的用法說明,包括命令格式、參數(arguments)和選項(Option)列表。選項指的是一個短線后面緊跟着一個或多個英文字母,如-a、-Bc,命令的選項影響該命令所進行的操作。在幫助文檔中,方括號([-a])表示該選項不是一個必須的部分。幫助中指出
who
的寫法可以是who
或者who -a
,或者who -
加上AbdhHlmMpqrstTu
這些字母的任意組合,在命令的末尾還可以有一個文件參數。
從幫助中可以知道who命令還有其他幾種形式:- whoami
- who am i
- who mom likes
從聯機幫助中還可以獲得上述形式的進一步幫助。
-
描述(DESCRIPTION)部分是關於命令功能的詳細闡述,根據命令和平台的不同,描述的內容也不同,有的簡潔、精確,有的包含了大量的例子。不管怎么樣,它描述了命令的所有功能,而且是這個命令的權威性解釋。
-
選項(OPTIONS)部分給出了命令行中每一個選項的說明。
-
參閱(SEE ALSO)部分包含與這個命令相關的其他主題。
到這,即使以前沒有用過who
命令,也知道它的功能了。
問題2. who
命令是如何實現的?
前面看到who命令可以顯示出當前系統中已經登錄的用戶信息,聯機幫助manpages中描述了who
的功能和用法,現在的問題是:who
是如何來實現這些功能的?
你可能會認為,像who這樣的系統程序一定會用到一些特殊的系統調用,需要高級管理員的權限,要編寫這樣的程序得要花很多錢來購買系統開發工具,包括光盤、參考書等。實際上,所需的資料都在系統中,你要知道的僅僅是如何找到這些資料。
我們首先關注manpages,上面who
的幫肋文檔中第43行提供了一個重要信息:
If FILE is no specified, use /var/run/utmp,/var/log/wtmp as FILE is common.
/var/run/utmp
是個什么東東?
我們通過ls /var/run/utmp
可以查年有沒有這個東西。我們看到有這個文件.
我們通過file /var/run/utmp
可以查年這是個什么東西(文件類型)。file
告訴我們是個數據(data)文件。這里用了一個命令行技巧:!$
代表了上一條命信令的參數。我們剛運行過ls /var/run/utmp
,!$
就代表/var/run/utmp
,file !$
就等價於file /var/run/utmp
.
我們通過cat /var/run/utmp
查看utmp文件的內容,發現是亂碼,但也能看出有who
中的信息。
我們確定了/var/run/utmp
是個二進制文件,可以通過od -tx1 /var/run/utmp
來一個字節一個字節的查看其內容,還挺有規律的,我們可以想象utmp是一條記錄,一條記錄組成的文件。
命令和結果如下圖所示:
我們對utmp大致有個了解和判斷了,但還不精確。
我們在「別出心裁的Linux命令學習法」,學到可以像使用Google/Baidu一樣使用man -k key1| grep key2| grep key3| ...
在manpages中搜索你想要的信息。我們試試man -k utmp
utmp(5)是個有意思的結構,證實了我們關於「utmp是一條記錄,一條記錄組成的文件」的猜想。
我們用man 5 utmp
看一下:
一切盡在不言中,我們要實現who
命令的東西都在這了。
who
的聯機幫助說who
要讀utmp這個文件,進一步,從以上的說明可以知道utmp這個文件里面保存的是結構體數組,數組元素是utmp類型的結構,utmp結構保存了登錄記錄。它包含9個成員變量,ut_user 數組保存登錄名,ut_line 數組保存設備名,也就是用戶的終端類型,ut_time 保存登錄時間。 utmp這個結構所包含的其他成員沒有被who命令所用到。
我們在utmp.h
中應該能找到utmp類型的定義,Linux中的頭文件都在/usr/include
目錄里。
實踐中我們找不到。我們可以通過grep -nr "struct utmp" /usr/include
查找struct utmp在哪個頭文件中定義。結果如下圖所示:
通過聯機幫助manpages來學習Linux就像在網絡上尋找信息一樣,經常能從某一個幫助主題中找到相關信息,鏈接到其他有用或有趣的主題。這個過程正是「做中學(Leaning by Doing)」的要義。
問題3. 能不能自己編寫一個who
命令?
我們前面說過寫程序要寫三種代碼:
- 偽代碼:確定你解決了問題沒
- 產品代碼: 用計算機語言解決問題,偽代碼是產品代碼最好的注釋
- 測試代碼:是不是正確的解決了問題
對於問題3,我們已經解決了,文件中的結構數組存放登錄用戶的信息,所以直接的想法就是把記錄一個一個地讀出並顯示出來。我們可以寫出實現who
命令的偽代碼,中英文都行,可能的話最好用英文,我們給出中文的例子:
打開utmp 文件
讀取utmp中的每一條記錄
顯示記錄中的相關信息
關閉utmp文件
簡單吧?我們下面用C語言寫出產品代碼。
為了能夠順利實現who
命令,需要經常從聯機幫助manpages中獲取信息。我們不用寫 測試代碼,只要把程序的輸出與系統who
命令的輸出做比較就行了。通過分析可以確認,在編寫 who
程序時只有兩件事情是要做的:
- 從文件中讀取數據結構
- 將結構中的信息以合適的形式顯示出來
如果你的C語言基礎好,利用C庫中的fopen,fread,fclose
就可以實現。我們要學習系統調用,有什么系統調用可用呢?我們想讀文件(read a file),不知道用什么系統調用沒關系,我們用man -k read | grep file | grep 2
搜一下:
read(2)好象是我們需要的,其他的看起來都不像,所以進一步用man 2 read
看read(2)的幫助:
NAME節解釋了這個這個系統調用的功能,
這個系統調用可以將文件中一定數目的字節(參數count)讀入一個緩沖區(參數buf),因為每次都要讀入一個數據結構,所以要用sizeof(struct utmp)來指定每次讀人的宇節數。read函數需要一個文件描述符作為輸人參數,如何得到文件描述符呢?我們關注一下上面read(2)的幫助的「SEE ALSO」部分。你如果還記得C語言中的讀文件的fopen,fread,fclose
的組合模式,你就會想到open(2)正是我們需要的。我們用man 2 open
看一下open(2)的幫助:
查看open(2)的聯機幫助,從open(2)中又可以找到對close(2)的引用,通過閱讀聯機幫助,可以知道以上3個系統調用(open(2),read(2),close(2))都是進行文件操作所必需的。
大家可以看到,上面的幫助文檔都是英文的。對大部分同學來說,一看是英文的,再簡單都沒信心看下去。實在不行,你可以通過sudo apt-get install manpages-zh
安裝中文版幫助文檔:
中文幫助文檔與英文不是一一對應的,少了一些內容,並且翻譯的太差了。在我看來,還沒英文版的看着舒服。這與買一些爛的翻譯版圖書一樣,還不如看原版。
我們現在實現一下who
命令,源程序文件名who1.c:
#include <stdio.h>
#include <stdlib.h>
#include <utmp.h>
#include <fcntl.h>
#include <unistd.h>
int show_info( struct utmp *utbufp )
{
printf("%-8.8s", utbufp->ut_name);
printf(" ");
printf("%-8.8s", utbufp->ut_line);
printf(" ");
printf("%10ld", utbufp->ut_time);
printf("\n");
return 0;
}
int main()
{
struct utmp current_record;
int utmpfd;
int reclen = sizeof(current_record);
//打開utmp 文件
if ( (utmpfd = open(UTMP_FILE, O_RDONLY)) == -1 ){
perror( UTMP_FILE );
exit(1);
}
//讀取utmp中的每一條記錄
while ( read(utmpfd, ¤t_record, reclen) == reclen )
//顯示記錄中的相關信息
show_info(¤t_record);
//關閉utmp文件
close(utmpfd);
return 0;
}
這段代碼應用了前面學到的內容,在while循環內從文件中逐條地把數據讀取出來,存放在記錄current_record中,然后調用函數show_info把登錄信息顯示出來,當文件中已經沒有數據時,循環結束,最后關閉文件返回。這里調用了函數perror,這是一個系統函數,使用這個函數來處理系統報錯。
我們用gcc who1.c -o who1
編譯一下程序,運行結果如下:
我們自己編寫的who1已經可以工作了,它能正確顯示出用戶名、終端名、登錄時間,但跟系統的who
命令比起來還不完善,至少在兩處有問題需要改進的:
- 消除空白記錄
- 正確顯示登錄時間
針對這兩個問題,我們繼續編寫who的第2個版本,解決問題的方法還是通過閱讀聯機幫助manpages和頭文件utmp.h。
消除空白記錄
系統所帶的who
只列出已登錄用戶的信息,而剛才編寫的who1除了會列出已登錄的用戶,還會顯示其他的信息,而這些都來自於utmp文件。實際上utmp包含所有終端的信息,甚至那些尚未被用到的終端的信息也會存放在utmp中,所以要修改剛才的程序,做到能夠區分出哪些終端對應活動的用戶。如何區分呢?
一種簡單的思路是過濾掉那些用戶名為空的記錄,但這樣做是有問題的,如剛才的輸出中,用戶名為LOGIN的那一行對應的是控制台,而不是一個真實的用戶。最好有一種方法能夠指出某一條記錄確實對應着已登錄的用戶。
在utmp. h中,有以下內容:
utmp結構中有一個成員ut_type,當它的值為7(USER_PROCESS)時,表示這是一個巳經登錄的用戶。根據這一點,對原來的程序做以下修改,就可以消除空白行:
show_info( struct utmp * utbufp ){
if ( utbufp-〉ut_type != USER_PROCESS ) /* users only */
return;
printf ( " % - 8. 8s", utbufp ->ut_name) ; /* the username */
}
以可讀的方式顯示登錄時間
接下來要處理的是時間顯示的問題,要把時間以易於理解的形式顯示。還是需要借助聯機幫助manpages和頭文件utmp.h。
如果你C語言基礎比較好,對time.h比較熟悉,這是一個很簡單的問題。如果不知道怎么辦也沒關系,我們用man -k key1|grep key2|...
來搜索。現在要解決的問題是轉換(convert,transform)時間格式,我們用man - k time | grep convert
和man - k time | grep transform
搜一下:
who1中的時間是time_t數據類型,用一個整數來表示,它的數值是從1970年1月1日0時開始所經過的秒數。ctime(3)將表示時間的整數值轉換成人們日常所使用的時間形式。
關於程序設計中的時間,可以參考漫談程序設計中的時間。
ctime(3)函數要輸人一個指針,返回的時間字符串類似於以下格式:
Wed Jun 30 21:49:08 2016
注意:並不是所有的字符串內容都需要,需要的是其中用標識出來的部分,接下來就很容易處理了,將ctime返回的字符串從第4個字符開始,輸出12個字符:
printf("% 12.12s",ctime(&t) + 4)
我們可以編寫who2.c了:
#include <stdio.h>
#include <unistd.h>
#include <utmp.h>
#include <fcntl.h>
#include <time.h>
void showtime(long);
void show_info(struct utmp *);
int main()
{
struct utmp utbuf; /* read info into here */
int utmpfd; /* read from this descriptor */
if ( (utmpfd = open(UTMP_FILE, O_RDONLY)) == -1 ){
perror(UTMP_FILE);
exit(1);
}
while( read(utmpfd, &utbuf, sizeof(utbuf)) == sizeof(utbuf) )
show_info( &utbuf );
close(utmpfd);
return 0;
}
void show_info( struct utmp *utbufp )
{
if ( utbufp->ut_type != USER_PROCESS )
return;
printf("%-8.8s", utbufp->ut_name); /* the logname */
printf(" "); /* a space */
printf("%-8.8s", utbufp->ut_line); /* the tty */
printf(" "); /* a space */
showtime( utbufp->ut_time ); /* display time */
printf("\n"); /* newline */
}
void showtime( long timeval )
{
char *cp; /* to hold address of time */
cp = ctime(&timeval); /* convert time to string */
/* string looks like */
/* Mon Feb 4 00:46:40 EST 2016 */
/* 0123456789012345. */
printf("%12.12s", cp+4 ); /* pick 12 chars from pos 4 */
}
至此就完成了who
命令的編寫。
系統調用學習示例2-cp
我們再通過cp
命令示范如何學習系統調用,同樣有三個問題:
cp
命令能做什么cp
命令是如何實現的?- 能不能自己編寫一個
cp
命令?
問題1. cp
命令能做什么
cp
能夠復制文件,典型的用法是:
cp source-file target-file
如果target-file所指定的文件不存在,cp
就創建這個文件,如果已經存在就覆蓋,target-file的內容與source-file相同。
問題2. cp
命令是如何實現的?
了解cp
的功能,實現cp
很簡單,偽代碼:
打開source-file
創建target-file
從source-file讀出一段數據
把這段數據寫入target-file
關閉source-file
關閉target-file
問題3. 能不能自己編寫一個cp
命令?
現在的問題是如何創建文件?如何寫文件?
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#define BUFFERSIZE 4096
#define COPYMODE 0644
void oops(char *, char *);
int main(int argc, char *argv[])
{
int in_fd, out_fd, n_chars;
char buf[BUFFERSIZE];
if (argc != 3) {
fprintf(stderr, "usage: %s source destination\n", *argv);
exit(1);
}
if ((in_fd = open(argv[1], O_RDONLY)) == -1)
oops("Cannot open ", argv[1]);
if ((out_fd = creat(argv[2], COPYMODE)) == -1)
oops("Cannot creat", argv[2]);
while ((n_chars = read(in_fd, buf, BUFFERSIZE)) > 0)
if (write(out_fd, buf, n_chars) != n_chars)
oops("Write error to ", argv[2]);
if (n_chars == -1)
oops("Read error from ", argv[1]);
if (close(in_fd) == -1 || close(out_fd) == -1)
oops("Error closing files", "");
}
void oops(char *s1, char *s2)
{
fprintf(stderr, "Error: %s ", s1);
perror(s2);
exit(1);
}
這里要注意main函數的兩個參數:
- argc記錄了用戶在運行程序的命令行中輸入的參數的個數。
- arg[]指向的數組中至少有一個字符指針,即arg[0].它通常指向程序中的可執行文件的文件名。
總結
通過上面的例子,我們學習了Linux中學習Linxu系統編程的方法:
- 仔細研究manpages
- 問題驅動,使用
man -k key1|grep key2|...
在manpages中搜索你要的內容 - 閱讀.h文件: 可以通過
grep -nr XXXX /usr/incldue
查找相關的宏定義,結構體定義,類型定義等 - 解決一個問題要多個系統調用,可以參考manpages的
SEE ALSO
部分來得到相關系統調用的信息
更好的答案
我們上面通過man -k key1|grep key2|...
推導出實現who
命令,cp
命令所需要的系統調用,這個過程對學習來講意義很大。其實我們可以使用strace who
來查看實際上調用了哪些系統調用。十種不好的學習方式中說「只看問題答案而不去自己解題」是個不好的學習方式:
看答案的時候感覺什么都會,自己一碰到題目立刻抓瞎,這種情況怕是誰都遭遇過罷。
看答案的過程是一個單向輸入的過程,而輸入結果怎么樣,只有靠輸出才能驗證。所謂厚積薄發,其實在很多方面都適用,想要自己獨立解答出題目,就必須對相關概念以及概念背后的知識理解到一定深度才可能成功。看懂答案僅僅是掌握的第一步,止步於此會讓所有的以為理解變成最終遺忘。
與看答案的壞方式類似的還有一種壞方式,就是僅思考解題思路而不動手解題。中學時候一同學成績不錯,做課后習題很少動手,只是在大腦里模擬解題,感覺思路沒有問題后就在題目旁邊標注一個 “易”,然后跳過繼續往后做其它題目。這種方式貌似很高效,最后卻在考試中慘敗,很多本以為思路很明確的地方在實際解題時卻發現完全不是自己想象的那樣。所以勤動手永遠是學習中最重要的幾點之一。
常用的系統調用
通過上面的方法,我們可以學習Linux的核心系統調用,學習路徑參考下圖:
通過「做中學(Learning by doing)」進行Linux系統編程學習是個很好的方法。有了上面的學習方法,剩下的就是時間問題了。
按我的經驗,學Linux系統調用一兩 個月夠了,按某個培訓機構的說法,你可以月薪11k了:
參考資料
- Unix/Linux編程實踐教程(Understanding UNIX/LINUX Programming)
- 深入理解計算機系統(英文版,官網2E,官網3E)
- Unix系統編程(Unix Systems Programming)
- 計算機系統要素(The Elements of Computing Systems)
- Unix編程環境(The Unix Programming Environment)
- 操作系統教程
歡迎關注“rocedu”微信公眾號(手機上長按二維碼)
做中教,做中學,實踐中共同進步!
-
版權聲明:自由轉載-非商用-非衍生-保持署名| Creative Commons BY-NC-ND 3.0
如果你覺得本文對你有幫助,請點一下左下角的“好文要頂”和“收藏該文”