學號:sa×××310 姓名:××濤
環境:Ubuntu13.04 gcc4.7.3
1.進程管理
Linux中的進程主要由kernel來管理。系統調用是應用程式與核心互動的一種方式。系統調用作為一種介面,通過系統調用,應用程式能夠進入作業系統核心,從而使用核心提供的各種資源,比如操作硬體,開關中斷,改變特權模式等等。
常見的系統調用:exit,fork,read,write,open,close,waitpid,execve,lseek,getpid...
使用者態和核心態
為了使作業系統提供一個很好的進程抽象,限制一個程式可以執行的指令和可以訪問的地址空間。
處理器通常是使用某個控制寄存器中的一個模式位來提供這種功能,該寄存器描述了進程當前享有的特權。當設定了模式位時,進程就運行在核心態,可以執行指令集中的任何指令,並且可以訪問系統中任何儲存空間位置。
沒有設定模式位時,進程就運行在使用者態,不允許執行特權指令,也不允許直接引用地址空間中核心區內的代碼和資料。任何這樣的嘗試都會導致致命的保護故障,反之,使用者程式必須通過系統調用介面間接地訪問核心代碼和資料。
關於fork的分析,參見這篇博文。
waitpid
首先來瞭解一下殭屍進程,當一個進程由於某種原因終止時,核心並不是立即把它從系統中清除。相反,進程被儲存在一種已終止的狀態中,直到它的夫進程回收。當父進程回收已終止的子進程時,核心將子進程的退出狀態傳遞給父進程,然後拋棄已終止的進程,從此時開始,該進程就不存在了。一個終止了但還未被回收的進程稱為殭屍進程。
如果父進程沒有回收子進程就終止了,子進程就成了殭屍進程,即時沒有運行,但仍然消耗系統的儲存空間資源。
一個進程可以通過調用waitpid函數來等待它的子進程終止或是停止。
函數原型如下:
pid_t waitpid(pid_t pid, int *status, int options)
如果成功,則為子進程的PID,如果WNOHANG,則為0,如果其他錯誤,則為-1.
看一個waitpid函數的例子。
#include"csapp.h"#include<errno.h>#define N 5int main(){ int status, i; pid_t pid; for(i=0; i<N; i++) { if((pid = Fork())==0) exit(100+i); } while((pid = waitpid(-1, &status, 0))>0) { if(WIFEXITED(status)) printf("Child %d exited normally with status=%d!\n",pid,WIFEXITED(status)); else printf("Child %d terminated abnormally!\n",pid); } if(errno != ECHILD) unix_error("waitpid error\n"); return 1;}
運行結果
waitpid的第一個參數是-1,則等待集合由父進程的所有子進程組成。大於0的話就是等待進程的pid。
waitpid的第三個參數是-1,則waitpid會掛起調用進程的執行,直到它的等待集合的一個子進程終止。如果等待集合中的一個進程終止了,那麼waitpid就立即返回。
程式啟動並執行結果就是waitpid函數不按照特定的順序回收僵死的子進程。
提一下wait函數,它就是waitpid函數的簡單版本,原型如下:
pid_t wait(int *status)
等價於waitpid(-1, &status, 0)
execve
在Linux中要使用exec函數族來在 一個進程中啟動另一個程式。系統調用execve()對當前進程進行替換,替換者為一個指定的程式,其參數包括檔案名稱(filename)、參數列表(argv)以及環境變數(envp)。exec函數族當然不止一個,但它們大致相同,在 Linux中,它們分別是:execl,execlp,execle,execv,execve和execvp,下面我只以execve為例,其它函數究竟與execlp有何區別,請通過man exec命令來瞭解它們的具體情況。
一個進程一旦調用exec類函數,它本身就"死亡"了,系統把程式碼片段替換成新的程式的代碼,廢棄原有的資料區段和堆棧段,並為新程式分配新的資料區段與堆棧段,唯一留下的,就是進程號,也就是說,對系統而言,還是同一個進程,不過已經是另一個程式了。(不過exec類函數中有的還允許繼承環境變數之類的資訊。)
原型如下:
int execve(const char *filename, const char *argv[], const char *envp[]);
成功調用不會返回,出錯返回-1.
execve函數載入並運行可執行目標檔案filename,且帶參數列表argv和環境變數列表envp.只有當出現錯誤時,例如找不到filename,execve才會返回到調用程式。所以,與fork一次調用返回兩次,execve調用一次並從不返回。
argv的在記憶體中組織方式如:
argv[0]是可執行目標檔案的名字。
envp的在記憶體中組織方式如:
環境變數的列表是由一個和指標數組類似的資料結構表示,envp變數指向一個以null結尾的指標數組,其中每個指標指向一個環境變數串,其中每個串都是形如“NAME=VALUE”的索引值對。
可以用下面的命令來列印命令列參數和環境變數:
#include"csapp.h"int main(int argc, char *argv[], char *envp[]){int i; printf("Command line arguments:\n"); for(i=0; argv[i]!=NULL; i++) printf("argv[%2d]: %s\n", i, argv[i]); printf("\n"); printf("Environment variables:\n"); for(i=0; envp[i]!=NULL; i++) printf("envp[%2d]: %s\n", i, envp[i]); exit(0);}
2.簡單的shell結合上面的fork,wait和exec,下面來實現一個簡單shell。先搭建一個shell架構,步驟是讀取一個來自使用者的命令列,求值並解析命令列。
#include<stdio.h>#include"csapp.h"#define MAXARGS 128void eval(char *cmdline);int parseline(char *buf,char **argv);int builtin_command(char **argv);int main(){char cmdline[MAXLINE];while(1){printf("> ");Fgets(cmdline,MAXLINE,stdin);if(feof(stdin)) exit(0);eval(cmdline);}//printf("Hello\n");return 1;}int builtin_command(char **argv){ if(!strcmp(argv[0],"quit")) exit(0); if(!strcmp(argv[0],"&")) return 1; if(!strcmp(argv[0],"-help")) { printf("-help help infomation.\n"); printf("ls list files and folders of current path.\n"); printf("pwd show current path.\n"); return 1; } if(!strcmp(argv[0],"pwd")) { printf("%s\n",getcwd(NULL,0)); return 1;} return 0;}void eval(char *cmdline){char *argv[MAXARGS];char buf[MAXLINE];int bg;pid_t pid;strcpy(buf, cmdline);bg = parseline(buf, argv);if(argv[0] ==NULL) return;if(!builtin_command(argv)){if((pid = Fork()) == 0){if(execve(argv[0],argv,environ) < 0){printf("%s:Command not found.\n",argv[0]);exit(0);}}if(!bg){int status;if(waitpid(pid,&status,0)<0)unix_error("waitfg:waitpid error");}else printf("%d %s",pid, cmdline);}return;}int parseline(char *buf, char **argv){char *delim;int argc;int bg;buf[strlen(buf)-1]=' ';while(*buf && (*buf==' ')) buf++;argc = 0;while((delim = strchr(buf,' '))){argv[argc++] = buf;*delim = '\0';buf = delim + 1;while(*buf && (*buf==' ')) buf++;}argv[argc] = NULL;if(argc == 0) return 1;bg = (*argv[argc-1] == '&');if(bg !=0) argv[--argc] = NULL;return bg; }
解釋一下代碼。主要的幾個函數:eval:解釋收到的命令。parseline:解析以空格分隔的命令列參數,並構造argv傳遞給execve,執行相應的程式。builtin_command: 檢測參數是否為shell的內建命令,如果是,就立即解釋這個命令,並返回1,否則返回0.下面用通過一些System Call,實現幾個linux的常用命令。
ls顯示當前路徑下的檔案和檔案夾資訊。c代碼實現:
#include<stdio.h>#include<time.h>#include<sys/types.h>#include<dirent.h>#include<sys/stat.h>#include<stdlib.h>#include<string.h>#include<pwd.h> #include<grp.h>void do_ls(char[]);void dostat(char *);void show_file_info(char *,struct stat *);void mode_to_letters(int,char[]);char * uid_to_name(uid_t);char * gid_to_name(gid_t);void main(int argc,char *argv[]){ if(argc==1) do_ls("."); else printf("Error input\n");}void do_ls(char dirname[]){ DIR *dir_ptr; //Path struct dirent *direntp; //Struct to save next file node if((dir_ptr=opendir(dirname))==0) fprintf(stderr,"ls:cannot open %s\n",dirname); else{ while((direntp=readdir(dir_ptr))!=0) dostat(direntp->d_name); closedir(dir_ptr); }}void dostat(char *filename){ struct stat info; if(lstat(filename,&info)==-1) perror("lstat"); else show_file_info(filename,&info);}void show_file_info(char *filename,struct stat *info_p){ char modestr[11]; mode_to_letters(info_p->st_mode,modestr); printf("%-12s",modestr); printf("%-4d",(int)info_p->st_nlink); printf("%-8s",uid_to_name(info_p->st_uid)); printf("%-8s",gid_to_name(info_p->st_gid)); printf("%-8ld",(long)info_p->st_size); time_t timelong=info_p->st_mtime; struct tm *htime=localtime(&timelong); printf("%-4d-%02d-%02d %02d:%02d",htime->tm_year+1990,htime->tm_mon+1,htime->tm_mday,htime->tm_hour,htime->tm_min); printf(" %s\n",filename);}//cope with permissionvoid mode_to_letters(int mode,char str[]){ strcpy(str,"----------"); if(S_ISDIR(mode)) str[0]='d'; if(S_ISCHR(mode)) str[0]='c'; if(S_ISBLK(mode)) str[0]='b'; if(mode & S_IRUSR) str[1]='r'; if(mode & S_IWUSR) str[2]='w'; if(mode & S_IXUSR) str[3]='x'; if(mode & S_IRGRP) str[4]='r'; if(mode & S_IWGRP) str[5]='w'; if(mode & S_IXGRP) str[6]='x'; if(mode & S_IROTH) str[7]='r'; if(mode & S_IWOTH) str[8]='w'; if(mode & S_IXOTH) str[9]='x';}//transfor uid to usernamechar * uid_to_name(uid_t uid){ struct passwd *pw_str; static char numstr[10]; if((pw_str=getpwuid(uid))==NULL){ sprintf(numstr,"%d",uid); return numstr; } else return pw_str->pw_name;}//transfor gid to usernamechar * gid_to_name(gid_t gid){ struct group *grp_ptr; static char numstr[10]; if((grp_ptr=getgrgid(gid))==NULL){ sprintf(numstr,"%d",gid); return numstr; } else return grp_ptr->gr_name;}
實現思路:主要是do_ls函數,通過opendir命令開啟檔案夾,然後用readdir來讀取檔案夾中的檔案或檔案夾,輸出資訊。通過剛才的shell調用編譯好ls程式,效果如下:3.訊號 非強制中斷訊號(signal,又簡稱為訊號)用來通知進程發生了非同步事件。進程之間可以互相通過系統調用kill發送非強制中斷訊號。核心也可以因為內部事件而給進程發送訊號,通知進程發生了某個事件。注意,訊號只是用來通知某進程發生了什麼事件,並不給該進程傳遞任何資料。
收到訊號的進程對各種訊號有不同的處理方法。處理方法可以分為三類:第一種是類似中斷的處理常式,對於需要處理的訊號,進程可以指定處理函數,由該函數來處 理。第二種方法是,忽略某個訊號,對該訊號不做任何處理,就象未發生過一樣。第三種方法是,對該訊號的處理保留系統的預設值,這種預設操作,對大部分的信 號的預設操作是使得進程終止。進程通過系統調用signal來指定進程對某個訊號的處理行為。 比如一個進程可以通過向另一個進程發送SIGKILL訊號強制終止它。當一個子進程終止或者停止時,核心會發送一個SIGCHLD給父進程。 訊號有很多種,每種訊號類型都對應於某種系統事件。訊號的處理流程如下:定義訊號的接受處理函數原型如下:
#include <signal.h>typedef void (*sighandler_t)(int);sighandler_t signal(int signum, sighandler_t handler);Returns: ptr to previous handler if OK, SIG_ERR on error (does not set errno)
看一個接受訊號的例子:
#include "csapp.h"/* SIGINT handler */void handler(int sig){return; /* Catch the signal and return */}unsigned int snooze(unsigned int secs) {unsigned int rc = sleep(secs);printf("Slept for %u of %u secs.\n", secs - rc, secs);return rc;}int main(int argc, char **argv) {if (argc != 2) {fprintf(stderr, "usage: %s <secs>\n", argv[0]);exit(0);}if (signal(SIGINT, handler) == SIG_ERR) /* Install SIGINT handler */unix_error("signal error\n");(void)snooze(atoi(argv[1]));exit(0);}
程式解析:程式接受一個int參數,用於設定sleep的秒數,正常情況下sleep相應的秒數之後就自動結束程式,由於註冊了SIGINI,當按下鍵盤的Ctrl+C鍵的時候,跳轉到handler函數,處理訊號。4.動態連結和靜態連結庫有動態與靜態兩種,動態通常用.so為尾碼,靜態用.a為尾碼。例如:libhello.so libhello.a
為了在同一系統中使用不同版本的庫,可以在庫檔案名稱後加上版本號碼為尾碼,例如: libhello.so.1.0,由於程式串連預設以.so為檔案尾碼名。所以為了使用這些庫,通常使用建立符號串連的方式。
ln -s libhello.so.1.0 libhello.so.1
ln -s libhello.so.1 libhello.so
使用庫
當 要使用靜態程式庫時,連接器會找出程式所需的函數,然後將它們拷貝到執行檔案,由於這種拷貝是完整的,所以一旦串連成功,靜態程式庫也就不再需要了。然 而,對動態庫而言,就不是這樣。動態庫會在執行程式內留下一個標記‘指明當程式執行時,首先必須載入這個庫。由於動態庫節省空間的,linux下進行串連的 預設操作是首先串連動態庫,也就是說,如果同時存在靜態和動態庫,不特別指定的話,將與動態庫相串連。
5.ELF檔案格式與進程地址空間的聯絡進程地址空間中典型的儲存地區分配情況如下:可以看出:
從低地址到高地址分別為:程式碼片段、(初始化)資料區段、(未初始化)資料區段(BSS)、堆、棧、命令列參數和環境變數
堆向高記憶體位址生長
棧向低記憶體位址生長對於ELF檔案,一般有下面幾個段.text section:主要是編譯後的源碼指令,是唯讀欄位。
.data section :初始化後的非const的全域變數、局部static變數。
.bss:未初始化後的非const全域變數、局部static變數。
.rodata:是存放唯讀資料 關於ELF的檔案的只是這裡就不贅述了。在ELF檔案中,使用section和program兩種結構描述檔案的內容。通常來說,ELF可重定位檔案採用section,ELF可執行檔使用program,可重連結檔案則兩種都用。
裝載檔案,其實是一個很簡單的過程,通過section或者program中的type屬性判斷是否需要載入,然後通過offset屬性找到檔案中的資料,將它讀取(複製)到相應的記憶體位置就可以了。 這個位置,可以通過program裡面的vaddr屬性確定;對於section來說,則可以自己定義裝載的位置。動態串連的本質,就是對ELF檔案進行重定位和符號解析。
重定位可以使得ELF檔案可以在任意的執行(普通程式在連結時會給定一個固定執行地址);符號解析,使得ELF檔案可以引用動態資料(連結時不存在的資料)。
從流程上來說,我們只需要進行重定位。而符號解析,則是重定位流程的一個分支。6.參考程式員的自我修養—連結、裝載與庫
Computer Systems: A Programmer's Perspective 3rd EdithLinux核心編程understanding the kernel 3rd Edith