進程相關的概念 程式與進程 程式,是指編譯好的二進位文件,在磁碟上,不占用系統資源(CPU、記憶體、打開的文件、設備、鎖等等)。 進程,是一個抽象的概念,與操作系統原理聯繫緊密。進程是活躍的程式,占用系統資源。在記憶體中執行(程式運行起來,產生一個進程)。 程式 --> 劇本(紙),進程 -->戲(舞臺 ...
進程相關的概念
程式與進程
-
程式,是指編譯好的二進位文件,在磁碟上,不占用系統資源(CPU、記憶體、打開的文件、設備、鎖等等)。
-
進程,是一個抽象的概念,與操作系統原理聯繫緊密。進程是活躍的程式,占用系統資源。在記憶體中執行(程式運行起來,產生一個進程)。
-
程式 --> 劇本(紙),進程 -->戲(舞臺、演員、燈光、道具等等)。同一個劇本可以在多個舞臺同時上演。同樣,同個程式也可以載入為不同的進程(彼此之間互不影響)。如:同時開兩個終端。各自都有一個bash,但彼此ID不同。
併發
-
併發,在操作系統中,一個時間段中有多個進程都處於已啟動運行到運行完畢之間的狀態。但任一個時刻點上仍只有一個進程在運行。
-
例如,當下,我們使用電腦時可以邊聽音樂邊聊天上網。若籠統的將他們均看做一個進程的話,為什麼可以同時運行呢?因為併發。
-
分時復用CPU
單道程式設計
- 所有進程一個一個排隊執行。若A阻塞,B只能等待,即使CPU處於空閑狀態。而在人機交互時阻塞的出現是必然的。所有這種模型在系統資源利用上及其不合理,在電腦發展史上存在不久,大部分已被淘汰了。
多道程式設計
-
在電腦記憶體中同時存放幾道相互獨立的程式,它們在管理程式控制之下,相互穿插的運行。多道程式設計必須有硬體基礎作為保證。
-
時鐘中斷即為多道程式設計模型的理論基礎。併發時,任意進程在執行期間都不希望放棄CPU。因此系統需要一種強制讓進程讓出CPU資源的手段。時鐘中斷有硬體基礎作為保障,對進程而言不可抗拒。操作系統中的中斷處理函數,來負責調度程式執行。
-
在多道程式設計模型中,多個進程輪流使用CPU(分時復用CPU資源)。而當下常見CPU為納米級,1秒可以執行大約10億條指令。由於人眼的反應速度是毫秒級,所以看似同時在運行。
1s = 1000ms 1ms = 1000us 1us = 1000ms
-
實質上,併發是巨集觀並行,微觀串列! -- 推動了電腦蓬勃發展,將人類引入了多媒體時代。
CPU與MMU
- 記憶體管理單元MMU
進程式控制制塊PCB
-
我們知道,每個進程在內核中都有一個進程式控制制塊(PCB)來維護進程相關的信息,Linux內核的進程式控制制塊是task_struct結構體。
-
/usr/src/linux-headers-3.16.0-30/include/linux/sched.h文件中可以查看struct task_struct結構體定義。其內部成員有很多,我們重點掌握以下部分即可:
- 進程ID。系統中每個進程唯一的ID,在C語言中用pid_t類型表示,其實就是一個非負整數。
- 進程的狀態,有就緒、運行、掛起、停止等狀態。
- 進程切換時需要保存和恢復的一些CPU寄存器。
- 描述虛擬地址空間的信息。
- 描述控制終端的信息。
- 當前工作目錄(Current Working Directory)
- umask掩碼。
- 文件描述符,包含很多指向file結構體的指針。
- 和信號相關的信息。
- 用戶id和組id。
- 會話(Session)和進程組。
- 進程可以使用的資源上限(Resource Limit)。
進程狀態
- 進程基本的狀態有5種。分別為初始態,就緒態,運行態,掛起態與終止態。其中初始態為進程準備階段,常與就緒態結合來看。
環境變數
-
環境變數,是指在操作系統中用來指定操作系統運行環境的一些參數。通常具備以下特征:
- 1、字元串(本質)。
- 2、有統一的格式:名=值[:值]。
- 3、值用來描述進程環境信息。
- 存儲形式:與命令行參數類似。char*[]數組,數組名environ,內部存儲字元串,NULL作為哨兵結尾。
- 使用形式:與命令行參數類似。
- 載入位置:與命令行參數類似。位於用戶區,高於stack的起始位置。
- 引入環境變數表:須聲明環境變數。extern char **environ;
-
練習:列印當前進程的所有環境變數。
#include <stdio.h> extern char **environ; int main(int argc, char *argv[]) { int i; for(i = 0; environ[i]; i++) { printf("%s\n", environ[i]); } return 0; }
常見環境變數
-
按照慣例,環境變數字元串都是name=value這樣的形式,大多數name由大寫字母加下劃線組成,一般把name的部分叫做環境變數,value的部分則是環境變數的值。環境變數定義了進程的運行環境,一些比較重要的環境變數的含義如下:
-
PATH
-
可執行文件的搜索路徑。ls命令也是一個程式,執行它不需要提供完整的路徑名/bin/ls,然而通常我們執行當前目錄下的程式a.out卻需要提供完整的路徑名./a.out,這是因為PATH環境變數的值裡面包含了ls命令所在的目錄/bin,卻不包含a.out所在的目錄。PATH環境變數的值可以包含多個目錄,用:號隔開。在shell中用echo命令可以查看這個環境變數的值:
echo $PATH
-
-
SHELL
- 當前shell,它的值通常是/bin/bash。
-
TERM
- 當前終端類型,在圖形界面終端下它的值通常是xterm,終端類型決定了一些程式的輸出顯示方式,比如圖形界面終端可以顯示漢字,而字元終端一般不行。
-
LANG
- 語言和locale, 決定了字元編碼以及時間、貨幣等信息的顯示格式。
-
HOME
- 當前用戶主目錄的路徑,很多程式需要在主目錄下保存配置文件,使得每個用戶在運行該程式時都有自己的一套配置。
-
getenv函數
-
獲取環境變數
-
char *getenv(const char *name);
- 成功:返迴環境變數的值;
- 失敗:NULL(name)不存在。
-
練習:編程實現getenv函數。
setenv函數
-
設置環境變數的值
-
int setenv(const char *name, const char * value, int overwrite);
- 成功:0;
- 失敗:-1
- 參數overwrite取值:
- 1:覆蓋原環境變數
- 0:不覆蓋。(該參數常用於設置新環境變數,如:ABC=day-night)
unsetenv函數
-
刪除環境變數name的定義
-
int unsetenv(const char *name);
- 成功:0;
- 失敗:-1
- 註意事項:name不存在仍返回0(成功), 當name命名為"ABC="時則會出錯。
-
示例
#include <stdio.h> #include <stdlib.h> #include <string.h> int main(int argc, char * argv[]) { char * val; const char * name = "ABD"; val = getenv(name); printf("1, %s = %s\n", name, val);//ABD = NULL setenv(name, "efg", 1); val = getenv(name); printf("2, %s = %s\n", name, val);//ABD = efg int ret = unsetenv(name); printf("ret = %d \n", ret);//0 val = getenv(name); printf("3, %s = %s \n", name, val);//ABD = NULL return 0; }
進程式控制制
fork函數
- 創建一個子進程。
- pid_t fork(void);
- 失敗返回-1;
- 成功返回:1、父進程返回子進程的ID(非負);2、子進程返回0。
- pid_t類型表示進程ID,但為了表示-1, 它是有符號整形。(0不是有效進程ID,init最小為1)。
- 註意返回值,不是fork函數能返回兩個值,而是fork後,fork函數變為兩個,父子需各自返回一個。
-
示例
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(int argc, char *argv[]) { printf("father process exec begin..."); pid_t pid = fork(); if(pid == -1) { perror("fork error"); exit(1); } else if(pid == 0) { printf("I'm child, pid = %u, ppid = %u \n", getpid(), getppid()); } else { printf("I'm father, pid = %u, ppid = %u \n", getpid(), getppid()); sleep(1); } printf("father process exec end..."); return 0; }
-
迴圈創建n個子進程
- 一次fork函數調用可以創建一個子進程。那麼創建n個子進程應該怎麼實現呢?
- 簡單想,for(i = 0; i< n; i++){ fork() }即可。但這樣創建的是N個子進程嗎?
-
錯誤示例
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(int argc, char *argv[]) { printf("father process exec begin..."); pid_t pid; int i; for(i = 0; i < 5; i++) { pid_t pid = fork(); if(pid == -1) { perror("fork error"); exit(1); } else if(pid == 0) { printf("I'm %dth child, pid = %u, ppid = %u \n", i+1, getpid(), getppid()); } else { printf("I'm father, pid = %u, ppid = %u \n", getpid(), getppid()); sleep(1); } } printf("father process exec end..."); return 0; }
-
正確的調用方式
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(int argc, char *argv[]) { printf("father process exec begin..."); pid_t pid; int i; for(i = 0; i < 5; i++) { pid_t pid = fork(); if(pid == -1) { perror("fork error"); exit(1); } else if(pid == 0) { //不讓子進程現創建孫子進程 break; } } if(i<5) { sleep(i); printf("I'm %dth child, pid = %u, ppid = %u \n", i+1, getpid(), getppid()); } else { sleep(i); printf("I'm father"); } return 0; }
-
getpid函數
- 獲取進程ID。
pid_t getpid(void);
- 獲取進程ID。
-
getppid函數
- 獲取父進程ID。
pid_t getppid(void);
- 獲取父進程ID。
-
getuid函數
- 獲取當前進程實際用戶ID。
uid_t getuid(void);
- 獲取當前進程有效用戶ID。
uid_t geteuid(void);
- 獲取當前進程實際用戶ID。
-
getgid函數
- 獲取當前進程使用用戶組ID。
gid_t getgid(void);
- 獲取當前進程有效用戶組ID。
gid_t getegid(void);
- 獲取當前進程使用用戶組ID。
-
進程共用
- 父子進程之間在fork後,有哪些相同,那些想異之處呢?
- 父子相同處:全局變數、.data、.text、棧、堆、環境變數、用戶ID、宿主目錄、進程工作目錄、信號處理方式……
- 父子不同處:進程ID、fork返回值、父進程ID、進程運行時間、鬧鐘(定時器)、未決定信號集。
- 似乎,子進程複製了父進程0-3G用戶空間內容,以及父進程的PCB,但pid不同。真的每fork一個子進程都要將父進程的0-3G地址空間完全拷貝一份,然後在映射至物理記憶體嗎?
- 當然不是,父子進程間遵循讀時共用寫時複製的原則。這樣設計,無論子進程執行父進程的邏輯還是執行自己的邏輯都能節省記憶體開銷。
- 練習:編寫程式測試,父子進程是否共用全局變數。
- 重點註意!躲避父子進程共用全局變數的知識誤區!
- 重點:父子進程共用:1、文件描述符(打開文件的結構體)。2、mmap建立的映射區(進程間通信詳解)。
- 特別的,fork之後的父進程先執行還是子進程先執行不確定。取決於內核所使用的調度演算法。
- 父子進程之間在fork後,有哪些相同,那些想異之處呢?
-
gdb調試
- 使用gdb調試的時候,gdb只能跟蹤一個進程。可以在fork函數調用之前,通過指令設置gdb調試工具跟蹤父進程或者是跟蹤子進程。預設跟蹤父進程。
set follow-fork-mode child
命令設置gdb在fork之後跟蹤子進程。set follow-fork-mode parent
設置跟蹤父進程。- 註意:一定要在fork函數調用之前設置才有效。
exec函數族
-
fork創建子進程後執行的是和父進程相同的程式(但有可能執行不同的代碼分支), 子進程往往要調用一種exec函數以執行另一個程式。當進程調用一種exec函數時,該進程的用戶空間代碼和數據完全被新程式替換,從新程式的啟動常式開始執行。調用exec並不創建新進程,所以調用exec前後該進程的id並未改變。
-
將當前進程的.text、.data替換為所要載入的程式的.text、.data,然後讓進程從新的.text第一條指令開始執行,但進程ID不變,換核不換殼。
-
其實有六種以exec開頭的函數,統稱exec函數:
- int execl(const char *path, const char *arg, ...);
- int execlp(const char *file, const char *arg, ...);
- int execle(const char *path, const char *arg, ..., char * const envp[]);
- int execv(const char *path, char *const argv[]);
- int execvp(const char *file, char *const argv[]);
- int execve(const *path, char * const argv[], char *const envp[]);
execlp函數
-
載入一個進程,藉助PATH環境變數
-
int execlp(const char *file, const char *arg, ...); 成功:無返回;失敗:-1。
-
參數1:要載入的程式的名字。該函數需要配合PATH環境變數來使用,當PAHT中所有目錄搜索後沒有參數1則出錯返回。
-
該函數通常用來調用系統程式。如:ls、date、cp、cat等命令。
-
示例
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(int argc, char * argv[]) { pid_t pid; pid = fork(); if(pid == -1) { perror("fork error"); exit(1); } else if (pid > 0) { sleep(1); printf("parent"); } else { execlp("ls", "ls", "-l", "-a", NULL); } return 0; }
execl函數
-
載入一個進程,通過路徑+程式名來載入。
-
int execl(const char *path, const char *arg, ...);成功:無返回;失敗:-1
-
對比execlp, 如載入“ls”命令帶有-l,-F參數
execlp("ls", "ls", "-l", "-F", NULL); 使用程式名在PATH中搜索 execl("/bin/ls", "ls", "-l", "-F", NULL); 使用參數1給出的絕對路徑搜索
execvp函數
-
載入一個進程,使用自定義環境變數env。
-
int execvp(const char *file, const char *argv[]);
-
變參形式:1、... 2、argv[] (main 函數也是變參函數,形式上等同於int main(int argc, char *argv0, ...))
-
變參終止條件:1、NULL結尾;2、固參指定。
-
execvp與execlp參數形式不同,原理一致。
char *argv[] = {"ls", "-l", "-a", NULL}; execvp("ls", argv); execv("/bin/ls", argv);
-
練習:將當前系統中的進程信息,列印到文件中。
#include <unistd.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> int main(int argc, char *argv[]) { int fd; fd = open("ps.out", O_WRONLY | O_CREAT | O_TRUNC, 0644); if(fd < 0) { perror("open ps.out error"); exit(1); } dup2(fd, STDOUT_FILENO); execlp("ps", "ps", "aux", NULL);//執行成功,後面的語句不會執行 perror("execlp error"); exit(1); return 0; }
exec函數族一般規律
-
exec函數一旦調用成功即執行新的程式,不返回。只有失敗才返回,錯誤值-1。所以通常我們直接在exec函數調用後直接調用perror和exit(),無需if判斷。
l(list) 命令行參數列表。 p(path) 搜索file時使用path變數 v(vector) 使用命令行參數數組 e(environment) 使用環境變數數組,不使用進程原有的環境變數,設置新載入程式運行的環境變數。
-
事實上,只有execve是真正的系統調用,其它五個函數最終都是調用execve,所以execve在man手冊第2節,其它函數在man手冊第3節。這些函數之間的關係如下圖所示。
回收子進程
孤兒進程
-
孤兒進程:父進程先於子進程結束,則子進程成為孤兒進程,子進程的父進程成為init進程,稱為init進程領養孤兒進程。
-
示例,產生一個孤兒進程:
#include <stdio.h> #include <unistd.h> #include <sys/wait.h> int main(int argc, char * argv[]) { pid_t pid; pid = fork(); if(pid == 0) { while(1) { printf("I am child, my parent pid is %d\n", getppid()); sleep(1); } } else if(pid >0) { printf("I am parent, my pid is %d \n", getpid()); sleep(9); printf("----------parent going to die---------\n"); } else { perror("fork"); return 1; } return 0; }
僵屍進程
-
僵屍進程:進程終止,父進程尚未回收,子進程殘留資源(PCB)存放於內核中,變成僵屍(Zombie)進程。
-
特別註意:僵屍進程是不能使用kill命令清除掉的。因為kill命令只是用來終止進程的,而僵屍進程已經終止。
-
思考,用什麼辦法可清除僵屍進程呢?
-
示例,產生一個僵屍進程:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/wait.h> int main(int argc, char *argv[]) { pid_t pid; pid = fork(); if(pid == 0) { printf("---child, my parent=%d, going to sleep 10s \n", getppid()); sleep(10); printf("-------------child die--------------\n"); } else if(pid > 0) { while(1) { printf("I am parent, pid = %d, myson = %d \n", getpid(), pid); } } else { perror("fork error"); exit(1); } return 0; }
wait函數
-
一個進程在終止時會關閉所有文件描述符,釋放在用戶空間分配的記憶體,但它的PCB還保留著,內核在其中保存了一些信息:如果是正常終止則保存著退出狀態,如果是異常終止則保存著導致該進程終止的信號是哪個。這個進程的父進程可以調用wait或waitpid獲取這些信息,然後徹底清除掉這個進程。我們知道一個進程的退出狀態可以在shell中用特殊變數$?查看,因為Shell是它的父進程,當它終止時Shell調用wait或waitpid得到它的退出狀態,同時徹底清除掉這個進程。
-
父進程調用wait函數可以回收子進程終止信息。該函數有三個功能:
- 1、阻塞等待子進程退出。
- 2、回收子進程殘留資源。
- 3、獲取子進程結束狀態(退出原因)。
- pid_t wait(int *status);
- 成功:清理掉的子進程ID;
- 失敗:-1(沒有子進程)。
-
當進程終止時,操作系統的隱式回收機制會:
- 1、關閉所有文件描述符。
- 2、釋放用戶空間分配的記憶體。內核的PCB仍存在。其中保存該進程的退出狀態。(正常終止-->退出值班;異常終止-->終止信號)。
-
可使用wait函數傳出參數status來保存進程的退出狀態。藉助巨集函數來進一步判斷進程終止的具體原因。巨集函數可以分為如下三組:
1、WIFEXITED(status) 為非0 --> 進程正常結束 WEXITSTATUS(status) 如上巨集為真,使用此巨集 --> 獲取進程退出狀態(exit的參數) 2、WIFSIGNALED(status) 為非0 --> 進程異常結束 WTERMSIG(status) 如上巨集為真,使用此巨集 --> 取得使進程終止的那個信號的編號。 3、WIFSTOPPED(status) 為非0 --> 進程處於暫停狀態 WSTOPSIG(status) 如上巨集為真,使用此巨集 --> 取得使進程暫停的那個信號的編號。 WIFCONTINUED(status) 為真 --> 進程暫停後已經繼續運行。
-
示例
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/wait.h> int main(int argc, char *argv[]) { pid_t pid, wpid; int status; pid = fork(); if(pid == 0) { printf("---child, my parent=%d, going to sleep 10s \n", getppid()); sleep(30); printf("-------------child die--------------\n"); //exit(100); return 100; } else if(pid > 0) { wpid = wait(&status); if(wpid == -1) { perror("wait error"); exit(1); } if(WIFEXITED(status)) { printf("child exit with %d \n", WEXITSTATUS(status)); } if(WIFSIGNALED(status)) { printf("child killed by %d \n", WTERMSIG(status)); } while(1) { printf("I am parent, pid = %d, myson = %d \n", getpid(), pid); } } else { perror("fork error"); exit(1); } return 0; }
waitpid函數
-
作用同wait,但可指定pid進程清理,可以不阻塞。
-
pid_t waitpid(pid_t pid, int *status, int options);成功:返回清理掉的子進程ID;失敗:-1(無子進程)。
-
特殊參數和返回情況:
參數pid: >0 回收指定ID的子進程 -1 回收任意子進程(相當於wait) 0 回收和當前調用waitpid一個組的所有子進程 <-1 回收指定進程組內的任意子進程 參數status 參數options: 0 (wait)阻塞回收 WNOHANG 非阻塞回收(輪詢) 返回: 成功 pid 失敗 -1 0 參數3傳WNOHANG,並且子進程尚未結束
-
註意:一次wait或waitpid調用只能清理一個子進程,清理多個子進程應使用迴圈。
-
作業:父進程fork 3個子進程,三個子進程一個調用ps命令,一個調用自定義程式1(正常),一個調用自定義程式2(會出現錯誤)。父進程使用waitpid對其子進程進行回收。
-
示例
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/wait.h> int main(int argc, char *argv[]) { int n = 5, i;//預設創建五個子進程 pid_t p, q; pid_t wpid; if(argc == 2) { n = atoi(argv[1]); } for(i = 0; i < n; i++) {//出口1,父進程專用出口 p = fork(); if(p == 0) { break;//出口2,子進程出口,i不自增 } else if (i == 3) { q = p; } } if(n == i) { sleep(n); printf("I am parent, pid = %d\n", getpid(), getgid()); //waitpid(q, NULL, 0); //1、回收第三個子進程 //while(waitpid(-1, NULL, 0)); //2、等價於wait(NULl),阻塞回收任意子進程 do { //3、非阻塞回收任意子進程 //如果wpid == 0 說明子進程正在運行 wpid = waitpid(-1, NULL, WNOHANG); if(wpid > 0) { n--; } sleep(1); } while(n > 0) printf("wait finish\n"); } else { sleep(i); printf("I'm %dth child, pid = %d, gid = %d \n", i+1, getpid(), getgid()); } return 0; }
IPC(InterProcess Communication)進程間通信
- linux環境下,進程地址空間相互獨立,每個進程各自有不同的用戶地址空間。任何一個進程的全局變數在另一個進程中都看不到,所以進程和進程之間不能相互訪問,要交換數據必須通過內核,在內核中開闢一塊緩衝區,進程1把數據從用戶空間拷到內核緩衝沖區,進程2再從內核緩衝區把數據讀走,內核提供的這種機制稱為進程間通信(IPC)。
- 在進程間完成數據傳遞需要藉助操作系統提供特殊的方法,如:文件、管道、信號、共用記憶體、消息隊列、套接字、命名管道等。隨著電腦的蓬勃發展,一些方法由於自身設計缺陷被淘汰或者棄用。常用的進程間通信方式有:
- 管道(使用最簡單)
- 信號(開銷最小)
- 共用映射區(無血緣關係)
- 本地套接字(最穩定)
管道
管道的概念
-
管道是一種最基本的IPC機制,作用於有血緣關係的進程之間,完成數據傳遞。調用pipe系統函數即可創建一個管道。有如下特質:
- 1、其本質是一個偽文件(實為內核緩衝區)
- 2、由兩個文件描述符引用,一個表示讀端,一個表示寫端。
- 3、規定數據從管道的寫端流入管道,從讀端流出。
-
管理的原理:管道實為內核使用環形隊列機制,藉助內核緩衝區(4k)實現。
-
管道的局限性:
- 1、數據自己讀不能自己寫。
- 2、數據一旦被讀走,便不在管道中存在,不可反覆讀取。
- 3、由於管道採用半雙工通信方式。因此,數據只能在一個方向上流動。
- 4、只能在有公共祖先的進程間使用管道。
pipe函數
-
int pipe(int pipefd[2]);
-
參數
- fd[2] (傳出參數)
-
返回值
- 成功:0
- 失敗:-1,設置errno
管道的讀寫行為
-
讀管道
- 管道中有數據
- read返回實際讀到的位元組數
- 管道中無數據
- 寫端全關閉:read返回0
- 仍有寫端打開:阻塞等待
- 管道中有數據
-
寫管道
- 讀端全關閉
- 進程異常終止(SIGPIPE信號)
- 有讀端打開
- 管道未滿:寫數據,返回寫入位元組數
- 管道已滿:阻塞(少見)
- 讀端全關閉
-
父子進程間通信
ls | wc -l
#include <stdio.h> #include <unistd.h> #include <sys/wait.h> int main(void) { pid_t pid; int fd[2]; pipe(fd); pid = fork(); //子進程 if(pid == 0){ //子進程從管道中讀數據,關閉寫端 close(fd[1]); //讓wc從管道中讀取數據 dup2(fd[0], STDIN_FILENO); //wc命令預設從標準讀入取數據 execlp("wc", "wc", "-l", NULL); }else { //父進程向管道中寫數據,關閉讀端 close(fd[0]); //將ls的結果寫入管道中 dup2(fd[1], STDOUT_FILENO); //ls輸出結果預設對應屏幕 execlp("ls", "ls", NULL); } return 0; }
-
兄弟進程間通信
#include <stdio.h> #include <unistd.h> #include <sys/wait.h> int main(void) { pid_t pid; int fd[2], i; pipe(fd); for(i = 0; i < 2; i++){ if((pid = fork()) == 0){ break; } } if(i == 0){ //兄 close(fd[0]); //寫,關閉讀端 dup2(fd[1], STDOUT_FILENO); execlp("ls", "ls", NULL); }else if(i == 1){ //弟 close(fd[1]); //讀,關閉寫端 dup2(fd[0], STDIN_FILENO); execlp("wc", "wc", "-l", NULL); }else { close(fd[0]); close(fd[1]); for(i = 0; i < 2; i++){ //兩個兒子wait兩次 wait(NULL); } } return 0; }
管道緩衝區的大小
-
命令:
ulimit -a
-
函數:
fpathconf
, 參數2:__PC_PIPE_BUF
管道的優劣
-
優點:
- 實現手段簡單
-
缺點:
- 單向通信
- 只能有血緣關係進程間使用
FIFO
-
命名管道(Linux基礎文件類型)
-
創建
- 命令:
mkfifo
- 函數:
int mkfifo(const char *pathname, mode_t mode);
- 參數:
- name
- mode:8進位
- 返回值:
- 成功:0
- 失敗:-1,設置errno
- 參數:
- 命令:
-
無血緣關係進程間通信
- 使用同一FIFO
- 可多讀端,多寫端
共用存儲映射
文件進程間通信
-
使用文件也可以完成IPC,理論依據是,fork後,父子進程共用文件描述符。也就共用打開的文件。
-
練習:編程測試,父子進程共用打開的文件。藉助文件進行進程間通信。
-
思考:無血緣關係的進程可以打開同一個文件進行通信嗎?為什麼?
-
示例
/** *父子進程共用打開的文件描述符------使用文件完成進程間共用 */ #include <stdio.h> #include <unistd.h> #include <string.h> #include <stdlib.h> #include <fcntl.h> #include <sys/wait.h> int main(int argc, char *argv[]) { int fd1, fd2; pid_t pid; char * str = "----test for shared fd in parent child process----\n"; pid = fork(); if(pid < 0) { perror("fork error"); exit(1); } else if(pid == 0) { fd1 = open("test.txt", O_RDWR); if(fd1 < 0) { perror("open error"); exit(1); } //子進程寫入數據 write(fd1, str, strlen(str)); printf("child wrote over...\n"); } else { fd2 = open("test.txt", O_RDWR); if(fd2 < 0) { perror("open error"); exit(1); } sleep(1); //保證子進程寫入數據 //父進程讀取數據 int len = read(fd2, buf, sizeof(buf)); write(STDOUT_FILENO, buf, len); wait(NULL); } return 0; }
存儲映射I/O
-
存儲映射I/O(Memory-mmapped I/O)使一個磁碟文件與存儲空間中一個緩衝區相映射。於是當從緩衝區取數據,就相當於讀文件中的相應位元組。於此類似,將數據存入緩衝區,則相應的位元組就自動寫入文件。這樣,就可在不適用read和write函數的情況下,使用地址(指針)完成I/O操作。
-
使用這種方法,首先應通知內核,將一個指定文件映射到存儲區域中。這個映射工作可以通過mmap函數來實現。
-
mmap函數
-
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
-
返回:
- 成功:返回創建的映射區的首地址。
- 失敗:MAP_FAILED巨集。
-
參數:
- addr:建立映射區的首地址,由Linux內核指定。使用時,直接傳遞NULL。
- length:欲創建映射區的大小。
- prot:映射區許可權PROT_READ、PROT_WRITE、PROT_READ | PROT_WRITE。
- flags:標誌位參數(常用於設定更新物理區域、設置共用、創建匿名映射區)
- MAP_SHARED:會將映射區所做的操作反映到物理設備(磁碟)上。
- MAP_PRIVATE:映射區所做的修改不會反映到物理設備。
- fd:用來建立映射區的文件描述符。
- offset:映射文件的偏移(4k的整數倍)
-
示例
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <sys/mmap.h> void sys_err(char *str) { perror(str); exit(1); } int main(int argc, char *argv[]) { char *p = NULL; int fd = open("test.txt", O_CREAT|O_REWR, 0644); if(fd < 0) { sys_err("open error"); } int len = ftruncate(fd, 4); if(len == -1) { sys_err("ftruncate error"); } p = mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); if(p == MAP_FAILED) { sys_err("mmap error"); } strcpy(p, "abc"); //寫數據 int ret = munmap(p, 4); if(ret == -1) { sys_err("munmap error"); } close(fd); return 0; }
-
-
munmap函數
-
同malloc函數申請記憶體空間類似的,mmap建立的映射區在使用結束後也應調用類似free的函數來釋放。
-
int munmap(void *addr, size_t length);
- 成功:0;
- 失敗:-1。
-
借鑒malloc和free函數原型,嘗試封裝自定義smalloc,sfree來完成映射區的建立和釋放。思考函數應如何設計?
-
-
mmap註意事項
-
思考:
- 1、可以open的時候O_CREAT一個新文件來創建映射區嗎?(可以)
- 2、如果open時O_RDONLY,mmap時PROT參數指定PROT_READ|PROT_WRITE會怎樣?(許可權不足)
- 3、文件描述符先關閉,對mmap映射有沒有影響?(沒有影響)
- 4、如果偏移量為1000會怎樣?(不行,必須是4k的整數倍)
- 5、對mem越界操作會怎樣?(不能越界)
- 6、如果mem++,mmap可否成功?(不行)
- 7、mmap什麼情況下會調用失敗?(每個參數都有影響)
- 8、如果不檢測mmap的返回值,會怎樣?(會死得很難看)
-
總結:使用mmap時務必註意以下事項:
- 1、創建映射區的過程中,隱含著一次對映射文件的讀操作。
- 2、當MAP_SHARED時,要求:映射區的許可權<=文件打開的許可權(出於對映射區的保護)。而MAP_PRIVATE則無所謂,因為mmap中的許可權是對記憶體的限制。
- 3、映射區的釋放與文件關閉無關。只要映射建立成功,文件可以立即關閉。
- 4、特別註意,當映射文件大小為0時,不能創建映射區。所以:用於映射的文件必須要有實際大小!!mmap使用時常常會出現匯流排錯誤,通常是由於共用文件存儲空間大小引起的。
- 5、munmap傳入的地址一定是mmap的返回地址。堅決杜絕指針++、--操作。
- 6、如果文件偏移量必須為4k的整數倍。
- 7、mmap創建映射區出錯概率非常高,一定要檢查返回值,確保映射區建立成功再進行後續操作。
-
mmap父子進程通信
-
父子等有血緣關係的進程之間也可以通過mmap建立的映射區來完成數據通信。但相應的要在創建映射區的時候指定對應的標誌位參數flags:
- MAP_PRIVATE:(私有映射)父子進程各自獨占映射區。
- MAP_SHARED:(共用映射)父子進程共用映射區。
-
練習:父進程創建映射區,然後fork子進程,子進程修改映射區內容,然後,父進程讀取映射區內容,查驗是否共用。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <sys/mman.h> #include <sys/wait.h> int var = 100; int main(int argc, char *argv[]) { int *p; pid_t pid; int fd; fd = open("temp", O_RDWR|O_CREAT|O_TRUNC, 0644); if(fd < 0) { perror("open error"); exit(1); } unlink("temp"); //刪除臨時文件目錄項,使之具備被釋放條件 ftruncate(fd, 4); p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); //p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0); if(p == MAP_FAILED) //註意:不是p == NULL { perror("mmap error"); exit(1) } close(fd); //映射區建立完畢,即可關閉文件 pid = fork();//創建子進程 if(pid == 0) { *p = 2000; var = 1000; printf("child, *p = %d, var = %d\n", *p, var); } else { sleep(1); printf("parent, *p = %d, var = %d\n", *p, var); wait(NULL); } int ret = mnumap(p, 4);//釋放映射區 if(ret == -1) { perror("mnumap error"); exit(1); } return 0; }
-
結論:父子進程共用:
- 1、打開的文件
- 2、mmap建立的映射區(但必須要使用MAP_SHARED)
匿名映射
- 通過使用我們發現,使用映射區來完成文件讀寫操作十分方便,父子進程間通信也較容易。但缺陷是,每次創建映射區一定要依賴一個文件才能實現。通常為了建立映射區要open一個temp文件,創建好了再unlink、close掉,比較麻煩。可以直接使用匿名精映射來代替。其實Linux系統給我們提供了創建匿名映射區的方法,無需依賴一個文件即可創建映射區。同樣需要藉助標誌位參數flags來指定。
-
使用MAP_ANONYMOUS(或MAP_ANON),如:
int *p = mmap(NUll, 4, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0); "4"隨意舉例,該位置大小,可依實際需要填寫。
-
需要註意的是,MAP_ANONYMOUS和MAP_ANON這兩個巨集是Linux操作系統特有的巨集。在類Unix系統中如無該巨集定義,可使用如下兩步來完成匿名映射區的建立。
1、fd = open("/dev/zero", O_RDWR); 2、p = mmap(NULL, size, PROT_READ|PROT_WRITE, MMAP_SHARED, fd, 0);
-
示例
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <sys/mman.h> #include <sys/wait.h> int var = 100; int main(int argc, char *argv[]) { int *p; pid_t pid; p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANON, -1, 0); if(p == MAP_FAILED) //註意:不是p == NULL { perror("mmap error"); exit(1) } pid = fork();//創建子進程 if(pid == 0) { *p = 2000; var = 1000; printf("child, *p = %d, var = %d\n", *p, var); } else { sleep(1); printf("parent, *p = %d, var = %d\n", *p, var); wait(NULL); } int ret = mnumap(p, 4);//釋放映射區 if(ret == -1) { perror("mnumap error"); exit(1); } return 0; }
mmap無血緣關係進程間通信
-
實質上mmap是內核藉助文件幫我們創建了一個映射區,多個進程之間利用該映射區完成數據傳遞。由於內核空間多進程共用,因此無血緣關係的進程間也可以使用mmap來完成通信。只要設置相應的標誌位參數flags即可。若想實現共用,當然應該使用MAP_SHARED了。
-
示例
-
讀端
#include <stdio.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <stdlib.h> #include <sys/mman.h> #include <string.h> struct STU { int id; char name[20]; char sex; } void sys_err(char *str) { perror(str); exit(-1); } int main(int argc, char *argv[]) { int fd; struct STU student; struct STU *mm; if(argc < 2) { printf("./a.out file_shared\n"); exit(-1); } fd = open(argv[1], O_RDONLY); if(fd == -1) { sys_err("open error"); } mm = mmap(NULL, sizeof(student), PROT_READ, MAP_SHARED, fd, 0); if(mm == MAP_FAILED) { sys_err("mmap error"); } close(fd); while(1) { printf("id=%d\t name=%s\t %c\n", mm->id, mm->name, mm->sex); sleep(2); } munmap(mm, sizeof(student)); return 0; }
-
寫端
#include <stdio.h> #include <sys/stat.h> #include <sys/types.h> #include <fcntl.h> #include <unistd.h> #include <stdlib.h> #include <sys/mman.h> #include <string.h> struct STU { int id; char name[20]; char sex; } void sys_err(char *str) { perror(str); exit(-1); } int main(int argc, char *argv[]) { int fd; struct STU student = {10, "xiaoming", 'm'}; char *mm; if(argc < 2) { printf("./a.out file_shared\n"); exit(-1); } fd = open(argv[1], O_RDWR | O_CREAT, 0664); ftruncate(fd, sizeof(student)); mm = mmap(NULL, sizeof(student), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if(mm == MAP_FAILED) { sys_err("mmap error"); } close(fd); while(1) { memcpy(mm, &student, sizeof(student)); student.id++; sleep(1); } munmap(mm, sizeof(student)); return 0; }
-
階段綜合練習一
- 實現文件多進程拷貝。
- 假設有一個超大文件,需對其完成拷貝工作。為提高效率,可採用多進程並行拷貝的方法實現。假設文件大小為len,共有n個進程對該文件進行拷貝。那每個進程拷貝的位元組數應為len/n。但未必一定能整除,我們可以選擇最後一個進程負責剩餘部分拷貝工作。可使用len%(len/n)將剩餘部分大小求出。
- 為降低實現複雜度,可選用mmap來實現源、目標文件的映射,通過指針操作記憶體地址,設置每個進程拷貝的起始、結束位置。藉助MAP_SHARED選項將記憶體中所做的修改反映到物理磁碟上。
- 實現步驟:
- 1、指定創建子進程的個數
- 2、打開源文件
- 3、打開目的文件,不存在則創建
- 4、獲取文件大小
- 5、根據文件大小拓展目標文件
- 6、為源文件創建映射
- 7、為目標文件創建映射
- 8、求出每個子進程該拷貝的位元組數
- 9、創建N個子進程
- 10、子進程完成分塊拷貝(註意最後一個子進程拷貝起始位置)
- 11、釋放映射區
階段綜合練習二
- 實現簡單的互動式的Shell。
-
使用已學習的各種C函數實現一個簡單的互動式的Shell, 要求:
- 1、給出提示符,讓用戶輸入一行命令,識別程式名和參數並調用適當的exe函數執行程式,待執行完成後再次給出提示符。
- 2、該程式可識別和處理以下符號:
- 1)簡單的標準輸入輸出重定向:仿照例“父子進程ls | wc -l”,先dup2然後exec。
- 2)管理(|):Shell進程先調用pipe創建管道,然後fork出兩個子進程。一個子進程關閉讀端,調用dup2將寫端賦給標準輸出,另一個子進程關閉寫端,調用dup2把讀端賦給標準輸入,兩個子進程分別調用exec執行程式,而Shell進程把管道的兩端都關閉,調用wait等待兩個子進程終止。類似於“兄弟進程間ls | wc -l”練習的實現。
-
你的程式應該可以處理以下命令:
ls -l -R > file1 cat < file1 | wc -c > file1
-
實現步驟:
- 1、接收用戶輸入命令字元串,拆分命令及參數存儲。(自行設計數據存儲結構)
- 2、實現普通命令載入功能。
- 3、實現輸入、輸出重定向的功能。
- 4、實現管道。
- 5、支持多重管道。
階段綜合練習三
- 簡易本地聊天室
- 藉助IPC完成一個簡易的本地聊天功能。設有伺服器端和客戶端兩方。服務啟動監聽客戶端請求,並負責記錄處理客戶端登錄、聊天、退出等相關數據。客戶端完成登錄、發起聊天等操作。可以藉助伺服器轉發向某個指定客戶端完成數據包發送(聊天)。
- 客戶端向伺服器發送數據包,可採用如下協議格式來存儲客戶端數據,使用”協議號“區分客戶端請求和各種狀況。伺服器依據包號處理客戶端對應請求。
信號的概念
- 信號在我們的生活中隨處可見,如:古代戰爭中摔杯為號;現代戰爭中的信號彈;體育比賽中使用的信號槍......
- 他們都有共性:
- 1、簡單
- 2、不能攜帶大量信息
- 3、滿足某個特設條件才發送
- 信號是信息的載體,Linux/Unix環境下,古老、經典的通信方式,現下依然是主要的通信手段。
- Unix早期版本就提供了信號機制,但不可靠,信號可能丟失。Berkeley和AT&T都對信號模型做了更改,增加了可靠信號機制。但彼此不相容。POSIX.1對可靠信號常式進行了標準化。
信號的機制
- A給B發送信號,B收到信號之前執行自己的代碼,收到信號後,不管執行到程式的什麼位置,都要暫停運行,去處理信號,處理完畢再繼續執行。與硬體中斷類似--非同步模式。但信號是軟體層面上實現的中斷,早期常被稱為”軟中斷“。
- 信號的特質:由於信號是通過軟體方法實現,其實現手段導致信號很強的延時性。但對於用戶來說,這個延遲時間非常短,不易察覺。
- 每個進程收到的所有信號,都是由內核負責發送的,內核處理。
與信號相關的事件和狀態
- 產生信號
- 1、按鍵產生,如:ctrl+c、ctrl+z、ctrl+\
- 2、系統調用產生,如:kill、raise、abort
- 3、軟體條件產生,如:定時器alarm
- 4、硬體異常產生,如:非法訪問記憶體(段錯誤)、除0(浮點數例外)、記憶體對齊出錯(匯流排錯誤)
- 5、命令產生,如:kill命令
- 遞達:遞達並且到達進程。
- 未決:產生和遞達之間的狀態。主要由於阻塞(屏蔽)導致該狀態。
- 信號的處理方式:
- 1、執行預設動作
- 2、忽略(丟棄)
- 3、捕捉(調用戶處理函數)
- Linux內核的進程式控制制塊PCB是一個結構體,task_struct,除了包含進程id,狀態,工作目錄,用戶id,組id,文件描述符表,包含了信號相關的信息,主要指阻塞信號集和未決信號集。
- 阻塞信號集(信號屏蔽字):將某些信號加入集合,對他們設置屏蔽,當屏蔽x信號後,再收到該信號,該信號的處理將推後(解除屏蔽後)
- 未決信號集
- 1、信號產生,未決信號集中描述該信號的位立刻翻轉為1,表信號處於未決狀態。當信號被處理對應位翻轉回為0。這一時刻往往非常短暫。
- 2、信號產生後由於某些原因(主要是阻塞)不能抵達。這類信號的集合稱之為未決信號集。在屏蔽解除前,信號一直處於未決狀態。
信號的編號
-
可以使用
kill -l
命令來查看當前系統可使用的信號有哪些。1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR 31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8 43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2 63) SIGRTMAX-1 64) SIGRTMAX
-
不存在編號為0的信號。其中1-31號信號稱之為常規信號(也叫普通信號或標準信號),34-64稱之為實時信號,驅動編程與硬體相關。名字上區別不大。而前32個名字各不相同。
信號4要素
- 與變數三要素類似的,每個信號也有其必備4要素,分別是:
- 1、編號
- 2、名稱
- 3、事件
- 4、預設處理動作
-
可通過
man 7 signal
查看幫助文檔獲取。也可查看/usr/src/linux-headers-3.16.0-30/arch/s390/include/uapi/asm/signal.h
Signal Value Action Comment ────────────────────────────────────────────────────────────────────── SIGHUP 1 Term Hangup detected on controlling terminal or death of controlling process SIGINT 2 Term Interrupt from keyboard SIGQUIT 3 Core Quit from keyboard SIGILL 4 Core Illegal Instruction SIGABRT 6 Core Abort signal from abort(3) SIGFPE 8 Core Floating point exception SIGKILL 9 Term Kill signal SIGSEGV 11 Core Invalid memory reference SIGPIPE 13 Term Broken pipe: write to pipe with no readers SIGALRM 14 Term Timer signal from alarm(2) SIGTERM 15 Term Termination signal SIGUSR1 30,10,16 Term User-defined signal 1 SIGUSR2 31,12,17 Term User-defined signal 2 SIGCHLD 20,17,18 Ign Child stopped or terminated SIGCONT 19,18,25 Cont Continue if stopped SIGSTOP 17,19,23 Stop Stop process SIGTSTP 18,20,24 Stop Stop typed at terminal SIGTTIN 21,21,26 Stop Terminal input for background process SIGTTOU 22,22,27 Stop Terminal output for background process The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.
-
在標準信號中,有一些信號是有三個“Value”,第一個值通常對alpha和sparc架構有效,中間值針對x86、arm和其他架構,最後一個應用於mips架構。一個'-'表示在對應架構中尚未定義該信號。
- 不同的操作系統定義了不同的系統信號。因此有些信號出現在Unix系統內,也出現在Linux中,而有的信號出現在FreeBSD或Mac OS中,卻沒有出現在Linux中。這裡我們只研究Linux系統中的信號。
- 預設動作:
- Term:終止進程
- Ign:忽略信號(預設即時對該種信號忽略操作)
- Core:終止進程,生成Core文件。(查驗進程死亡原因,用於gdb調試)
- Stop:停止(暫停)進程
- Cont:繼續運行進程
- 註意
man 7 signal
幫助文檔中可看到:The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.
這裡特別強調了9) SIGKILL
和19) SIGSTOP
信號,不允許忽略和捕捉,只能執行預設動作。甚至不能將其設置為阻塞。 - 另外需清楚,只有每個信號所對應的事件發生了,該信號才會被遞達(但不一定遞達),不應亂髮信號!!
Linux常規信號一覽表
- 1)SIGHUP:當用戶退出Shell時,由該Shell啟動的所有進程將收到這個信號,預設動作為終止進程。
- 2)SIGINT:當用戶按下了
ctrl+c
組合鍵時,用戶終端向正在運行中的由該終端啟動的程式發出此信號。預設動作為終止進程。 - 3)SIGQUIT:當用戶按下
ctrl+\
組合鍵時產生該信號,用戶終端向正在運行中的由該終端啟動的程式發出此信號。預設動作為終止進程。 - 4)SIGILL:CPU檢測到某進程執行了非法指令。預設動作為終止進程並產生core文件。
- 5)SIGTRAP:該信號由斷點指令或其他trap指令產生。預設動作為終止進程並產生core文件。
- 6)SIGABRT:調用abort函數時產生該信號。預設動作為終止進程並產生core文件。
- 7)SIGBUS:非法訪問記憶體地址,包括記憶體對齊出錯,預設動作為終止進程並產生core文件。
- 8)SIGFPE:在發生致命的運算錯誤時發出。不僅包括浮點運算錯誤,還包括溢出及除數為0等所有的演算法錯誤。預設動作為終止進程並產生core文件。
- 9)SIGKILL:無條件終止進程。本信號不能被忽略,處理和阻塞。預設動作為終止進程。它向系統管理員提供了可以殺死任何進程的方法。
- 10)SIGUSR1:用戶定義的信號。即程式員可以在程式中定義並使用該信號。預設動作為終止進程。
- 11)SIGSEGV:指示進程進行了無效記憶體訪問。預設動作為終止進程並產生core文件。
- 12)SIGUSR2:另外一個用戶自定義信號,程式員可以在程式中定義並使用該信號。預設動作為終止進程。
- 13)SIGPIPE:Broken pipe向一個沒有讀端的管道寫數據。預設動作為終止進程。
- 14)SIGALRM:定時器超時,超時的時間由系統調用alarm設置。預設動作為終止進程。
- 15)SIGTERM:程式結束信號,與SIGKILL不同的是,該信號可以被阻塞和終止。通常用來表示程式正常退出。執行Shell命令kill時,預設產生這個信號。預設動作為終止進程。
- 16)SIGSTKFLT:Linux早期版本出現的信號,現仍保留向後相容。預設動作為終止進程。
- 17)SIGCHLD:子進程結束時,父進程會收到這個信號。預設動作為忽略這個信號。
- 18)SIGCONT:如果進程已停止,則使其繼續運行。預設動作為繼續或忽略。
- 19)SIGSTOP:停止進程的執行。信號不能被忽略、處理和阻塞。預設動作為暫停進程。
- 20)SIGTSTP:停止終端交互進程的運行。按下
ctrl+z
組合鍵時發出這個信號。預設動作為暫停進程。 - 21)SIGTTIN:後臺進程讀終端控制台。預設動作為暫停進程。
- 22)SIGTTOU:該信號類似於SIGTTIN,在後臺進程要向終端輸出數據時發生。預設動作為暫停進程。
- 23)SIGURG :套接字上有緊急數據時,向當前正在運行的進程發出信號,報告有緊急數據到達。如網路帶外數據到達,預設動作為忽略該信號。
- 24)SIGXCPU:進程執行時間超過了分配給該進程的CPU時間,系統產生該信號併發送給該進程。預設動作為終止進程。
- 25)SIGXFSZ:超過文件的最大長度設置。預設動作為終止進程。
- 26)SIGVTALRM:虛擬時鐘超時時產生該信號。類似於SIGALRM,但是該信號只計算該進程占用CPU的使用時間。預設動作為終止進程。
- 27)SIGPROF:類似於SIGVTALRM,它不包括該進程占用CPU時間還包括執行系統調用時間。預設動作為終止進程。
- 28)SIGWINCH:視窗變化大小時發出。預設動作為忽略該信號。
- 29)SIGIO:此信號向進程指示發出了一個非同步IO事件。預設動作為忽略。
- 30)SIGPWR:關機。預設動作為終止進程。
- 31)SIGSYS:無效的系統調用。預設動作為終止進程並產生core文件。
- 34)SIGRTMIN ~ 64)SIGRTMAX:Linux的實時信號,它們沒有固定的含義(可以由用戶自定義)。所有的實時信號的預設動作都為終止進程。
信號的產生
終端按鍵產生信號
ctrl+c 2) SIGINT(終止/中斷) "INT" -- Interrupt
ctrl+z 20)SIGTSTP(暫停/停止) "T" -- Terminal終端
ctrl+\ 3) SIGQUIT(退出)
硬體異常產生信號
除0操作 8) SIGFPE(浮點數例外) "F" -- float 浮點數
非法訪問記憶體 11)SIGSEGV(段錯誤)
匯流排錯誤 7) SIGBUS
kill函數/命令產生信號
- kill命令產生信號:
kill -SIGKILL pid
- kill函數:給指定進程發送指定信號(不一定殺死)
int kill(pid_t pid, int sig);
- 返回值
- 成功:0
- 失敗:-1(ID非法,信號非法,普通用戶殺init進程等權級問題),設置errno。
- 參數
- sig:不推薦直接使用數字,應使用巨集名,因為不同操作系統信號編號可能不同,但名稱一致。
- pid>0:發送信號給指定的進程。
- pid=0:發送信號與調用kill函數進程屬於同一進程組的所有進程。
- pid<0:取|pid|發給對應進程組。
- pid=-1:發送給進程有許可權發送的系統中所有進程。
- 返回值
- 進程組:每個進程都屬於一個進程組,進程組是一個或多個進程集合,他們相互關聯,共同完成一個實體任務,每個進程組都有一個進程組長,預設進程組ID與進程組長ID相同。
- 許可權保護:super用戶(root)可以發送信號給任意用戶,普通用戶是不能向系統用戶發送信號的。kill -9(root用戶的pid)是不可以的。同樣,普通用戶也不能向其他普通用戶發送信號,終止其進程。只能自己創建的進程發送信號。普通用戶基本規則是:發送者實際或有效用戶ID==接收者實際或有效用戶ID。
- 練習:迴圈創建5個子進程,任一子進程用kill函數終止其父進程。
-
示例
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <signal.h> #define N 5 int main(int argc, char *argv[]) { int i; pid_t pid; for(i = 0; i < N; i++) { pid = fork(); if(pid == 0) break; if(i == 2) q = pid; } if(i < N) { while(1) { printf("I am child %d, getpid() = %u \n", i+1, getpid()); sleep(1); } } else { sleep(1); kill(q, SIGKILL); while(1); } // int ret = kill(getpid(), SIGKILL); // if(ret == -1) // exit(1); return 0; }
raise和abort函數
- raise函數
- 給當前進程發送指定信號(自己給自己發)
raise(signo) == kill(getpid(), signo)
int raise(int sig);
- 成功:0
-
- 給當前進程發送指定信號(自己給自己發)