欧美日韩不卡一区二区三区,www.蜜臀.com,高清国产一区二区三区四区五区,欧美日韩三级视频,欧美性综合,精品国产91久久久久久,99a精品视频在线观看

C語言

如何用C語言寫一個(gè)簡單的Unix Shell

時(shí)間:2025-04-01 16:28:56 C語言 我要投稿
  • 相關(guān)推薦

如何用C語言寫一個(gè)簡單的Unix Shell

  shell 是允許你與操作系統(tǒng)的核心作交互的一個(gè)界面(interface)。下面是小編為大家?guī)淼年P(guān)于如何用C語言寫一個(gè)簡單的 Unix Shell的知識(shí),歡迎閱讀。

  shell 是什么?

  關(guān)于這一點(diǎn)已經(jīng)有很多書面資料,所以對(duì)于它的定義我不會(huì)探討太多細(xì)節(jié)。只用一句話說明:

  shell 是允許你與操作系統(tǒng)的核心作交互的一個(gè)界面(interface)。

  shell 是怎樣工作的?

  shell解析用戶輸入的命令并執(zhí)行它。為了能做到這一點(diǎn),shell的工作流程看起來像這樣:

  啟動(dòng)shell

  等待用戶輸入

  解析用戶輸入

  執(zhí)行命令并返回結(jié)果

  回到第 2 步。

  但在這整個(gè)流程中有一個(gè)重要的部分:進(jìn)程。shell是父進(jìn)程。這是我們的程序的主線程,它等待用戶輸入。然而,由于以下原因,我們不能在主線程自身中執(zhí)行命令:

  一個(gè)錯(cuò)誤的命令會(huì)導(dǎo)致整個(gè)shell停止工作。我們要避免此情況。

  獨(dú)立的命令應(yīng)該有他們自己的進(jìn)程塊。這被稱為隔離,屬于容錯(cuò)(機(jī)制)。

  Fork

  為了能避免此情況,我們使用系統(tǒng)調(diào)用 fork。我曾以為我理解了 fork,直到我用它寫了大約4行代碼(才發(fā)現(xiàn)我沒有理解)。

  fork 創(chuàng)建當(dāng)前進(jìn)程的一份拷貝。這份拷貝被稱為“子進(jìn)程”,系統(tǒng)中的每個(gè)進(jìn)程都有與它聯(lián)系在一起的唯一的進(jìn)程 id(pid)。讓我們看以下代碼片段:

  fork.c

  #include

  #include

  #include

  int

  main() {

  pid_t child_pid = fork();

  // The child process

  if (child_pid == 0) {

  printf("### Child ###nCurrent PID: %d and Child PID: %dn",

  getpid(), child_pid);

  } else {

  printf("### Parent ###nCurrent PID: %d and Child PID: %dn",

  getpid(), child_pid);

  }

  return 0;

  }

  fork 系統(tǒng)調(diào)用返回兩次,每個(gè)進(jìn)程一次。這一開始聽起來是反直覺的。但讓我們看一下在底層發(fā)生了什么。

  通過調(diào)用 fork,我們在程序中創(chuàng)建了一個(gè)新的分支。這與傳統(tǒng)的 if-else 分支不同。fork 對(duì)當(dāng)前進(jìn)程創(chuàng)建一份拷貝并從中創(chuàng)建了一個(gè)新的進(jìn)程。最終系統(tǒng)調(diào)用返回子進(jìn)程的進(jìn)程 id。

  一旦 fork 調(diào)用成功,子進(jìn)程和父進(jìn)程(我們的代碼的主線程)會(huì)同時(shí)運(yùn)行。

  fork() 創(chuàng)建了一個(gè)新的子進(jìn)程,但與此同時(shí),父進(jìn)程的執(zhí)行并沒有停止。子進(jìn)程執(zhí)行的開始和結(jié)束獨(dú)立于父進(jìn)程,反之亦然。

  更進(jìn)一步討論以前,先說明一點(diǎn):getpid 系統(tǒng)調(diào)用返回當(dāng)前的進(jìn)程 id。

  如果你編譯并執(zhí)行這段代碼,會(huì)得到類似于下面的輸出:

  ### Parent ###

  Current PID: 85247 and Child PID: 85248

  ### Child ###

  Current PID: 85248 and Child PID: 0

  在 ### Parent ### 下面的片段中,當(dāng)前進(jìn)程 ID 是 85247,子進(jìn)程 ID 是 85248。注意,子進(jìn)程的 pid 比父進(jìn)程的大,表明子進(jìn)程是在父進(jìn)程之后創(chuàng)建的。(更新:正如某人在 Hacker News 上正確指出的,這并不是確定的,雖然往往是這樣。原因在于,操作系統(tǒng)可能回收無用的老進(jìn)程 id。)

  在 ### Child ### 下面的片段中,當(dāng)前進(jìn)程 ID 是 85248,這與前面片段中子進(jìn)程的 pid 相同。然而,這里的子進(jìn)程 pid 為 0。

  實(shí)際的數(shù)字會(huì)隨著每一次執(zhí)行而變化。

  你可能在想,我們已經(jīng)在第 9 行明確的給 child_pid 賦了一個(gè)值(譯者注:應(yīng)該是第7行),那么 child_pid 怎么會(huì)在同一個(gè)執(zhí)行流程中呈現(xiàn)兩個(gè)不同的值,這種想法值得原諒。但是,回想一下,調(diào)用 fork 創(chuàng)建了一個(gè)新進(jìn)程,這個(gè)新進(jìn)程與當(dāng)前進(jìn)程相同。因此,在父進(jìn)程中,child_pid 是剛創(chuàng)建的子進(jìn)程的實(shí)際值,而子進(jìn)程本身沒有自己的子進(jìn)程,所以 child_pid 的值為 0。

  因此,為了控制哪些代碼在子進(jìn)程中執(zhí)行,哪些又在父進(jìn)程中執(zhí)行,需要我們在 12 到 16 行定義的 if-else 塊(譯者注:應(yīng)該是 10 到 16 行)。當(dāng) child_pid 為 0 時(shí),代碼塊將在子進(jìn)程下執(zhí)行,而 else 塊卻會(huì)在父進(jìn)程下執(zhí)行。這些塊被執(zhí)行的順序是不確定的,取決于操作系統(tǒng)的調(diào)度程序。

  引入確定性

  讓我向你介紹系統(tǒng)調(diào)用 sleep。引用 linux man 頁面的話:

  sleep – 暫停執(zhí)行一段時(shí)間

  時(shí)間間隔以秒為單位。

  讓我們給父進(jìn)程,即我們代碼中的 else 塊,加一個(gè) sleep(1) 調(diào)用:

  sleep_parent.c

  #include

  #include

  #include

  int

  main() {

  pid_t child_pid = fork();

  // The child process

  if (child_pid == 0) {

  printf("### Child ###nCurrent PID: %d and Child PID: %dn",

  getpid(), child_pid);

  } else {

  sleep(1); // Sleep for one second

  printf("### Parent ###nCurrent PID: %d and Child PID: %dn",

  getpid(), child_pid);

  }

  return 0;

  }

  當(dāng)你執(zhí)行這段代碼時(shí),輸出將類似這樣:

  ### Child ###

  Current PID: 89743 and Child PID: 0

  1秒鐘以后,你將看到

  ### Parent ###

  Current PID: 89742 and Child PID: 89743

  每次執(zhí)行這段代碼時(shí)你會(huì)看到同樣的表現(xiàn)。這是因?yàn)椋何覀冊诟高M(jìn)程中做了一個(gè)阻塞性的 sleep 調(diào)用,與此同時(shí),操作系統(tǒng)調(diào)度程序發(fā)現(xiàn)有空閑的 CPU 時(shí)間可以給子進(jìn)程執(zhí)行。

  類似的,如果你反過來,把 sleep(1) 調(diào)用加到子進(jìn)程,也就是我們代碼中的 if 塊里面,你會(huì)發(fā)現(xiàn)父進(jìn)程塊立刻輸出到控制臺(tái)上。但你也會(huì)發(fā)現(xiàn)程序終止了。子進(jìn)程塊的輸出被轉(zhuǎn)存到標(biāo)準(zhǔn)輸出?雌饋硎沁@樣:

  $ gcc -lreadline blog/sleep_child.c -o sleep_child && ./sleep_child

  ### Parent ###

  Current PID: 23011 and Child PID: 23012

  $ ### Child ###

  Current PID: 23012 and Child PID: 0

  這段源代碼可在 sleep_child.c 獲取。

  這是因?yàn)楦高M(jìn)程在 printf 語句之后無事可做,被終止了。然而,子進(jìn)程在 sleep 調(diào)用處被阻塞了 1 秒鐘,之后才執(zhí)行 printf 語句。

  正確實(shí)現(xiàn)的確定性

  然而,使用 sleep 來控制進(jìn)程的執(zhí)行流程不是最好的方法,因?yàn)槟阕隽艘粋(gè) n 秒的 sleep 調(diào)用:

  你怎么確保不管你等待的是什么,都會(huì)在 n 秒內(nèi)完成執(zhí)行呢?

  不管你等待的是什么,要是它在遠(yuǎn)遠(yuǎn)早于 n 秒時(shí)就結(jié)束了呢?在此情況下你不必要地閑置了。

  有一種更好的方法是,使用 wait 系統(tǒng)調(diào)用(或一種變體)來代替。我們將使用 waitpid 系統(tǒng)調(diào)用。它帶有以下參數(shù):

  你想要程序等待的進(jìn)程的進(jìn)程 ID。

  一個(gè)變量,用來保存進(jìn)程如何終止的相關(guān)信息。

  選項(xiàng)標(biāo)志,用來定制 waitpid 的行為

  wait.c

  #include

  #include

  #include

  #include

  int

  main() {

  pid_t child_pid;

  pid_t wait_result;

  int stat_loc;

  child_pid = fork();

  // The child process

  if (child_pid == 0) {

  printf("### Child ###nCurrent PID: %d and Child PID: %dn",

  getpid(), child_pid);

  sleep(1); // Sleep for one second

  } else {

  wait_result = waitpid(child_pid, &stat_loc, WUNTRACED);

  printf("### Parent ###nCurrent PID: %d and Child PID: %dn",

  getpid(), child_pid);

  }

  return 0;

  }

  當(dāng)你執(zhí)行這段代碼,你會(huì)發(fā)現(xiàn)子進(jìn)程塊立刻被打印,然后等待很短的一段時(shí)間(這里我們在 printf 后面加了 sleep)。父進(jìn)程等待子進(jìn)程執(zhí)行結(jié)束,之后就有空?qǐng)?zhí)行它自己的命令。

  這里將介紹 exec 函數(shù)家族。即以下函數(shù):

  execl

  execv

  execle

  execve

  execlp

  execvp

  為了滿足需要,我們將使用 execvp,它的簽名看起來像這樣:

  int execvp(const char *file, char *const argv[]);

  函數(shù)名中的 vp 表明:它接受一個(gè)文件名,將在系統(tǒng) $PATH 變量中搜索此文件名,它還接受將要執(zhí)行的一組參數(shù)。

  你可以閱讀 exec 的 man 頁面 以得到其它函數(shù)的更多信息。

  讓我們看一下以下代碼,它執(zhí)行命令 ls -l -h -a:

  execvp.c

  #include

  int main() {

  char *argv[] = {"ls", "-l", "-h", "-a", NULL};

  execvp(argv[0], argv);

  return 0;

  }

  關(guān)于 execvp 函數(shù),有幾點(diǎn)需要注意:

  第一個(gè)參數(shù)是命令名。

  第二個(gè)參數(shù)由命令名和傳遞給命令自身的參數(shù)組成。并且它必須以 NULL 結(jié)束。

  它將當(dāng)前進(jìn)程的映像交換為被執(zhí)行的命令的映像,后面再展開說明。

  如果你編譯并執(zhí)行上面的代碼,你會(huì)看到類似于下面的輸出:

  total 32

  drwxr-xr-x 5 dhanush staff 170B Jun 11 11:32 .

  drwxr-xr-x 4 dhanush staff 136B Jun 11 11:30 ..

  -rwxr-xr-x 1 dhanush staff 8.7K Jun 11 11:32 a.out

  drwxr-xr-x 3 dhanush staff 102B Jun 11 11:32 a.out.dSYM

  -rw-r--r-- 1 dhanush staff 130B Jun 11 11:32

  它和你在你的主 shell 中手動(dòng)執(zhí)行l(wèi)s -l -h -a的結(jié)果完全相同。

  既然我們能執(zhí)行命令了,我們需要使用在第一部分中學(xué)到的fork 系統(tǒng)調(diào)用構(gòu)建有用的東西。事實(shí)上我們要做到以下這些:

  當(dāng)用戶輸入時(shí)接受命令。

  調(diào)用 fork 以創(chuàng)建一個(gè)子進(jìn)程。

  在子進(jìn)程中執(zhí)行命令,同時(shí)父進(jìn)程等待命令完成。

  回到第一步。

  我們看看下面的函數(shù),它接收一個(gè)字符串作為輸入。我們使用庫函數(shù) strtok 以空格分割該字符串,然后返回一個(gè)字符串?dāng)?shù)組,數(shù)組也用 NULL來終結(jié)。

  include

  #include

  char **get_input(char *input) {

  char **command = malloc(8 * sizeof(char *));

  char *separator = " ";

  char *parsed;

  int index = 0;

  parsed = strtok(input, separator);

  while (parsed != NULL) {

  command[index] = parsed;

  index++;

  parsed = strtok(NULL, separator);

  }

  command[index] = NULL;

  return command;

  }

  如果該函數(shù)的輸入是字符串 “l(fā)s -l -h -a”,那么函數(shù)將會(huì)創(chuàng)建這樣形式的一個(gè)數(shù)組:[“l(fā)s”, “-l”, “-h”, “-a”, NULL],并且返回指向此隊(duì)列的指針。

  現(xiàn)在,我們在主函數(shù)中調(diào)用 readline 來讀取用戶的輸入,并將它傳給我們剛剛在上面定義的 get_input。一旦輸入被解析,我們在子進(jìn)程中調(diào)用 fork 和 execvp。在研究代碼以前,看一下下面的圖片,先理解 execvp 的含義:

  當(dāng) fork 命令完成后,子進(jìn)程是父進(jìn)程的一份精確的拷貝。然而,當(dāng)我們調(diào)用 execvp 時(shí),它將當(dāng)前程序替換為在參數(shù)中傳遞給它的程序。這意味著,雖然進(jìn)程的當(dāng)前文本、數(shù)據(jù)、堆棧段被替換了,進(jìn)程 id 仍保持不變,但程序完全被覆蓋了。如果調(diào)用成功了,那么 execvp 將不會(huì)返回,并且子進(jìn)程中在這之后的任何代碼都不會(huì)被執(zhí)行。這里是主函數(shù):

  #include

  #include

  #include

  #include

  #include

  #include

  int main() {

  char **command;

  char *input;

  pid_t child_pid;

  int stat_loc;

  while (1) {

  input = readline("unixsh> ");

  command = get_input(input);

  child_pid = fork();

  if (child_pid == 0) {

  /* Never returns if the call is successful */

  execvp(command[0], command);

  printf("This won't be printed if execvp is successuln");

  } else {

  waitpid(child_pid, &stat_loc, WUNTRACED);

  }

  free(input);

  free(command);

  }

  return 0;

  }

  全部代碼可在此處的單個(gè)文件中獲取。如果你用 gcc -g -lreadline shell.c 編譯它,并執(zhí)行二進(jìn)制文件,你會(huì)得到一個(gè)最小的可工作 shell,你可以用它來運(yùn)行系統(tǒng)命令,比如 pwd 和 ls -lha:

  unixsh> pwd

  /Users/dhanush/github.com/indradhanush.github.io/code/shell-part-2

  unixsh> ls -lha

  total 28K

  drwxr-xr-x 6 root root 204 Jun 11 18:27 .

  drwxr-xr-x 3 root root 4.0K Jun 11 16:50 ..

  -rwxr-xr-x 1 root root 16K Jun 11 18:27 a.out

  drwxr-xr-x 3 root root 102 Jun 11 15:32 a.out.dSYM

  -rw-r--r-- 1 root root 130 Jun 11 15:38 execvp.c

  -rw-r--r-- 1 root root 997 Jun 11 18:25 shell.c

  unixsh>

  注意:fork 只有在用戶輸入命令后才被調(diào)用,這意味著接受用戶輸入的用戶提示符是父進(jìn)程。

  錯(cuò)誤處理

  到目前為止,我們一直假設(shè)我們的命令總會(huì)完美的運(yùn)行,還沒有處理錯(cuò)誤。所以我們要對(duì) shell.c做一點(diǎn)改動(dòng):

  fork – 如果操作系統(tǒng)內(nèi)存耗盡或是進(jìn)程數(shù)量已經(jīng)到了允許的最大值,子進(jìn)程就無法創(chuàng)建,會(huì)返回 -1。我們在代碼里加上以下內(nèi)容:

  ...

  while (1) {

  input = readline("unixsh> ");

  command = get_input(input);

  child_pid = fork();

  if (child_pid < 0) {

  perror("Fork failed");

  exit(1);

  }

  ...

  execvp – 就像上面解釋過的,被成功調(diào)用后它不會(huì)返回。然而,如果執(zhí)行失敗它會(huì)返回 -1。同樣地,我們修改 execvp 調(diào)用:

  ...

  if (execvp(command[0], command) < 0) {

  perror(command[0]);

  exit(1);

  }

  ...

  注意:雖然fork之后的exit調(diào)用終止整個(gè)程序,但execvp之后的exit 調(diào)用只會(huì)終止子進(jìn)程,因?yàn)檫@段代碼只屬于子進(jìn)程。

  malloc – It can fail if the OS runs out of memory. We should exit the program in such a scenario:

  malloc – 如果操作系統(tǒng)內(nèi)存耗盡,它就會(huì)失敗。在這種情況下,我們應(yīng)該退出程序:

  char **get_input(char *input) {

  char **command = malloc(8 * sizeof(char *));

  if (command == NULL) {

  perror("malloc failed");

  exit(1);

  }

  ...

  動(dòng)態(tài)內(nèi)存分配 – 目前我們的命令緩沖區(qū)只分配了8個(gè)塊。如果我們輸入的命令超過8個(gè)單詞,命令就無法像預(yù)期的那樣工作。這么做是為了讓例子便于理解,如何解決這個(gè)問題留給讀者作為一個(gè)練習(xí)。

  上面帶有錯(cuò)誤處理的代碼可在這里獲取。

  內(nèi)建命令

  如果你試著執(zhí)行 cd 命令,你會(huì)得到這樣的錯(cuò)誤:

  cd: No such file or directory

  我們的 shell 現(xiàn)在還不能識(shí)別cd命令。這背后的原因是:cd不是ls或pwd這樣的系統(tǒng)程序。讓我們后退一步,暫時(shí)假設(shè)cd 也是一個(gè)系統(tǒng)程序。你認(rèn)為執(zhí)行流程會(huì)是什么樣?在繼續(xù)閱讀之前,你可能想要思考一下。

  流程是這樣的:

  用戶輸入 cd /。

  shell對(duì)當(dāng)前進(jìn)程作 fork,并在子進(jìn)程中執(zhí)行命令。

  在成功調(diào)用后,子進(jìn)程退出,控制權(quán)還給父進(jìn)程。

  父進(jìn)程的當(dāng)前工作目錄沒有改變,因?yàn)槊钍窃谧舆M(jìn)程中執(zhí)行的。因此,cd 命令雖然成功了,但并沒有產(chǎn)生我們想要的結(jié)果。

  因此,要支持 cd,我們必須自己實(shí)現(xiàn)它。我們也需要確保,如果用戶輸入的命令是 cd(或?qū)儆陬A(yù)定義的內(nèi)建命令),我們根本不要 fork 進(jìn)程。相反地,我們將執(zhí)行我們對(duì) cd(或任何其它內(nèi)建命令)的實(shí)現(xiàn),并繼續(xù)等待用戶的下一次輸入。,幸運(yùn)的是我們可以利用 chdir 函數(shù)調(diào)用,它用起來很簡單。它接受路徑作為參數(shù),如果成功則返回0,失敗則返回 -1。我們定義函數(shù):

  int cd(char *path) {

  return chdir(path);

  }

  并且在我們的主函數(shù)中為它加入一個(gè)檢查:

  while (1) {

  input = readline("unixsh> ");

  command = get_input(input);

  if (strcmp(command[0], "cd") == 0) {

  if (cd(command[1]) < 0) {

  perror(command[1]);

  }

  /* Skip the fork */

  continue;

  }

  ...

  帶有以上更改的代碼可從這里獲取,如果你編譯并執(zhí)行它,你將能運(yùn)行 cd 命令。這里是一個(gè)示例輸出:

  unixsh> pwd

  /Users/dhanush/github.com/indradhanush.github.io/code/shell-part-2

  unixsh> cd /

  unixsh> pwd

  /

  unixsh>

  第二部分到此結(jié)束。這篇博客帖文中的所有代碼示例可在這里獲取。在下一篇博客帖文中,我們將探討信號(hào)的主題以及實(shí)現(xiàn)對(duì)用戶中斷(Ctrl-C)的處理。敬請(qǐng)期待。


【如何用C語言寫一個(gè)簡單的Unix Shell】相關(guān)文章:

怎么寫一個(gè)簡單的c語言程序06-24

分析C語言一個(gè)簡單程序07-07

C語言的HashTable簡單實(shí)現(xiàn)10-12

如何用Linux操作系統(tǒng)批量建立用戶的shell08-04

C語言入門教程:分析第一個(gè)C語言程序09-23

C語言怎么輸出一個(gè)菱形09-27

C語言的第一個(gè)程序08-20

C語言知識(shí)總結(jié)及其簡單應(yīng)用08-23

Linux Shell腳本教程(一):Shell入門09-01