系統調用跟我學(4)

←手機掃碼閱讀     火星人 @ 2014-03-12 , reply:0
  進程管理相關的系統調用之三
雷鎮 (leicool@21cn.com)
2002 年 9 月

這是本專欄中進程相關的系統調用的最後一篇,用2個實例演示了以往學習的內容。其一是Mini Shell,仿常用的Bash而做,但對其作了大大簡化;其二是一個Daemon程序,可以使讀者一窺伺服器編程的端倪。
1.13 Shell

對Linux不是太陌生的讀者都應該對Shell有一定的了解,就是這個程序在我們登陸后自動執行,列印出一個$符號,然後等待我們輸入命令。Linux下最常用的Shell應用程序是Bash,絕大部分Linux發行版默認安裝的都是它。下面我們也來親手編寫一個Shell程序,這個Shell遠遠不如Bash複雜,但也能滿足我們一般的使用,下面,我們就開始。

首先,給這個Shell取一個名字,不妨就叫做Mini Shell。

Linux系統的命令分為內部命令和外部命令兩種,內部命令由Shell程序實現,如cd、echo等,Linux的內部命令數量有限,而且絕大部分都很少用到。而每一個Linux外部命令都是一個單獨的應用程序,我們非常熟悉的ls、cp等絕大多數命令都是外部命令,這些命令都以可執行文件的形式存在,絕大部分放在目錄/bin和/sbin中。這樣一來,我們編程的難度就可以大大下降了,我們只需要實現很有限的內部命令,對於其它的輸入,統統當作應用程序來執行即可。

為了簡單明了起見,Mini Shell只實現了2個內部命令:
1、cd 用於切換目錄,和我們熟悉的命令cd類似,除了沒有那麼多的附加功能。
2、quit 用於退出Mini Shell。

下面是程序清單:

1: /* mshell.c */
2: #include
1: #include
3: #include
4: #include
5: #include
6: #include
7:
9: void do_cd(char *argv[]);
10: void execute_new(char *argv[]);
11:
12: main()
13: {
14: char *cmd=(void *)malloc(256*sizeof(char));
15: char *cmd_arg[10];
16: int cmdlen,i,j,tag;
17:
18: do{
19: /* 初始化cmd */
20: for(i=0;i<255;i++) cmd[i]='\0';
21:
22: printf("-=Mini Shell=-*| ");
23: fgets(cmd,256,stdin);
24:
25: cmdlen=strlen(cmd);
26: cmdlen--;
27: cmd[cmdlen]='\0';
28:
29: /* 把命令行分解為指針數組cmd_arg */
30: for(i=0;i<10;i++) cmd_arg[i]=NULL;
31: i=0; j=0; tag=0;
32: while(i33: if(cmd[i]==' '){
34: cmd[i]='\0';
35: tag=0;
36: }else{
37: if(tag==0)
38: cmd_arg[j++]=cmd+i;
39: tag=1;
40: }
41: i++;
42: }
43:
44: /* 如果參數超過10個,就列印錯誤,並忽略當前輸入 */
45: if(j>=10 && i46: printf("TOO MANY ARGUMENTS\n");
47: continue;
48: }
49:
50: /* 命令quit:退出Mini Shell */
51: if(strcmp(cmd_arg[0],"quit")==0)
52: break;
53:
54: /* 命令cd */
55: if(strcmp(cmd_arg[0],"cd")==0){
56: do_cd(cmd_arg);
57: continue;
58: }
59:
60: /* 外部命令或應用程序 */
61: execute_new(cmd_arg);
62: }while(1);
63: }
64:
65: /* 實現cd的功能 */
66: void do_cd(char *argv[])
67: {
68: if(argv[1]!=NULL){
69: if(chdir(argv[1])<0)
70: switch(errno){
71: case ENOENT:
72: printf("DIRECTORY NOT FOUND\n");
73: break;
74: case ENOTDIR:
75: printf("NOT A DIRECTORY NAME\n");
76: break;
77: case EACCES:
78: printf("YOU DO NOT HAVE RIGHT TO ACCESS\n");
79: break;
80: default:
81: printf("SOME ERROR HAPPENED IN CHDIR\n");
82: }
83: }
84:
85: }
86:
87: /* 執行外部命令或應用程序 */
88: void execute_new(char *argv[])
89: {
90: pid_t pid;
91:
92: pid=fork();
93: if(pid<0){
94: printf("SOME ERROR HAPPENED IN FORK\n");
95: exit(2);
96: }else if(pid==0){
97: if(execvp(argv[0],argv)<0)
98: switch(errno){
99: case ENOENT:
100: printf("COMMAND OR FILENAME NOT FOUND\n");
101: break;
102: case EACCES:
103: printf("YOU DO NOT HAVE RIGHT TO ACCESS\n");
104: break;
105: default:
106: printf("SOME ERROR HAPPENED IN EXEC\n");
107: }
108: exit(3);
109: }else
110: wait(NULL);
111: }

這個程序稍稍有點長,我們來對它作一下詳細的解釋:

函數main:

14行:定義字元串cmd,用於接收用戶輸入的命令行。
15行:定義指針數組cmd_arg,它的形式和作用都與我們熟悉的char *argv[]一樣。

從以上2個定義可以看出Mini Shell對命令輸入的2個限制:首先,用戶輸入的命令行必須在255個字元之內(除去字元串結束標誌'\0');其次,命令行的參數個數不得超過10個(包括命令本身在內)。

18行:進入一個do-while循環,這個循環是本程序的主體部分,基本思想是"等待輸入命令--處理已輸入命令--等待輸入命令"。

22行:列印輸入提示信息。在Mini Shell中,你可以隨意定自己喜歡的命令輸入提示信息,本程序中使用了"-=Mini Shell=-*| ",是不是有點像一個CS高手?如果不喜歡,你可以用任意的字元替換它。

23行:接收用戶輸入。

25-27行:fgets接受輸入時,會將輸入字元串時末尾的換行符("\n")一起接受,這是我們不需要的,所以要把它去掉。本程序中簡單的用字元串結束標誌'\0'覆蓋了字元串cmd的最後一個字元來實現這個目的。

30行:初始化指針數組cmd_arg。

32-42行:對輸入進行分析,將cmd中參數間的空格用'\0'填充,並把各參數的起始地址分別賦與cmd_arg數組。這樣就把cmd分解成了cmd_arg,但分解后的各命令參數仍然使用著cmd的內存空間,所以在命令執行結束前不宜對cmd另外賦值。

45行:如果還未分析到輸入字元串的末尾(i=10),就認為輸入的命令行超出了10個參數的限制,列印錯誤並重新接收命令。

51-52行:內部命令quit:字元串cmd_arg[0]就是命令本身,如果命令是quit,則退出循環,也就等於退出該程序。

55-58行:內部命令cd:調用函數do_cd()完成cd命令的動作。

61行:對於其它的外部命令和應用程序,調用函數execute_new()執行。

函數do_cd:

68行:僅僅考慮緊跟在命令後面的參數argv[1],而不再考慮其它的參數。如果這個參數存在,就把它作為要轉換的目錄。

69行:調用系統調用chdir切換當前目錄,參見附錄1。

70-82行:對chdir可能出現的錯誤進行處理。

函數execute_new:

92行:調用系統調用fork產生新的子進程。

93行:如果返回負值,說明fork調用出現錯誤。

96行:如果返回0,說明當前進程是子進程。

97行:調用execvp執行新的應用程序,並檢測調用是否出錯(返回負值)。這裡使用execvp的原因是它可以自動在各默認目錄里尋找目標應用程序的位置,而不必我們自己編程實現。

98-107行:對execvp可能出現的錯誤進程處理。

108行:如果execvp的執行出現錯誤,子進程在這裡終止。表面上看起來,這個exit是接著97行的錯誤判斷的下一行語句,而非if語句的一部分,似乎無論調用execvp成功與否都會接著執行exit。但事實上,如果execvp調用成功的話,這個進程將會被新的程序代碼填充,因而根本不可能執行到這一行。反之,如果執行到了這一行,說明前面的execvp調用一定出現了錯誤。這樣的效果和exit被包含在if語句中的效果是完全一樣的。

109行:如果fork返回其它值,說明當前進程是父進程。

110行:調用系統調用wait。wait在這裡有兩個作用:

使父進程在此暫停,等待子進程執行完畢。這樣,就可以等子進程的所有信息全部輸出完畢后才列印命令提示符,等待下一條命令的輸入,從而避免了命令提示符和應用程序輸出混雜在一起的現象。
收集子進程退出后留下的殭屍進程。可能有讀者一直對這個問題存有疑問--"我們編程生成的子進程由我們自己設計的父進程負責收集,但我們手動執行的這個父進程由誰收集呢?"現在大家應該明白了,我們從命令行執行的所有進程最後都是由shell收集的。
關於Mini Shell的編譯和運行,這裡就不再敷述了,有興趣的讀者可以自行動手實驗,或者對這個程序進行改進,使之更接近甚至超過我們正使用的Bash。

1.14 daemon進程

1.14.1 了解daemon進程

這又是一個有趣的概念,daemon在英語中是"精靈"的意思,就像我們經常在迪斯尼動畫里見到的那些,有些會飛,有些不會,經常圍著動畫片的主人公轉來轉去,????艫靨嵋恍┲腋媯?輩皇鋇姑溝刈蒼謚?由希?惺焙蚧夠嵯氤鮃恍┬⌒〉幕ㄕ校?閻魅斯?擁腥聳種芯瘸隼矗??蛉緔耍?aemon有時也被譯作"守護神"。所以,daemon進程在國內也有兩種譯法,有些人譯作"精靈進程",有些人譯作"守護進程",這兩種稱呼的出現頻率都很高。

與真正的daemon相似,daemon進程也習慣於把自己隱藏在人們的視線之外,默默為系統做出貢獻,有時人們也把它們稱作"後台服務進程"。daemon進程的壽命很長,一般來說,從它們一被執行開始,直到整個系統關閉,它們才會退出。幾乎所有的伺服器程序,包括我們熟知的Apache和wu-FTP,都用daemon進程的形式實現。很多Linux下常見的命令如inetd和ftpd,末尾的字母d就是指daemon。

為什麼一定要使用daemon進程呢?Linux中每一個系統與用戶進行交流的界面稱為終端(terminal),每一個從此終端開始運行的進程都會依附於這個終端,這個終端就稱為這些進程的控制終端(Controlling terminal),當控制終端被關閉時,相應的進程都會被自動關閉。關於這點,讀者可以用X-Window中的XTerm試驗一下,(每一個XTerm就是一個打開的終端,)我們可以通過鍵入命令啟動應用程序,比如:

$netscape

然後我們關閉XTerm窗口,剛剛啟動的netscape窗口也會隨之一同突然蒸發。但是daemon進程卻能夠突破這種限制,即使對應的終端關閉,它也能在系統中長久地存在下去,如果我們想讓某個進程長命百歲,不因為用戶或終端或其他的變化而受到影響,就必須把這個進程變成一個daemon進程。

1.14.2 daemon進程的編程規則

如果想把自己的進程變成daemon進程,我們必須嚴格按照以下步驟進行:

調用fork產生一個子進程,同時父進程退出。我們所有後續工作都在子進程中完成。這樣做我們可以:
如果我們是從命令行執行的該程序,這可以造成程序執行完畢的假象,shell會回去等待下一條命令;
剛剛通過fork產生的新進程一定不會是一個進程組的組長,這為第2步的執行提供了前提保障。

這樣做還會出現一種很有趣的現象:由於父進程已經先於子進程退出,會造成子進程沒有父進程,變成一個孤兒進程(orphan)。每當系統發現一個孤兒進程,就會自動由1號進程收養它,這樣,原先的子進程就會變成1號進程的子進程。

調用setsid系統調用。這是整個過程中最重要的一步。setsid的介紹見附錄2,它的作用是創建一個新的會話(session),並自任該會話的組長(session leader)。如果調用進程是一個進程組的組長,調用就會失敗,但這已經在第1步得到了保證。調用setsid有3個作用:
讓進程擺脫原會話的控制;
讓進程擺脫原進程組的控制;
讓進程擺脫原控制終端的控制;

總之,就是讓調用進程完全獨立出來,脫離所有其他進程的控制。

把當前工作目錄切換到根目錄。如果我們是在一個臨時載入的文件系統上執行這個進程的,比如:/mnt/floppy/,該進程的當前工作目錄就會是/mnt/floppy/。在整個進程運行期間該文件系統都無法被卸下(umount),而無論我們是否在使用這個文件系統,這會給我們帶來很多不便。解決的方法是使用chdir系統調用把當前工作目錄變為根目錄,應該不會有人想把根目錄卸下吧。關於chdir的用法,參見附錄1。
當然,在這一步里,如果有特殊的需要,我們也可以把當前工作目錄換成其他的路徑,比如/tmp。

將文件許可權掩碼設為0。這需要調用系統調用umask,參見附錄3。每個進程都會從父進程那裡繼承一個文件許可權掩碼,當創建新文件時,這個掩碼被用於設定文件的默認訪問許可權,屏蔽掉某些許可權,如一般用戶的寫許可權。當另一個進程用exec調用我們編寫的daemon程序時,由於我們不知道那個進程的文件許可權掩碼是什麼,這樣在我們創建新文件時,就會帶來一些麻煩。所以,我們應該重新設置文件許可權掩碼,我們可以設成任何我們想要的值,但一般情況下,大家都把它設為0,這樣,它就不會屏蔽用戶的任何操作。
如果你的應用程序根本就不涉及創建新文件或是文件訪問許可權的設定,你也完全可以把文件許可權掩碼一腳踢開,跳過這一步。

關閉所有不需要的文件。同文件許可權掩碼一樣,我們的新進程會從父進程那裡繼承一些已經打開了的文件。這些被打開的文件可能永遠不被我們的daemon進程讀或寫,但它們一樣消耗系統資源,而且可能導致所在的文件系統無法卸下。需要指出的是,文件描述符為0、1和2的三個文件(文件描述符的概念將在下一章介紹),也就是我們常說的輸入、輸出和報錯這三個文件也需要被關閉。很可能不少讀者會對此感到奇怪,難道我們不需要輸入輸出嗎?但事實是,在上面的第2步后,我們的daemon進程已經與所屬的控制終端失去了聯繫,我們從終端輸入的字元不可能達到daemon進程,daemon進程用常規的方法(如printf)輸出的字元也不可能在我們的終端上顯示出來。所以這三個文件已經失去了存在的價值,也應該被關閉。

下面,就然我們親眼看一個daemon進程的誕生:

1.14.3 一個daemon程序

/* daemon.c */
#include
#include
#include

#define MAXFILE 65535

main()
{
pid_t pid;
int i;

pid=fork();
if(pid<0){
printf("error in fork\n");
exit(1);
}else if(pid>0)
/* 父進程退出 */
exit(0);

/* 調用setsid */
setsid();

/* 切換當前目錄 */
chdir("/");

/* 設置文件許可權掩碼 */
umask(0);

/* 關閉所有可能打開的不需要的文件 */
for(i=0;i close(i);

/*
到現在為止,進程已經成為一個完全的daemon進程,
你可以在這裡添加任何你要daemon做的事情,如:
*/

for(;;)
sleep(10);
}

編譯和運行的任務就交給讀者們自己完成。daemon進程不像其他進程一樣有很搶眼的運行結果,基本上它只是毫不聲張地做自己的事。你不可能看到任何東西,但可以用"ps -ajx"命令觀察一下你的daemon進程的狀態和一些參數。

1.15 附錄

1.15.1 系統調用chdir

#include

int chdir(const char *path);

chdir的作用是改變當前工作目錄。進程的當前工作目錄一般是應用程序啟動時的目錄,一旦進程開始運行后,當前工作目錄就會保持不變,除非調用chdir。chdir只有1個字元串參數,就是要轉去的路徑。例如:

chdir("/");

進程的當前路徑就會變為根目錄。

1.15.2 系統調用setsid

#include

pid_t setsid(void);

一個會話(session)開始於用戶登陸,終止於用戶退出,在此期間該用戶運行的所有進程都屬於這個會話,除非進程調用setsid系統調用。

系統調用setsid不帶任何參數,調用之後,調用進程就會成立一個新的會話,並自任該會話的組長。

1.15.3 系統調用umask

#include
#include

mode_t umask(mode_t mask);

系統調用umask可以設定一個文件許可權掩碼,用戶可以用它來屏蔽某些許可權,以防止誤操作導致給予某些用戶過高的許可權。

參考資料:

Linux man pages
Advanced Programming in the UNIX Environment by W. Richard Stevens, 1993
作者簡介

雷鎮,您可以通過電子郵件 leicool@21cn.com和他聯繫。








[火星人 ] 系統調用跟我學(4)已經有306次圍觀

http://coctec.com/docs/linux/show-post-69683.html