在上篇講解了如何創建和調用進程
c 進程和系統調用
這篇文章就專門講講進程通信的問題
先來看一段下邊的代碼,這段代碼的作用是根據關鍵字調用一個Python程序來檢索RSS源,然后打開那個URL
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <stdlib.h> 4 #include <errno.h> 5 #include <string.h> 6 7 void error(char *msg) { 8 fprintf(stderr, "Error: %s %s", msg, strerror(errno)); 9 exit(1); 10 } 11 12 void open_url(char *url) { 13 char launch[255]; 14 // windows 15 sprintf(launch, "cmd /c start %s",url); 16 system(launch); 17 // linux 18 sprintf(launch, "x-www-browser ‘%s’ &",url); 19 system(launch); 20 // mac 21 sprintf(launch, "open '%s'",url); 22 system(launch); 23 } 24 int main(int argc, char * argv[]) { 25 26 // 取出要搜索的參數 27 char *phrase = argv[1]; 28 // 設置RSS源 29 char *vars[] = {"RSS_FEED=http://news.baidu.com/n?cmd=1&class=civilnews&tn=rss&sub=0",NULL}; 30 31 // 開啟一條管道,用於連接下邊的父進程和子進程 32 int fd[2]; 33 if (pipe(fd) == -1) { 34 error("Can not create a pipe"); 35 } 36 37 // 開啟一個進程 38 pid_t pid = fork(); 39 if (pid == -1) { 40 error("Can not fork process"); 41 } 42 43 44 // 接下來讓子進程去源內查找結果 pid==0 為子進程 45 if (pid == 0) { 46 47 // 因為子進程不需要使用管道的輸出端,關閉它 48 close(fd[0]); 49 // 把標准輸出設置為管道的輸入端,之所以這么設置,是因為子集成當查詢到數據的時候就會調用標准輸出函數,然后把數據流入管道中 50 dup2(fd[1], 1); 51 52 if (execle("/usr/bin/python", "/usr/bin/python","./rssgossip.py","-u",phrase,NULL,vars) == -1) { 53 error("Can't run script"); 54 } 55 } 56 57 // 上邊的if中的代碼是子進程的代碼,下邊的代碼是父進程的 58 // 父進程中不使用管道的寫入端,關閉 59 close(fd[1]); 60 // 把標准輸入 定向為管道的讀取端 61 dup2(fd[0], 0); 62 63 char line[255]; 64 65 // 以\t 開頭的就是url 66 while (fgets(line, 255, stdin)) { 67 if (line[0] == '\t') { 68 open_url(line+1); 69 FILE *file = fopen("te.txt", "w"); 70 fclose(file); 71 } 72 } 73 74 75 return 0; 76 }
我們先看看進程內部是什么樣子的
進程含有它內部運行的程序,還有棧和堆的數據空間。除此之外,它還要記錄數據流的連向,比如標准輸出連接到哪里。進程用文件描述符來表示數據流,所謂的描述符其實就是一個數字,進程會把文件描述符和對應的數據流保存在描述符表中,就像下邊的這張圖一樣
文件描述表的一列是文件描述符號,另外一列是他們對應的數據流。雖然名字叫文件描述符,但他們不一定連接硬盤上的某個文件,也有可能連接鍵盤,屏幕,文件指針,網絡等等。
描述符表的前三項萬年不變:0代表標准輸入 ,1代表標准輸出 ,2代表標准錯誤,其他項要么為空,要么連接進程打開的數據流。比如程序在打開文件進行讀寫的時候,就會打開一項。
每當創建進程后,默認的0(標准輸入)指向鍵盤,1(標准輸出)和2(標准錯誤)指向屏幕。
那么問題來了,前三項不是萬年不變的嗎?那么我應該怎么控制數據流呢?
其實,0/1/2在描述符表中的位置雖然是不可變的,但是他們指向的數據流確實可以改變的。
舉個簡單的例子,我想打開一個文件,進程是怎么做呢?
首先我們先打開一個文件
FILE *file = fopen("quitar.mp3", "r");
當系統執行完上邊這行代碼的時候,系統會打開quitar.mp3這個文件,並且會返回一個指向這個文件的指針,系統還會表里描述符表的空項,並把新文件注冊在其中
但是當我們做了很多操作,描述符表中有很多項的時候,我們如何才能找到我們想要的那一項呢 ?
答案是fileno() 函數,成功會返回文件指針的文件描述符,錯誤了不會返回-1,只要你把打開文件的指針傳給了它,就一定會返回描述符。
int desc = fileno(file);
我們現在已經能夠通過fileno()函數拿到描述符了,那我們應該如何修改該項的數據流呢?
答案是dup2() 函數。他會復制當前描述符的數據流到制定的描述符的數據流。
dup2(4,3);
好了,我們已經知道如何改變數據流了,完全可以把標准輸出指向一個文件,然后把數據寫入這個文件中。
只知道這些還不夠,有點開發經驗的人都知道進程和進程之間執行任務所需要的時間是不一樣的,往往需要等待一個進程完成后再進行下邊的任務,比如父進程和子進程,父進程需要等待子進程完成后再繼續,這個該怎么辦呢?
答案就是waitpid()函數。它會等待子進程結束后才返回。
現在終於說到重點內容了,進程之間傳遞數據靠的就是管道,在進程之間創建一條管道,
比如我們要把子進程的數據傳給父進程。
使用pipe()函數打開兩條數據流。
因為子進程需要把數據發送到父進程,所以要用管道連接子進程的標准輸出和父進程的標准輸入。你將用pipe()函數建立管道,還記得嗎?我們說過,每當打開數據流的時候,它都會加入描述符表中,pipe()函數也是如此,它創建兩條相連的數據流,並把他們加入到表中,然后你只需要往其中一條數據流中寫數據,就能從另一條數據流中讀取。
pipe()在描述符表中創建這兩項時,會把他們的文件描述符保存在一個包含兩個元素的數組中:
好了,本片文章中開頭給出的代碼所使用到的知識點都已經講解清楚了,原理大概就是這樣的,
我們已經知道了進程是怎么一回事?知道了如何在進程中利用管道做一些事情了,然而,這些還不夠,雖然我們知道了如何創建進程,配置環境,進程間通信,但是進程是怎么結束的呢?
我們帶着這個小小的疑問繼續往下看
加入我們寫了一個從鍵盤數據的小程序,像這樣
1 #include <stdio.h> 2 int main () { 3 4 char name[30]; 5 printf("請輸入一個名字:"); 6 fgets(name, 30, stdin); 7 printf("你輸入的名字是: %s",name); 8 return 0; 9 }
當我們按下Ctrl+C 的時候程序就結束了,也就是進程就結束了。但這其中到底發生了什么呢?
printf("你輸入的名字是: %s",name);
printf並沒有調用,是fgets調用了exit()嗎?
其實這涉及到了操作系統是如何控制程序的問題
當調用fgets函數時,操作系統會從鍵盤讀取數據,當它發現用戶按了Ctrl-C 后就會向程序發送中斷信號,就像這樣:
信號是一個短消息,也就是一個整型。當信號到來時,進程必須停止手中一切工作來處理信號,進程會查看信號映射表,表中每一個信號都對應着一個信號處理函數,中斷信號的默認處理函數就是exit()函數。
那么系統為什么不直接結束程序呢?而是在信號表中查找處理函數?就是為了讓我們可以自定義信號處理函數。
使用sigaction函數
sigaction函數是一個函數包裝器,本質上是一個結構體
/* * Signal vector "template" used in sigaction call. */ struct sigaction { union __sigaction_u __sigaction_u; /* signal handler */ sigset_t sa_mask; /* signal mask to apply */ int sa_flags; /* see signal options below */ };
它有一個函數指針 __sigaction_u
/* union for signal handlers */ union __sigaction_u { void (*__sa_handler)(int); void (*__sa_sigaction)(int, struct __siginfo *, void *); };
但是在平時開發中一般這么使用
/* if SA_SIGINFO is set, sa_sigaction is to be used instead of sa_handler. */ #define sa_handler __sigaction_u.__sa_handler #define sa_sigaction __sigaction_u.__sa_sigaction
sigaction告訴操作系統收到信號時應該調用哪個函數,加入我們想在收到中斷信號的時候調用我們自定義的my_custom_fun(),就要把我們自定義的這個函數包裝成sigaction。
// 創建一個新動作 struct sigaction action; // 想讓計算機調用哪個函數,這個被包裝的my_custom_fun函數就叫做處理器 action.sa_handler = my_custom_fun; // 使用掩碼過濾信號,通常會用一個空的掩碼 sigemptyset(&action.sa_mask); // 一些附加的標志位,置為0就行了 action.sa_flags = 0;
當然這個被包裝的函數也需要以特定的方式創建,這個函數我們下邊就稱之為處理器了,處理器必須接受信號參數,信號是一個整形值,如果你自定義一個信號處理函數,就需要接受一個整型參數,像這樣:
void my_custom_fun(int sig) { exit(1); }
由於我們以參數的形式傳遞信號,所以多個信號可以共用一個處理器,也可以每個信號寫一個處理器。
要點: 處理器的代碼應該短而快,剛好能處理接受到的信號就好。
那么系統怎么知道我們偷偷的更換了處理器呢?
因此要使用sigaction() 函數來注冊sigaction,讓系統知道它的存在
sigaction(signal_no, &new_action, &old_action);
我們來看看這個函數的樣子:
int sigaction(int, const struct sigaction * __restrict, struct sigaction * __restrict);
它幾首3個參數:
1. 信號編號,這個整型值代表了你希望處理的信號,通常會傳遞類似SIGINT/SIGQUIT這樣的標准信號。
2. 新動作,你想注冊的新sigaction的地址
3. 舊動作, 如果你想保存被替換的信號處理器,可以再傳一個sigaction指針,如果不想保存,可以傳一個NULL。
注意:如果sigaction() 的函數失敗,會返回-1,並設置errno變量。
當然,為了能夠快速的使用這些功能,我們可以把創建這個sigaction的過程封裝起來,我們只需要告訴函數需要捕捉的信號是什么,設置的處理器是什么就夠了。
我們寫了下邊的函數:
int catch_signal(int sig, void (*handler)(int)) { // 創建一個新動作 struct sigaction action; // 想讓計算機調用哪個函數,這個被包裝的my_custom_fun函數就叫做處理器 action.sa_handler = handler; // 使用掩碼過濾信號,通常會用一個空的掩碼 sigemptyset(&action.sa_mask); // 一些附加的標志位,置為0就行了 action.sa_flags = 0; return sigaction(sig, &action, NULL); }
我們先用一個簡單的例子來演示下上邊講到的sigaction,代碼如下
1 #include <stdio.h> 2 #include <signal.h> 3 #include <stdlib.h> 4 5 int catch_signal(int sig, void (*handler)(int)) { 6 // 創建一個新動作 7 struct sigaction action; 8 // 想讓計算機調用哪個函數,這個被包裝的my_custom_fun函數就叫做處理器 9 action.sa_handler = handler; 10 // 使用掩碼過濾信號,通常會用一個空的掩碼 11 sigemptyset(&action.sa_mask); 12 // 一些附加的標志位,置為0就行了 13 action.sa_flags = 0; 14 15 return sigaction(sig, &action, NULL); 16 } 17 18 void my_custom_fun(int sig) { 19 printf("一切都結束了"); 20 exit(1); 21 } 22 23 int main () { 24 25 if (catch_signal(SIGINT, my_custom_fun) == -1) { 26 fprintf(stderr, "替換不成功"); 27 exit(2); 28 } 29 30 char name[30]; 31 printf("請輸入一個名字:"); 32 fgets(name, 30, stdin); 33 printf("你輸入的名字是: %s",name); 34 return 0; 35 }
運行程序,當我Ctrl-C 的時候,結果如下
請輸入一個名字:^C一切都結束了bogon:02- machao$
SIGINT | 進程被中斷 |
SIGQUIT | 有人要求停止進程,並把存儲器中的內容保存到核心轉存文件中 |
SIGFPE | 浮點錯誤 |
SIGTRAP | 調試人員詢問進程執行到了哪里 |
SIGSEGV | 進程試圖訪問非法存儲器地址 |
SIGWINCH | 終端窗口的大小發生變化 |
SIGTERM | 有人要求系統的內核終止進程 |
SIGPIPE | 進程在向一個沒有人讀的管道寫數據 |
那么問題又來了,我們應該如何發送系統信號呢?
最后我們使用一個稍微復雜點的例子來掩飾上邊講的內容
說明:
SIGALRM 是一個定時器信號,使用alarm()函數可以設置一個定時器,設置一個時間,當時間結束的時候,會發出SIGALRM信號,如果在定時器時間還未結束的情況下,再次調用了alarm()函數,定時器將重新計時
這個程序測試用戶的數學水平,要求用戶做乘法,程序的條件如下:
1.用戶Ctrl-C
2.回答時間超過5秒
程序在結束時會顯示總得分,並把退出狀態設為0
代碼如下
1 #include <stdio.h> 2 #include <signal.h> 3 #include <stdlib.h> 4 #include <string.h> 5 #include <errno.h> 6 #include <time.h> 7 #include <unistd.h> 8 9 int score = 0; 10 11 int catch_signal(int sig, void (*handler)(int)) { 12 // 創建一個新動作 13 struct sigaction action; 14 // 想讓計算機調用哪個函數,這個被包裝的my_custom_fun函數就叫做處理器 15 action.sa_handler = handler; 16 // 使用掩碼過濾信號,通常會用一個空的掩碼 17 sigemptyset(&action.sa_mask); 18 // 一些附加的標志位,置為0就行了 19 action.sa_flags = 0; 20 21 return sigaction(sig, &action, NULL); 22 } 23 24 // 結束游戲的處理器 25 void end_game(int sig) { 26 printf("\n 總得分: %i \n", score); 27 exit(0); 28 } 29 30 // 時間到了的處理器 31 void times_up(int sig) { 32 printf("\n 時間到了"); 33 // 當倒計時結束的時候,引發SIGINT信號,調用end_game函數 34 raise(SIGINT); 35 } 36 37 void error(char *msg) { 38 fprintf(stderr, "Error: %s %s", msg, strerror(errno)); 39 exit(1); 40 } 41 42 int main () { 43 44 // 中斷信號 45 if (catch_signal(SIGINT, end_game) == -1) { 46 fprintf(stderr, "結束不成功"); 47 exit(2); 48 } 49 // 定時信號 50 if (catch_signal(SIGALRM, times_up) == -1) { 51 fprintf(stderr, "鬧鍾不成功"); 52 exit(3); 53 } 54 // srandom函數利用一個時間因子產生一個不同的隊列給random函數調用,這樣random函數每次運行時就不會產生一樣的偽隨機輸出了 55 srandom (time (NULL)); 56 57 while (1) { 58 59 // 生成兩個 0 ~ 10 的隨機數 60 int a = random() % 11; 61 int b = random() % 11; 62 63 char txt[4]; 64 65 // 定時5秒 66 alarm(5); 67 68 printf("%i * %i = ?",a,b); 69 70 fgets(txt, 4, stdin); 71 72 int answer = atoi(txt); 73 74 if (answer == a * b) { 75 score++; 76 }else { 77 printf("錯誤!得分: %i \n",score); 78 } 79 } 80 81 return 0; 82 }
首先我們回答兩個問題,然后等待5秒倒計時結束 , 再次運行程序,我們Ctrl-C
運行結果為
好了,關於進程間的通信的內容,和進程的操作就寫到這里了