linux学习笔记 -- 系统编程
系統編程
- 相關概念
- 概念
- 簡易cpu結構
- mmu內存管理單元
- 環境變量
- PATH
- SHELL
- HOME
- LANG
- TERM
- getenv
- setenv
- unsetenv
- 進程控制
- fork函數
- getpid
- getppid
- getuid
- getgid
- 父子進程共享
- gdb調試
- exec
- execl
- execlp
- execle
- execv
- execvp
- execve
- 一般規律
- 回收子進程
- 孤兒進程
- 僵尸進程
- wait
- waitpid
- 2.IPC方法
- 管道
- 管道的概念
- pipe函數
- 管道的讀寫行為
- 管道緩沖區大小
- 管道的優勢
- FIFO
- 共享存儲映射(共享內存)
- 文件進程間通訊
- 存儲映射I/O
- mmap父子進程通訊
- 匿名映射
- mmap無血緣關系進程間通訊
- 3.信號
- 信號的事件和狀態
- 信號的產生
- 終端按鍵產生的信號
- 硬件異常產生信號
- kill產生
- raise和abort
- 軟件條件產生
- 信號的捕捉
- **signal函數:**
- **sigaction函數:**
- 信號捕捉特性
- 信號集操作函數
- 信號集設定
- sigprocmask
- sigpending函數
- 競態條件
- pause
- 時序競態
- 全局變量異步IO
- 可/不可重入函數
- SIGCHLD信號
- SIGCHLD的產生條件
- 借助SIGCHLD信號回收子進程
- 信號傳參
- 發送信號傳參
- 捕捉函數傳參
- 中斷系統調用
- 4.守護進程
- 終端
- 終端的啟動流程
- ttyname
- 網絡終端
- 進程組
- 進程操作函數
- 會話
- 創建會話
- 守護進程
- 創建守護進程模型
- 5.線程
- 線程的概念
- 什么是線程
- linux內核線程實現原理
- 線程共享資源
- 線程非共享資源
- 線程優缺點
- 線程控制原語
- pthread_self
- pthread_create
- pthread_exit
- pthread_join
- pthread_detach
- pthread_cancel
- pthread_equal
- 控制原語對比
- 線程屬性
- 線程屬性初始化
- 線程的分離狀態
- 線程的棧地址
- 線程的棧大小
- NPTL
- 線程注意事項
- 線程同步
- 同步概念
- 線程同步
- 數據混亂原因
- 互斥量mutex(互斥鎖)
- 主要應用函數
- 加鎖與解鎖
- 加鎖步驟測試
- 死鎖
- 讀寫鎖
- 讀寫鎖狀態
- 讀寫鎖特性
- 主要應用函數
- 讀寫鎖實例
- 條件變量
- 主要應用函數:
- 生產者消費者條件變量模型
- 條件變量的優點
- 信號量
- 主要應用函數
- 生產者消費者信號量模型
- 進程間同步
- 互斥量mutex
- 文件鎖
- 其他
- 文件存儲
- UNIX
- 優化
黑馬教程分享視頻學習筆記
相關概念
概念
程序:編輯好的二進制文件(.out);在磁盤上,不占用系統資源(cpu、內存、打開的文件、設備、鎖);進程:抽象的概念,與操作系統原理聯系緊密。進程是活躍的程序,占用系統資源。在內存中執行(程序運行起來,產生一個進程)同一個程序可以同時啟動兩個進程。并發:并行執行。 單道程序設計模式:不同任務之間排隊使用cpu(DOS) 多道程序設計模式:不同程序同時使用cpu;通過時鐘中斷,硬件手段在不同進程之間回收、分配cpu使用權力;簡易cpu結構
數據讀取時,硬盤到內存中,再到緩沖區中,然后進入cpu的寄存器中。cpu的預取器取出一條指令;譯碼器翻譯指令并存儲相關數據到寄存器中;然后交給ALU進行計算,操作寄存器堆,再放回緩存,返回到內存。
mmu內存管理單元
虛擬地址:可用的地址空間有4G。
mmu將虛擬內存地址與物理內存地址進行映射;設置修改內存訪問級別(內核空間和用戶空間),cpu可使用的級別有四種,linux只使用了最高和最低兩種。
PCB:進程描述符、進程控制塊。
進程之間彼此獨立,每運行一個程序,需要進行物理內存映射,開辟新的物理內存空間使用。但不同進程之間的映射的物理內存是同一塊的,由mmu實現不同進程的PCB描述數據不同。
進程控制塊PCB:
? 在linux內核的進程控制塊是task_struct結構體。查找結構體命令:grep -r "task_struct {" /usr/
? 存在于/usr/src/linux-headers-3.16.0-30/include/linux/sched.h,常用成員如下:
1. 進程ID。系統中每個進程有唯一的id,在c語言中用pid_t類型表示,非負整數 2. 進程狀態。就緒(包括初始化,等待cpu分配時間片)、運行(占用cpu)、掛起(等待除cpu以外的其他資源 主動放棄cpu)、停止 3. 進程切換時需要保存和恢復的一些cpu寄存器 4. 描述虛擬地址空間的信息 5. 描述控制終端的信息 6. 當前工作目錄 7. umask掩碼 8. 文件描述符表,包括很多指向file結構體的指針。 9. 和信號相關的信息。 10. 用戶id和組id 11. 會話和進程組。 12. 進程可以使用的資源上限。``ulimit -a`` linux系統中查看資源上下限環境變量
linux 是多任務、多用戶的開源操作系統。
環境變量,是指在操作系統中用來指定操作系統運行環境的一些參數。具備一下特征:
存儲形式:與命令行參數類似。char* []數組,數組名environ,內部存儲字符串,NULL作為哨兵結尾。
使用形式:與命令行參數類似。
加載位置:與命令行參數類似。位于用戶區,高于stack的起始位置。
引入環境變量表:需聲明環境變量。extern char** environ;
#include<stdio.h>extern char **environ;int main(void) {int i;for(i = 0; environ[i]; i++){printf("%s\n",environ[i]);}return 0; }PATH
? 可執行文件的搜索路徑。ls命令也是一個程序,執行他不需要提供完整的路徑名稱/bin/ls,但是通常我們執行當前目錄下的程序a.out確需要提供完整的路徑名/a.out,這是因為PATH環境變量里的值包含了ls命令所在的目錄/bin,卻不包含a.out所在的目錄。PATH環境變量的值可以包含多個目錄,用:號隔開,使用時,shell會按照環境變量順序,從前到后檢索對應路徑下是否有可用的應用程序,知道最后或找到。在shell中用echo命令可以查看這個環境變量的值:echo $PATH
SHELL
? 當前Shell,他通常是/bin/bash,當前命令解析器。
HOME
? 當前的家目錄
LANG
? 當前的預言,執行echo $LANG后可以看到為 zh_CN.UTF-8.
TERM
? 當前終端類型,在圖形界面終端下它的值通常是xterm,終端類型決定了一些程序的顯示方式,比如圖形界面終端可以顯示漢字,而字符終端一般不行。
getenv
獲取環境變量值
char *getenv(const char * name);成功:返回環境變量的值;失敗:NULL(name不存在)
setenv
設置環境變量的值
int setenv(const char *name, const char *value, int overwrite);成功:0;失敗-1.
參數overwrite取值:1. 覆蓋原環境變量;0. 不覆蓋(常用于設置新環境變量)
unsetenv
刪除環境變量name的定義
int unsetenv(const char * name);成功:0;失敗:-1
注意: name不存在仍返回0,當name命名為“ABC=”時會出錯。
#include<stdio.h> #include<stdlib.h> #include<string.h>int main(void) {char *val;const char *name = "wxf";val = getenv(name);printf("1 %s=%s\n",name,val);int ret = setenv(name, "hallo world", 1);val = getenv(name);printf("2 %s=%s\n",name,val);#if 0ret = unsetenv("wxf=");printf("3 ret = %d\n",ret);val = getenv(name);printf("4 %s=%s\n",name,val); #elseret = unsetenv(name);printf("3 ret = %d\n",ret);val = getenv(name);printf("4 %s=%s\n",name,val); #endifreturn 0; }進程控制
fork函數
? 創建一個子進程。
? pid_t fork(void); 失敗返回-1;成功返回:父進程返回子進程的ID(非負)、子進程返回0 。
? pid_t 類型表示進程ID,但為了表示-1,他是有符號整形。(0不是有效的進程ID,init最小,為1)
? 注意返回值,不是fork函數能返回兩個值,而是fork后,fork函數變成兩個,父子需各自返回一個。fork函數執行完成后,創建了子進程,父、子進程同時繼續執行fork函數后面的代碼。
#include<stdio.h> #include<unistd.h> #include<stdlib.h>int main(void) {printf("begin 88888888888888888888\n");pid_t pid;pid = fork();if(pid == -1){perror("fork");exit(1);}printf("pid = %d \n",pid);if(pid == 0){printf("child process,pid = %u,ppid=%u\n",getpid(),getppid());}else{printf("parent process,pid = %u,ppid=%u\n",getpid(),getppid());}printf("end 88888888888888888888\n");return 0; }循環創建n個子進程
? 一次fork函數調用可以創建一個子進程。那么創建N個子進程應該如何實現呢,簡單想,``for(int i=0;i<n;i++)){fork()}即可,但是是這樣嗎。
#include<stdio.h> #include<unistd.h> #include<stdlib.h>int main(void) {printf("begin 88888888888888888888\n");pid_t pid;int times = 5;int i = 0;for ( i = 0; i < times; i++){pid = fork();if(pid == -1){perror("fork");exit(1);}if(pid == 0){printf("child %d process,pid = %u,ppid=%u\n",i+1, getpid(),getppid());break;//子進程跳出循環,不再產生孫進程}}if(i < 5){sleep(i);//為了保證輸入的先后順序printf("end child %d pid = %u\n",i+1,getpid()); }else{sleep(i);printf("end 88888888888888888888\n");}return 0; }? 子進程產生后與父進程同時搶奪cpu使用時間,(tips:沒有理論依據,但父進程獲取cpu的幾率大些)
? 去除sleep后打印混亂。執行可執行命令的shell進程(爺爺進程)在父進程return后輸出控制臺,而子進程可能還沒結束,將繼續打印。
getpid
返回當前進程IDpid_t getpid();
getppid
返回當前進程父進程IDpid_t getppid();
getuid
獲取當前進程實際用戶ID:uid_t getuid(void);
對應 獲取當前進程有效用戶ID:uid_t geteuid(void);
getgid
獲取當前進程實際用戶組ID:gid_t getgid(void);
對應 獲取當前進程有效用戶組ID:gid_t getegid(void);
父子進程共享
剛fork之后(后續執行代碼后按照各自進程處理):
? 父子相同處:全局變量、.data、.text、棧、堆、環境變量、用戶ID、宿主目錄、進程工作目錄、信號處理方式等。
? 父子不同處:
1. 進程ID2. fork返回值3. 父進程ID4. 進程運行時間5. 鬧鐘(定時器)6. 未決信號集? 父子進程間遵循讀時共享,寫時復制的原則。無論子進程執行父進程的邏輯還是執行自己的邏輯都能節省內存開銷。
注意:父子進程共享:
gdb調試
編譯時需要加-g:gcc fork.c -g
進入調試:gbd a.out
? 使用gdb調試的時候,只能跟蹤一個進程,可以在fork之前,通過指令設置gdb調試工具跟蹤父進程或者子進程。默認跟蹤父進程。
? set follow-fork-mode child 命令設置gdb在fork之后跟蹤子進程
? set follow-fork-mode parent 命令設置gdb在fork之后跟蹤父進程
exec
? fork創建子進程后執行的是和父進程相同的程序(但有可能執行不同的代碼分支),子進程往往需要調用一種exec函數以執行另一個程序。當進程調用一種exec函數時,該進程的用戶空間代碼和數據完全被新程序替換,從新程序的啟動例程(main)開始執行。調用exec并不創建新進程,所以調用exec前后該進程的ID并未改變。
? 將當前進程的.text、.data替換為所要加載的程序的.text、.data,然后讓進程從新的.text第一條指令開始執行,但進程ID不變,換核不換殼。
? 其實有六種以exec開頭的函數,統稱為exec函數。
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給出的絕對路徑搜索
也可以執行自己的程序,如當前目錄下的fork:execl("./fork", "fork", NULL);
execlp
int execlp(const char *file, const char * arg,...);
list,path
參數:file 可執行程序文件名;arg命令行參數,注意第一個arg相當于argv[0],相當于ls -l中的ls,一般而言,可執行程序可能不會讀取argv[0],所以argv[0],只起到站位的作用,只要后續參數不錯就行。另外需要以NULL結尾。
該函數需要配合PATH環境變量來使用,當PATH中所有目錄搜索后沒有參數1則出錯返回。
該函數通常用來調用系統程序。如:ls、date、cp、cat等命令。
#include<stdio.h> #include<stdlib.h> #include<unistd.h>int main() {pid_t pid;pid = fork();if(pid == -1){perror("fork");exit(1);}if(pid > 0){printf("parent process\n");sleep(1);}else{execlp("ls", "ls", "-l", "-a", NULL);}return 0; }只有發生錯誤的時候,函數才會有返回值-1,成功時不會有返回值。
execle
int execle(const char * path, const char * arg, ... ,char * const envp[]);
需要引入環境變量。
execv
int execv(const char * path,char * const argv[]);
char *argv[] = {"ls", "-l","-a",NULL}; execv("/bin/ls",argv);execvp
int execvp(const char * file, char * const argv[]);
execve
int execve(const char * path,char * const argv[], char *const envp[]);
將所有進程保存在文件中
#include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<fcntl.h>int main() {int fd;fd = open("ps.out", O_WRONLY | O_CREAT | O_TRUNC, 0644);if(fd < 0){perror("open");exit(1);}dup2(fd, STDOUT_FILENO);execlp("ps", "ps", "ax", NULL);return 0; }一般規律
? exec函數一旦調用成功立即執行新的程序,不返回。只有失敗才返回,錯誤值-1。所以通常我們直接在exec函數調用后直接調用perror()和exit(),無需if判斷。
l(list) 命令行參數列表
p(path) 搜索file時使用path變量
v(vector) 使用命令行參數數組
e(environment) 使用環境變量數組,不適用進程原有的環境變量, 設置新加載程序運行的環境變量。
? 事實上,只有execve是真正的系統調用,其他五個函數最終都調用execve,所以execve在man手冊第二節,其他函數在第三節,關系如下圖。
回收子進程
孤兒進程
? 父進程先于子進程結束,則子進程成為孤兒進程,子進程的父進程成為init進程,稱為init進程領養孤兒進程。
僵尸進程
? 進程終止,父進程尚未回收,子進程殘留資源(PCB)存放在內核中,變成僵尸進程。
? 注意:僵尸進程是不能使用kill命令清除掉的。因為kill命令只是用來終止進程的,而僵尸進程已經終止。
? 通過殺死進程的父進程,使init進程領養該進程,int進而回收此進程資源。
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來保存進程的退出狀態。借助宏函數來進一步判斷進程終止的具體原因。宏函數可分為如下三組:
WIFEXITED(status) 為非0,進程正常結束
WEXITSTATUS(status) 如上宏為真,使用此宏,獲取進程退出狀態(exit的參數)
WIFSIGNALED(status) 為非0, 進程異常終止
WTERMSIG(status) 如上宏為真,使用此宏,獲取使進程終止的信號的編號
WIFSTOPPED(status) 為非0,進程處于暫停狀態
WSTOPSIG(status) 如上宏為真,使用此宏,獲取使進程暫停的信號的編號
WIFCONTINUED(status) 為真,進程暫停后已經繼續運行
? 一次wait調用,回收一個子進程。
waitpid
? 作用同wait,但可指定pid進程清理,可以不阻塞。
? pid_t waitpid(pid_t pid,int *status, in options);成功:返回清理掉的子進程ID;失敗-1
? 特殊參數和返回情況:
? 參數pid:
1. 大于0,回收指定ID的子進程2. -1, 回收任意子進程(相當于wait)3. 0, 回收和當前調用waitpid一個組的所有子進程(一次回收一個)4. 小于0, 回收指定進程組內的任意子進程? 參數status:用戶獲取進程退出狀態。
? 參數options:0:阻塞;WNOHANG:非阻塞。
? 返回0:參數3為WNOHANG,且子進程正在運行,不阻塞;使用時如果為了保證進程退出回收,可以輪詢調用。其他情況與wait一樣。
? 一次wait和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, wpid;if(argc == 2){n = atoi(argv[1]);}for ( i = 0; i < n; i++){p = fork();if(p == 0){break;}else if(i == 3){q = p;}}if(n == i){sleep(n);printf("parent process,pid = %d\n",getpid()); #if 0while(wait(NULL)>0){}printf("parent end\n"); #else// while(waitpid(-1,,NULL,0)>0){//阻塞,同wait// }// printf("parent end\n");// waitpid(q, NULL, o);//阻塞,回收指定進程do{wpid = waitpid(-1, NULL, WNOHANG);//非阻塞if(wpid > 0){n--;}//如果wpid==0,說明子進程正在運行sleep(1);}while(n > 0);printf("parent end\n"); #endif}else{sleep(i);printf("child %d process,pid=%d\n",i+1,getpid());}return 0; }2.IPC方法
? Linux環境下,進程地址空間相互獨立,每個進程各自有不同的用戶地址空間。任何一個進程的全局變量在另一個進程中都看不到,所以進程和進程之間不能互相訪問,要交換數據必須通過內核,在內核中開辟一塊緩沖區,進程1把數據從用戶空間考到內核緩沖區,進程2再從內核緩沖區把數據讀走,內核提供的這種機制稱為進程間通訊(IPC,InterProcess Communication)。
? 在進程間完成數據傳遞需要借助操作系統提供的方法,如:文件、管道、信號、共享內存、消息隊列、套接字、命名管道等。隨著計算機的蓬勃發展,一些方法由于自身設計缺陷被淘汰和棄用,如今的進程間通訊方式有:
1. 管道(使用最簡單) 2. 信號(開銷最小) 3. 共享映射區(無血緣關系) 4. 本地套接字(最穩定)管道
管道的概念
? 管道是一種最基本的IPC機制,作用于有血緣關系的進程之間,完成數據傳遞。調用pipe系統函數即可創建一個管道,如下特質:
1. 本質是一個偽文件(實為內核緩沖區) 2. 由兩個文件描述符引用,一個表示讀端,一個表示寫端。 3. 規定數據從管道的寫端流入管道,從讀端流出。占用存儲空間的文件類型(普通文件-;符號鏈接s;目錄d)。
偽文件類型(套接字s;塊設備b;字符設備c;管道p)。
管道的原理:管道實為內核使用環形隊列機制,借助內核緩沖區(4k)實現。
管道的局限性:
? 1) 數據自己讀不能自己寫。
? 2)數據一旦被讀走,便不再管道中存在,不可反復讀取。
? 3)由于管道采用半雙工通信方式。因此,數據只能在一個方向上流動。
? 4)只能在有公共祖先的進程間(有血緣關系的進程)使用管道。
pipe函數
? 創建管道
? int pipe(int pipefd[2]); 成功:0;失敗:-1,設置errno
? 函數調用成功返回r/w兩個文件描述符。無需open,但需手動close。規定:fd[0] ,r;fd[1],w;就像0對應標準輸入,1對應標準輸出一樣。向管道文件讀寫數據其實是在讀寫內核緩沖區。
? 管道創建成功以后,創建該管道的進程(父進程)同時掌握著管道的讀端和寫端。如何實現父子進程通信呢,通常步驟如下:
1. 父進程調用pipe函數創建管道,得到兩個文件描述符fd[0]、fd[1]指向管道的讀端和寫端。 2. 父進程調用fork創建子進程,那么子進程也有兩個文件描述符指向同一管道。 3. 父進程關閉管道讀端,子進程關閉管道寫端。父進程可以向管道中寫入數據,子進程將管道中的數據讀出。由于管道是利用環形隊列實現的,數據從寫端流入管道,從讀端流出,這樣就實現了進程間通信。 #include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<string.h>int main(int argc, char* argv[]) {int fd[2];int ret = pipe(fd);if(ret == -1){perror("pipe");exit(1);}pid_t pid = fork();if(-1 == pid){perror("fork");exit(1);}if( 0 == pid ){printf("child process read\n");close(fd[1]);char buf[1024];ret = read(fd[0], buf, sizeof(buf));if(ret == 0){printf("read over\n");}write(STDOUT_FILENO, buf, ret);}else{printf("parent process write\n");close(fd[0]);write(fd[1], "pipe trans\n",strlen("pipe trans\n"));}return 0; }管道的讀寫行為
1. 如果所有指向管道寫端的文件描述符都關閉了(管道寫端引用計數為0),而仍然有進程從管道讀端讀數據,那么管道中剩余的數據都被讀取后,再次read會返回0,就像讀到文件末尾一樣。 2. 如果有指向管道寫端的文件描述符沒有關閉(管道寫端引用計數不為0),且持有寫端描述符的進程也沒有向管道中寫數據,這時讀端從管道中讀取數據后,將會阻塞read,直到管道中有了數據再繼續讀取。 3. 如果所有指向管道讀端的文件描述符都關閉了(管道讀端引用計數為0),這時持有管道寫端文件描述符的進程會接收到信號SIGPIPE,導致該進程異常終止。 4. 如果有指向管道讀端的文件描述符沒有關閉(讀端引用計數不為0),此時如果管道已經數據寫滿了,那么將會阻塞write,直到有空間可以寫入數據。管道緩沖區大小
? 可以使用ulimit -a命令來查看當前系統中創建管道文件所對應的內核緩沖區大小,其大小通常為4k,每個扇區512b,使用8個扇區。
管道的優勢
優點:簡單
缺點:1. 只能單向通信,雙向可建立兩個管道
? 2. 只能用于父子進程,兄弟進程通信。
FIFO
? FIFO通常叫做命名管道,以區分管道(pipe)。
? FIFO是linux基礎文件類型中的一種,但是FIFO在磁盤上沒有數據庫,僅僅用來標識內核中的一條通道。各個進程可以打開這個文件進行讀寫操作,實際上是在讀寫內核通道,這樣就實現了進程間通信。
? int mkfifo(const char * pathname, mode_t mode); 成功:0;失敗:-1
? 創建FIFO后,可以使用open打開他,常用的io操作都可用于FIFO,如:close、read、write、unlink等。
? 可以在非血緣關系進程間實現通訊,借助隊列實現,不能反復讀取。
共享存儲映射(共享內存)
文件進程間通訊
? 使用文件也可以完成IPC,理論依據是,fork后,父子進程共享文件描述符,也就是共享打開的文件。
存儲映射I/O
? 存儲映射I/O(memory-mapped I/O)使一個磁盤文件與存儲空間中的一個緩沖區相映射。于是當從緩沖區中取數據,就相當于讀文件中的相應字節。與此類似,將數據存入緩沖區,則相應的字節就自動寫入文件。這樣,就可在不使用read和write函數的情況下,使用地址(指針)完成I/O操作。
? 使用這種方法,首先應通知內核,將一個指定文件映射到存儲區域中。這個映射工作可以通過mmap函數來實現。
mmap父子進程通訊
mmap函數:
void * mmap(void *adrr, size_t length, int prot, int flags, int fd, off_t offset);
返回:成功:返回創建的映射區首地址; 失敗:MAP_FAILED宏
參數:
MAP_SHARED: 會將映射區所做的操作反應到物理設備(磁盤)上。
MAP_PRIVATE:映射區所做的修改不會反應到物理設備。
注意:
父子進程通信
? MAP_PRIVATE:父子進程獨享映射,進程內的映射區不受其他進程影響
? MAP_SHARED: 父子進程共享映射
當以上mmap創建方式使用MAP_PRIVATE后,第二次打印出的信息為parent, *p = 0, var = 100
父子進程共享:
匿名映射
? 通過使用發現,使用映射區來完成文件讀寫操作十分方便,父子進程間通信也比較容易。但缺陷是,每次創建映射區一定要依賴一個文件才能實現。通過為了創建映射區要open一個temp文件,創建好了之后再unlink、close掉,比較麻煩。可以直接使用匿名映射來代替。其實linux系統為我們提供了創建匿名映射的方法,無需依賴文件即可創建映射區。同樣需要借助標志位參數flags來指定。
使用MAP_ANONYMOUS(或者MAN_ANON),如:
? int *p=mmap(NULL, 4, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1 ,0);
4隨意舉例,該位置表大小,按需求填寫即可。
? 需要注意的是,MAP_ANONYMOUS 和 MAP_ANON 這兩個宏是linux操作系統特有的宏,在類unix系統中無該宏定義,可使用如下兩步來完成匿名映射區的建立。/dev/zero文件可大可小,沒有限制。
fd = open('/dev/zero', O_RDWR); p = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED,fd,0);?
mmap無血緣關系進程間通訊
? 實質上mmap是內核借助文件幫我們創建了一個映射區,多個進程間利用該映射區完成數據傳遞。由于內核空間多進程共享,因此無血緣關系的進程間也可以使用mmap來完成通信。只要設置相應的標志位參數即可MAP_SHARED,映射的同一個文件即可。
讀進程:
#include<stdio.h> #include<fcntl.h> #include<string.h> #include<unistd.h> #include<stdlib.h> #include<sys/mman.h> #include<sys/stat.h>struct STU{int id;char name[20];char sex; };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(-1 == fd){printf("open");exit(1);}mm = mmap(NULL, sizeof(student), PROT_READ, MAP_SHARED , fd, 0);if(mm == MAP_FAILED){perror("mmap error");exit(1);}close(fd);while(1){printf("id=%d\tname=%s\t%c\n",mm->id,mm->name,mm->sex);sleep(2);}munmap(mm, sizeof(student));return 0; }寫進程:
#include<stdio.h> #include<fcntl.h> #include<string.h> #include<unistd.h> #include<stdlib.h> #include<sys/mman.h> #include<sys/stat.h> #include<sys/types.h>struct STU{int id;char name[20];char sex; };int main(int argc, char *argv[]) {int fd;struct STU student = {10,"xiaoming",'m'};struct STU *mm;if(argc < 2){printf("./a.out file_shared\n");exit(1);}fd = open(argv[1], O_RDWR | O_CREAT, 0664);if(-1 == fd){printf("open");exit(1);}ftruncate(fd, sizeof(student));mm = mmap(NULL, sizeof(student), PROT_READ, MAP_SHARED , fd, 0);if(mm == MAP_FAILED){perror("mmap error");exit(1);}close(fd);while(1){memcpy(mm, &student, sizeof(student));student.id++;sleep(1);}munmap(mm, sizeof(student));return 0; }練習:
博主鏈接:https://blog.csdn.net/bureau123/category_10691972.html
2. 簡易shell #include <unistd.h> #include <string.h> #include <sys/wait.h> #include <sys/types.h> #include <sys/stat.h> #include <dirent.h> #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <malloc.h>#define MAX_CMD_LENGTH 255 #define MAX_PATH_LENGTH 255 #define MAX_BUF_SIZE 4096 #define MAX_ARG_NUM 50 #define MAX_VAR_NUM 50 #define MAX_CMD_NUM 10 #define MAX_VAR_LENGTH 500#define FORK_ERROR 2 #define EXEC_ERROR 3struct cmd{struct cmd * next;int begin,end; // pos in cmdStrint argc;char lredir,rredir; 0:no redirect 1 <,> ; 2 >>char toFile[MAX_PATH_LENGTH],fromFile[MAX_PATH_LENGTH]; // redirect file pathchar *args[MAX_ARG_NUM];char bgExec; //failExec };struct cmd cmdinfo[MAX_CMD_NUM]; char cmdStr[MAX_CMD_LENGTH]; int cmdNum,varNum; char envVar[MAX_VAR_NUM][MAX_PATH_LENGTH];void Error(int ); void debug(struct cmd*); void init(struct cmd*); void setIO(struct cmd*,int ,int ); int getInput(); int parseCmds(int); int handleVar(struct cmd *,int); int getItem(char *,char *,int); int parseArgs(); int execInner(struct cmd*); int execOuter(struct cmd*);int main(){while (1){cmdNum = varNum = 0;printf("# ");fflush(stdin);int n = getInput();if(n<=0)continue; parseCmds(n);if(parseArgs()<0)continue;for(int i=0;i<cmdNum;++i){struct cmd *pcmd=cmdinfo+i, * tmp;//debug(pcmd);//pcmd = reverse(pcmd);int status = execInner(pcmd);if(status==1){/*notice!!! Use child proc to execute outer cmd, bacause exec funcs won't return when successfully execed. */pid_t pid = fork();if(pid==0)execOuter(pcmd);else if(pid<0)Error(FORK_ERROR);if(!pcmd->bgExec)wait(NULL); //background exec/* free malloced piep-cmd-node,and the first one is static , no need to free; */ pcmd=pcmd->next; while(pcmd){tmp = pcmd->next;free(pcmd);pcmd=tmp;}}}}return 0; }/* funcs implementation */ void init(struct cmd *pcmd){pcmd->bgExec=0;pcmd->argc=0;pcmd->lredir=pcmd->rredir=0;pcmd->next = NULL;pcmd->begin=pcmd->end=-1;/* // notice!!! Avoid using resudent args */for(int i=0;i<MAX_ARG_NUM;++i)pcmd->args[i]=NULL; }void Error(int n){switch(n){case FORK_ERROR:printf("fork error\n");break;case EXEC_ERROR:printf("exec error\n");break;default:printf("Error, exit ...\n");}exit(1); }int getInput(){/* multi line input */int pCmdStr=0,cur;char newline = 1;while(newline){cur = MAX_CMD_LENGTH-pCmdStr;if(cur<=0){printf("[Error]: You cmdStr is too long to exec.\n");return -1;// return -1 if cmdStr size is bigger than LENGTH}fgets(cmdStr+pCmdStr,cur,stdin);newline = 0;while(1){if(cmdStr[pCmdStr]=='\\'&&cmdStr[pCmdStr+1]=='\n'){newline=1;cmdStr[pCmdStr++]='\0';break;}else if(cmdStr[pCmdStr]=='\n'){break;}++pCmdStr;}}return pCmdStr; }int parseCmds(int n){/* clean the cmdStr and get pos of each cmd in the cmdStr (OoO) */char beginCmd=0;struct cmd * head; // use head cmd to mark background.for( int i=0;i<=n;++i){switch(cmdStr[i]){case '&':{if(cmdStr[i+1]=='\n'||cmdStr[i+1]==';'){cmdStr[i]=' ';head->bgExec=1;}}case '\t':cmdStr[i]=' ';break;case ';':{//including ';' a new cmdStrbeginCmd = 0;cmdStr[i]='\0'; cmdinfo[cmdNum++].end=i;break;}case '\n':{cmdStr[i]='\0';cmdinfo[cmdNum++].end =i;return 0;}case ' ':break;default:if(!beginCmd){beginCmd=1;head = cmdinfo+cmdNum;cmdinfo[cmdNum].begin = i;}}} }int getItem(char *dst,char*src, int p){ /* get redirect file path from the cmdStr */int ct=0;while(src[++p]==' ');if(src[p]=='\n')return -1; //no file char c;while(c=dst[ct]=src[p]){if(c==' '||c=='|'||c=='<'||c=='>'||c=='\n')break;++ct,++p;}dst[ct]='\0';return p-1; }int handleVar(struct cmd *pcmd,int n){char * arg = pcmd->args[n];int p_arg=0,p_var=0;while(arg[p_arg]){if((arg[p_arg]=='$')&&(arg[p_arg-1]!='\\')){if(arg[p_arg+1]=='{')p_arg+=2;else p_arg+=1;char *tmp=&envVar[varNum][p_var];int ct=0;while(tmp[ct]=arg[p_arg]){if(tmp[ct]=='}'){++p_arg;break;}if(tmp[ct]==' '||tmp[ct]=='\n'||tmp[ct]=='\0')break;++ct,++p_arg;}tmp[ct]='\0';tmp = getenv(tmp);for(int i=0;envVar[varNum][p_var++]=tmp[i++];);p_var-=1; //necessary}else envVar[varNum][p_var++]=arg[p_arg++];}envVar[varNum][p_var]='\0';pcmd->args[n] = envVar[varNum++];return 0; }int parseArgs(){/* get args of each cmd and create cmd-node seperated by pipe */char beginItem=0,beginQuote=0,beginDoubleQuote=0,hasVar=0,c;int begin,end;struct cmd* pcmd;for(int p=0;p<cmdNum;++p){if(beginQuote||beginItem||beginDoubleQuote){return -1; // wrong cmdStr}pcmd=&cmdinfo[p];begin = pcmd->begin,end = pcmd->end;init(pcmd);// initalize for(int i=begin;i<end;++i){c = cmdStr[i];if((c=='\"')&&(cmdStr[i-1]!='\\'&&(!beginQuote))){if(beginDoubleQuote){cmdStr[i]=beginDoubleQuote=beginItem=0;if(hasVar){hasVar=0;handleVar(pcmd,pcmd->argc-1); //note that is argc-1, not argc}}else{beginDoubleQuote=1;pcmd->args[pcmd->argc++]=cmdStr+i+1;}continue;}else if(beginDoubleQuote){if((c=='$') &&(cmdStr[i-1]!='\\')&&(!hasVar))hasVar=1;continue;}if((c=='\'')&&(cmdStr[i-1]!='\\')){if(beginQuote){cmdStr[i]=beginQuote=beginItem=0;}else{beginQuote=1;pcmd->args[pcmd->argc++]=cmdStr+i+1;}continue;}else if(beginQuote) continue;if(c=='<'||c=='>'||c=='|'){if(beginItem)beginItem=0;cmdStr[i]='\0';}if(c=='<'){if(cmdStr[i+1]=='<'){pcmd->lredir+=2; //<<cmdStr[i+1]=' ';}else{pcmd->lredir+=1; //<}int tmp = getItem(pcmd->fromFile,cmdStr,i);if(tmp>0)i = tmp;}else if(c=='>'){if(cmdStr[i+1]=='>'){pcmd->rredir+=2; //>>cmdStr[i+1]=' ';}else{pcmd->rredir+=1; //>}int tmp = getItem(pcmd->toFile,cmdStr,i);if(tmp>0)i = tmp;}else if (c=='|'){/*when encountering pipe | , create new cmd node chained after the fommer one */pcmd->end = i;pcmd->next = (struct cmd*)malloc(sizeof(struct cmd));pcmd = pcmd->next;init(pcmd);}else if(c==' '||c=='\0'){if(beginItem){beginItem=0;cmdStr[i]='\0';}}else{if(pcmd->begin==-1)pcmd->begin=i;if(!beginItem){beginItem=1;if((c=='$') &&(cmdStr[i-1]!='\\')&&(!hasVar))hasVar=1;pcmd->args[pcmd->argc++]=cmdStr+i;}}if(hasVar){hasVar=0;handleVar(pcmd,pcmd->argc-1); //note that is argc-1, not argc}}pcmd->end=end;//printf("%dfrom:%s %dto:%s\n",pcmd->lredir,pcmd->fromFile,pcmd->rredir,pcmd->toFile);} }int execInner(struct cmd* pcmd){ /*if inner cmd, {exec, return 0} else return 1 */if (!pcmd->args[0])return 0;if (strcmp(pcmd->args[0], "cd") == 0) {struct stat st;if (pcmd->args[1]){stat(pcmd->args[1],&st);if (S_ISDIR(st.st_mode))chdir(pcmd->args[1]);else{printf("[Error]: cd '%s': No such directory\n",pcmd->args[1]);return -1;}}return 0;}if (strcmp(pcmd->args[0], "pwd") == 0) {printf("%s\n",getcwd(pcmd->args[1] , MAX_PATH_LENGTH));return 0;}if (strcmp(pcmd->args[0], "unset") == 0) {for(int i=1;i<pcmd->argc;++i)unsetenv(pcmd->args[i]);return 0;}if (strcmp(pcmd->args[0], "export") == 0) {for(int i=1;i<pcmd->argc;++i){ //putenv(pcmd->args[i]);char *val,*p;for(p = pcmd->args[i];*p!='=';++p);*p='\0';val = p+1;setenv(pcmd->args[i],val,1);}return 0;}if (strcmp(pcmd->args[0], "exit") == 0)exit(0);return 1; } void setIO(struct cmd *pcmd,int rfd,int wfd){/* settle file redirect */if(pcmd->rredir>0){ // >, >>int flag ;if(pcmd->rredir==1)flag=O_WRONLY|O_TRUNC|O_CREAT; // > note: trunc is necessary!!!else flag=O_WRONLY|O_APPEND|O_CREAT; //>>int wport = open(pcmd->toFile,flag);dup2(wport,STDOUT_FILENO);close(wport);}if(pcmd->lredir>0){ //<, <<int rport = open(pcmd->fromFile,O_RDONLY);dup2(rport,STDIN_FILENO);close(rport);}/* pipe */if(rfd!=STDIN_FILENO){dup2(rfd,STDIN_FILENO);close(rfd);}if(wfd!=STDOUT_FILENO){dup2(wfd,STDOUT_FILENO);close(wfd);} } int execOuter(struct cmd * pcmd){if(!pcmd->next){setIO(pcmd,STDIN_FILENO,STDOUT_FILENO);execvp(pcmd->args[0],pcmd->args);}int fd[2];pipe(fd);pid_t pid = fork();if(pid<0){Error(FORK_ERROR);}else if (pid==0){close(fd[0]);setIO(pcmd,STDIN_FILENO,fd[1]);execvp(pcmd->args[0],pcmd->args);Error(EXEC_ERROR);}else{wait(NULL);pcmd = pcmd->next; //noticeclose(fd[1]);setIO(pcmd,fd[0],STDOUT_FILENO); execOuter(pcmd);} }博主鏈接:https://www.jianshu.com/p/d6d9b5b976e8
3. 本地聊天室簡單 //server #include <unistd.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <sys/types.h> #include <sys/stat.h> #include <string.h>#define SERVER_FIFO "/home/wx/test/sysday3/codes/SERVER_FIFO"struct client {char clientName[20];//客戶端名字int fifoDis;//私有管道的描述符 };typedef struct client CL; //用來記錄客戶機的數量 int clientlen=0; //利用數組將存儲客戶隊列(不方便,而且會浪費),可以改造為鏈表(最好)。 CL clientDueue[100];struct messagePacket {int messageNo;//消息編號char senderName[20];//消息發送方char receiverName[20];//消息接收方char data[1024];//數據采用不定長消息 };typedef struct messagePacket MSP;//公共管道 int serFifo; //服務器啟動標志 int startFlags=0;//初始化,負責初始化服務器。 void initServer(); //負責接收客戶端發送的包 void receiverPacket(); //負責將客戶端發送的包解析 void parsingPacket(MSP *msp); //負責客戶端登陸,將客戶端插入客戶隊列中,并創建私有管道 void clientLogin(char* loginName); //負責將消息發送到對應的接受方 void messageSend(MSP *pMsp); //負責客戶端的退出,將客戶端從客戶隊列中刪除,并刪除創建的管道 void clientQuit(char* quitName); //負責關閉服務器,關閉打開的管道和刪除客戶機列表 void closeServer(); //負責處理輸入的數據 void messageHanle(char* pMes);#define BUFSIZE 1068void initServer() {//將STDIN_FILENO修改為非阻塞int serFlags=fcntl(STDIN_FILENO,F_GETFL);serFlags|=O_NONBLOCK;fcntl(STDIN_FILENO,F_SETFL,serFlags);int results = mkfifo(SERVER_FIFO, 0666);if(results<0){perror("SERVER mkfifo:");exit(1);}//以非阻塞只讀的方式打開管道serFifo=open(SERVER_FIFO,O_RDONLY|O_NONBLOCK);if(serFifo<0){perror("SERVER OPEN:");exit(1);}printf("服務器已啟動,正在監聽...\n");startFlags=1-startFlags; }void receiverPacket() {char buf[BUFSIZE];MSP *msp;int len=read(serFifo,buf,sizeof(buf));if(len>0){msp=(MSP*)buf;parsingPacket(msp);} }void parsingPacket(MSP *msp) {//根據相應的功能號,調用相應的函數。switch(msp->messageNo){case 0:clientLogin(msp->senderName);break;case 1:messageSend(msp);break;case 2:clientQuit(msp->senderName);break;} }void clientLogin(char* loginName) {//不能直接賦值,會造成淺拷貝strcpy(clientDueue[clientlen].clientName,loginName);char path[23]="./";strcat(path,loginName);//確保創建的文件的權限為分配權限umask(0);//創建管道mkfifo(path,0777);//將管道的文件描述符存入數組中clientDueue[clientlen].fifoDis=open(path,O_WRONLY);char buf[]="您和服務器的連接已經成功建立,可以開始通訊了\n";write(clientDueue[clientlen].fifoDis,buf,sizeof(buf));//這里應該將管道創建為臨時的,如果是使用數據庫,可以創建為永久的unlink(path);//沒有對cientlen進行限制++clientlen; }void clientQuit(char* quitName) {//最好是利用鏈表管理登錄的客戶機int i=0;for(i=0;i<clientlen;i++){if(strcmp(quitName,clientDueue[i].clientName)==0){//關閉對應的私有通過close(clientDueue[i].fifoDis);clientDueue[i].fifoDis=-1;clientDueue[i].clientName[0]='\0';break;}}printf("%s已退出\n",quitName); }void messageSend(MSP *pMes) {int i=0;char* buf=(void*)pMes;if(strlen(pMes->receiverName)!=0){//單發for(i=0;i<clientlen;++i){if(strcmp(pMes->receiverName,clientDueue[i].clientName)==0){write(clientDueue[i].fifoDis,buf,BUFSIZE);break;}}}else{//群發for(i=0;i<clientlen;++i){write(clientDueue[i].fifoDis,buf,BUFSIZE);}} }void messageHanle(char* pMes) {if(strcmp(pMes,"quit-->()")==0){closeServer();}//可以繼續增加一些命令(顯示有幾個客戶端,客戶端的管道描述符等) } void closeServer() {char buf[]="服務器維護中,請稍后登錄。";int i=0;for(i=0;i<clientlen;++i){if(clientDueue[i].fifoDis!=-1){write(clientDueue[i].fifoDis,buf,strlen(buf));close(clientDueue[i].fifoDis);}}close(serFifo);startFlags=1-startFlags;printf("以關閉所有管道,服務器安全退出"); } int main() {initServer();char mes[1024];while(startFlags){receiverPacket();if(scanf("%s",mes)!=EOF){messageHanle(mes);}}return 0; } //Client #include <fcntl.h> #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <errno.h> #include <string.h> #define SERVER_FIFO "/home/wx/test/sysday3/codes/SERVER_FIFO"int linkFlags=0;//連接標志 int serFifo;//公共管道文件描述符 int cliFifo;//客戶端私有端道文件描述符 char clientName[20];//客戶端名稱struct messagePacket {int messageNo;//消息編號char senderName[20];//消息發送方char receiverName[20];//消息接收方char data[1024];//數據采用定長消息 };typedef struct messagePacket MSP;//初始化客戶大端 void initClient(); //登陸服務器 void loginServer(); //處理用戶輸入的數據 void messageHanle(char* pMes); //向服務器發送消息 void sendSerMes(int mesNO); //向其他用戶發送消息 void sendMessage(char* receiverName,char* data); //接收消息 void receiverMes(); //關閉客戶端 void closeClient();//localClient.c #define BUFSIZE 1068void initClient() {loginServer();//將連接標志置為1.linkFlags=1-linkFlags;//將STDIN文件屬性修改為非阻塞int flags=fcntl(STDIN_FILENO,F_GETFL);flags |= O_NONBLOCK;fcntl(STDIN_FILENO,F_SETFL,flags); }void loginServer() {printf("請輸入客戶端名稱(不超過20個字符):\n");//write(STDIN_FILENO,clientName,20);scanf("%s",clientName);serFifo=open(SERVER_FIFO,O_WRONLY|O_NONBLOCK);if(serFifo<0){perror("open server fifo");exit(1);}sendSerMes(0);char path[23]="./";strcat(path,clientName);//測試管道是否創建成功while(access(path,F_OK)!=0);cliFifo=open(path,O_RDONLY|O_NONBLOCK);if(cliFifo<0){perror("open client fifo");}printf("私有管道創建成功\n"); }void sendSerMes(int mesNO) {MSP msp;char *buf;msp.messageNo=mesNO;strcpy(msp.senderName,clientName);buf=(void*)&msp;write(serFifo,buf,sizeof(msp)); }void messageHanle(char* pMes) {//將“quit-->()”設置為退出消息if(strcmp(pMes,"quit-->()")==0){sendSerMes(2);closeClient();return;}//發送數據格式為:接受者姓名:消息內容//如果數據不符合規范,則將消息轉為群發。int i=0;int j=0;char receiverName[20];char data[1024];while(pMes[i]!='\0'&&pMes[i]!=':'){receiverName[i]=pMes[i];++i;}receiverName[i]='\0';if(pMes[i]==':'){//將:跳過++i;}else{i=0;receiverName[0]='\0';}while(pMes[i]!='\0'){data[j++]=pMes[i++];}data[j]='\0';sendMessage(receiverName,data); }void sendMessage(char* receiverName,char* data) {MSP msp;char *buf;msp.messageNo=1;strcpy(msp.senderName,clientName);strcpy(msp.receiverName,receiverName);strcpy(msp.data,data);buf=(void*)&msp;write(serFifo,buf,sizeof(msp)); }void receiverMes() {char buf[BUFSIZE];int len=read(cliFifo,buf,sizeof(MSP));MSP *pMes=NULL;pMes=(void*)buf; if(len>0&&pMes->messageNo==1){printf("%s:%s\n",pMes->senderName,pMes->data);}else if(len>0){printf("系統提示:%s",buf);} }void closeClient() {//將連接標志置為0linkFlags=1-linkFlags;//關閉私有管道close(cliFifo);//關閉公共管道close(serFifo);printf("以關閉所以管道,客戶端安全退出\n"); }int main() {initClient();char mesBuf[1024];while(linkFlags){//scanf()默認遇空格終止scanf("%49[^\n]",mesBuf)!=EOF//int len=write(STDIN_FILENO,mesBuf,BUFSIZE);if(scanf("%s",mesBuf)!=EOF){messageHanle(mesBuf);} receiverMes();}return 0; }原文鏈接:https://blog.csdn.net/qq_39038983/article/details/88418412
3.信號
? 信號是信息的載體,linux環境下,經典的通信方式,依然是主要的通信手段。
? 機制:A給B發送信號,B收到信號之前執行自己的代碼,收到信號后,不管執行到程序的什么位置,都要暫停運行,去處理信號,處理完畢再繼續執行。與硬件中斷類似,異步模式。但是信號是軟件層次上實現的中斷,早期被稱為軟中斷。
? 特質:由于信號是通過軟件的方法實現,所以導致信號有很強的延時性。但對于用戶,不易察覺。
? 每個進程收到的所有信號,都是由內核負責發送的,內核處理。
信號的事件和狀態
產生信號:
1. 按鍵產生:如ctrl + c 2. 系統調用: 如kill 3. 軟件條件:定時器 4. 硬件異常:如非法訪問內存(段錯誤)、除0 5. 命令產品:kill命令**遞達:**遞送并到達進程
未決:產生和遞達之間的狀態。主要由于阻塞(屏蔽)導致該狀態
信號的處理方式:
1. 執行默認動作 2. 忽略(丟棄) 3. 捕捉(調用戶處理函數)? linux內核的進程控制塊PCB是一個結構體,task_struct,除了包含進程id,狀態,工作目錄,用戶id,組id,文件描述符表,還包含了信號相關的信息,主要指阻塞信號集和未決信號集。
? **阻塞信號集(信號屏蔽字):**將某些信號加入集合,對他們設置屏蔽,當屏蔽x信號后,再收到該信號,該信號的處理將推后(解除屏蔽后)。
? 未決信號集:
1. 信號產生,未決信號集中描述該信號的位立刻翻轉為1,表示信號處于未決狀態。當信號被處理對應位翻轉回為0,這一過程往往非常短暫。 2. 信號產生后由于某些原因(主要是阻塞)不能抵達。這類信號的集合稱之為未決信號集。在屏蔽解除前,信號一直處于未決狀態。信號的編號:
? 可以使用kill -l命令查看當前系統可使用的信號有哪些。
信號的四要素:
1. 編號 2. 名稱 3.事件 4.默認處理動作可通過命令man 7 signal查看幫助文檔獲取。
? 默認處理動作:
? Term:終止進程
? core:終止進程,生成core文件(檢查進程死亡原因,用戶gdb調試)
? stop:停止(暫停)進程
? cont:繼續運行進程
? ign:忽略信號(默認即時對該種信號忽略操作)
? 9-SIGKILL 和 19-SIGSTOP信號,不允許忽略和捕捉,只能執行默認動作,甚至不能設置為阻塞。
信號的產生
終端按鍵產生的信號
ctrl+c → 2-SIGINT(終止/終端)
ctrl+z → 20-SIGTSTP(暫停/停止)停止終端交互進程的運行
ctrl+ \ → 3-SIGQUIT(退出)
硬件異常產生信號
除0操作 → 8-SIGFPE(浮點數例外)
非法訪問內存 → 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
raise和abort
raise:給當前進程發送指定信號(自己給自己發) raise(signo)== kill(getpid(),signo);
? int raise(int sig);成功:0,失敗:非0值
abort:給自己發送異常終止信號 6-SIGABRT信號,終止產生的core文件
? void abort(void);函數無返回
軟件條件產生
alarm函數:
? 設置定時器(鬧鐘)。在指定seconds后,內核會給當前進程發送 14-SIGALRM信號。進程收到該信號,默認動作終止。
? 每個進程都有且只有為一個定時器。
? unsigned int alarm(unsigned int seconds);返回0或剩余的秒數,無失敗。
? 常用:取消定時器alarm(0),返回舊鬧鐘余下秒數。
? 定時,與進程狀態無關(自然定時法)就緒、運行、掛起(阻塞、暫停)、終止、僵尸,無論進程處于何種狀態,alarm都計時。
? 可以使用time a.out查看程序運行時間,實際時間,用戶時間,內核時間。實際時間=用戶時間+內核時間+等待時間。
setitimer函數:
? 設置定時器(鬧鐘)。可以代替alarm函數。精度微秒us,可以實現周期定時。
? int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);成功:0,失敗:-1,設置errno
? 參數:which:指定定時方式
? 1、自然定時:ITIMER_REAL → 14-SIGALRM 計算自然時間
? 2、虛擬空間計時(用戶空間):ITIMER_VIRTUAL → 26-SIGVTALRM 只計算進程占用cpu的時間
? 3、運行時計時(用戶+內核):ITIMER_PROF → 27-SIGPROF 計算占用cpu及執行系統調用的時間
struct itimerval {struct timeval it_interval;//下一次定時的值struct timeval it_value;//當前定時的值 } struct timeval{time_t tv_sec;//秒suseconds_t tv_usec;//微妙 } #include<stdio.h> #include<stdlib.h> #include<sys/time.h>unsigned int myalarm(unsigned int sec) {struct itimerval it, oldit;int ret;it.it_value.tv_sec = sec;it.it_value.tv_usec = 0;it.it_interval.tv_sec = 0;it.it_interval.tv_usec = 0;ret = setitimer(ITIMER_REAL, &it, &oldit);if(-1 == ret){perror("setitimer");exit(1);}return oldit.it_value.tv_sec; }int main(){int i ;myalarm(1);for ( i = 0; ; i++){printf("%d\n",i);}return 0; }信號的捕捉
signal函數:
? typedef void (*sighandler_t)(int);
? sighandler_t signal(int signum, sighandler_t handler);
? 參數:signum,信號,可以使用宏
? handler,信號捕捉函數
#include<stdio.h> #include<stdlib.h> #include<sys/time.h> #include<signal.h> #include<errno.h>typedef void (*sighandler_t)(int);void myfunc(int signo) {printf("deal signal = %d\n",signo); }int main(){struct itimerval it, oldit;sighandler_t handler;handler = signal(SIGALRM, myfunc); //注冊sigalrm信號的捕捉處理函數if(handler == SIG_ERR){perror("signal");exit(1);}int ret;it.it_value.tv_sec = 5;it.it_value.tv_usec = 0;it.it_interval.tv_sec = 3;it.it_interval.tv_usec = 0;ret = setitimer(ITIMER_REAL, &it, &oldit);if(-1 == ret){perror("setitimer");exit(1);}while(1);return 0; }sigaction函數:
int sigactiong(int signum, const struct sigactiong *act, struct sigaction *oldact);
返回:成功:0;失敗:-1;設置errno
struct sigaction{void (*sa_handler)(int);//指定信號捕捉后的處理函數名(即注冊函數)。也可賦值SIG_IGN表示忽略或SIG_DFL表示執行默認動作void (*sa_sigaction)(int, siginfo_t *, void *);//當sa_flags被指定為SA_SIGINFO標志時,使用該信號處理程序(很少使用)sigset_t sa_mask;//信號處理函數期間屏蔽的信號集;調用信號處理函數時,所要屏蔽的信號集合(信號屏蔽字)。注意:僅在處理函數被調用期間屏蔽生效,臨時性設置。int sa_flags;//捕捉函數處理期間對相同信號的處理;通常為0,表示使用默認屬性。void (*sa_restorer)(void);//過時,不應該使用,棄用 }參數:signum,信號
? act:傳入參數,新的處理方式
? oldact:傳出參數,舊的處理方式
#include<stdio.h> #include<stdlib.h> #include<sys/time.h> #include<signal.h>void myfunc(int signo) {printf("deal signal = %d\n",signo); }int main(){struct sigaction act;act.sa_handler = myfunc;sigemptyset(&act.sa_mask);sigaddset(&act.sa_mask,SIGQUIT);act.sa_flags = 0;//自動屏蔽本信號int ret = sigaction(SIGINT, &act, NULL);if(-1 == ret){perror("sigaction");exit(1);}while(1);return 0; }信號捕捉特性
1. 進程正常運行時,默認PCB中有一個信號屏蔽字,假定為*,他決定了進程自動屏蔽哪些信號。當注冊了某個信號捕捉函數,捕捉到該信號以后,要調用該函數。而該函數有可能執行很長時間,在這期間所屏蔽的信號不由 *來指定。而是用 sa_mask來指定。調用完信號處理函數,再恢復為 *。 2. 某個信號捕捉函數執行期間,該信號自動被屏蔽。 3. 阻塞的常規信號不支持排隊,產生多次只記錄一次。(后32個實時信號支持排隊)? 被屏蔽的信號會在執行捕捉函數執行后執行,如果該信號是被捕捉的信號,則由捕捉函數處理,如果是其他信號,則按照其默認處理方式處理。
信號集操作函數
? 內核通過讀取未決信號集來判斷信號是否應被處理。信號屏蔽字mask可以影響未決信號集。我們可以在應用程序中自定義set來改變mask。以達到屏蔽指定信號的目的。
信號集設定
sigset_t set; //typedef unsigned log sigset_t int sigemptyset(sigset_t *set); 將某個信號集清0 成功:0;失敗-1 int sigfillset(sigset_t *set); 將某個信號集置1 成功:0;失敗-1 int sigaddset(sigset_t *set, int signum); 將某個信號加入信號集 成功:0;失敗-1 int sigdelset(sigset_t * set,int signum); 將某個信號清出信號集 成功:0;失敗-1 int sigismember(const sigset_t *set,int signum); 判斷某個信號是否在信號集中 返回值:在集合:1;不在:0;出錯-1? sigset_t 類型的本質是位圖,但不應該直接使用位操作,而應該使用上述函數,保證跨系統操作有效。
對比認知select函數。
sigprocmask
? 用來屏蔽信號,解除屏蔽也使用該函數。其本質,讀取或修改進程的信號屏蔽字(PCB中)
? 嚴格注意,屏蔽信號:只是將信號處理延后執行(延至解除屏蔽);而忽略表示將信號丟處理。
? int sigprocmask(int how,const sigset_t *set, sigset_t *oldset);成功:0;失敗:-1,設置errno。
? 參數:
? set :傳入參數,是一個位圖,set中哪位置1,就表示當前進程屏蔽哪個信號。
? oldset:傳出參數,保存舊的信號屏蔽集。
? how:參數取值:假設當前的信號屏蔽字為mask
sigpending函數
? 讀取當前進程的未決信號集。
? int sigpending(sigset_t * set);set傳出參數。成功:0;失敗:-1,設置errno
#include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<signal.h>void myfunc(sigset_t *ped) {int i = 0;for(i = 1; i < 32; i++){if(sigismember(ped,i) == 1){putchar('1');}else{putchar('0');}}printf("\n"); }int main(){sigset_t myset, oldset, ped;sigemptyset(&myset);sigaddset(&myset, SIGQUIT);sigaddset(&myset, SIGINT);int ret = sigprocmask(SIG_BLOCK, &myset, &oldset);while(1){sigpending(&ped);myfunc(&ped);sleep(1);}return 0; }競態條件
? 也稱為時序競態
pause
? 調用該函數可以造成進程主動掛起,等待信號喚醒。調用該系統調用的進程將處于阻塞狀態(主動放棄cpu)直到信號遞達將其喚醒。
? int pause(void); 返回值:-1,并設置errno為EINTR。
? 返回值:
1. 如果信號的默認處理動作是終止進程,則進程終止,pause函數沒有機會返回。 2. 如果信號的默認處理動作是忽略,進程繼續處于掛起狀態,pause函數不返回。 3. 如果信號的默認處理動作是捕捉,則 調用完信號處理函數之后,pause返回-1,errno設置為EINTR,表示被信號中斷。 4. pause收到的信號不能被屏蔽,如果被屏蔽,那么pause就不能被喚醒。. //模范sleep #include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<signal.h> #include<errno.h>void catch(int singno) {; }unsigned int sleepwx(unsigned int seconds) {int ret;struct sigaction act, oldact;act.sa_handler = catch;sigemptyset(&act.sa_mask);act.sa_flags = 0;ret=sigaction(SIGALRM, &act, &oldact);if(-1 == ret){perror("sigaction");exit(1);}alarm(seconds);ret = pause();if(-1 == ret && errno == EINTR){printf("pause ok\n"); }ret = alarm(0);sigaction(SIGALRM, &oldact, NULL);//恢復alrm信號原處理方式return ret; }int main(void) {while(1){sleepwx(5);printf("sleep end!!!!!!!!!!\n"); }return 0; }? pause函數使調用進程掛起直到捕捉到一個信號。只有執行了一個信號處理程序并從其返回時,pause才返回。
時序競態
? 如果在pause之前,調用alarm之后,程序失去cpu且時間較長,這期間 定時結束,發送完信號,再執行pause,程序將會一直卡住。
解決時序問題
? 可以通過設置屏蔽SIGALRM的方法來控制程序執行邏輯,但無論如何設置,程序都有可能在“解除信號屏蔽”與“掛起等待信號”這兩個操作間隙失去cpu資源。除非將這兩步驟合并成一個“原子操作”。sigsuspend函數具備這個功能。在對時序要求嚴格的場合下都應該使用sigsuspend替換pause。
? int sigsuspend(const sigset_t *mask); 掛起等待信號。
? sigsuspend 函數調用期間,進程信號屏蔽字由其參數mask指定。
? 可將某個信號 如SIGALRM從臨時信號屏蔽字mask中刪除,這樣在調用sigsuspend時將解除對該信號的屏蔽,然后掛起等待,當sigsuspend返回時,進程的信號屏蔽字恢復為原來的值。如果原來對該信號是屏蔽態,sigsuspend函數返回后仍然屏蔽該信號。
#include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<signal.h> #include<errno.h>void catch(int singno) {; }unsigned int sleepwx(unsigned int seconds) {int ret;struct sigaction act, oldact;sigset_t newmask, oldmask, suspmask;act.sa_handler = catch;sigemptyset(&act.sa_mask);act.sa_flags = 0;ret = sigaction(SIGALRM, &act, &oldact);if(-1 == ret){perror("sigaction");exit(1);}//設置阻塞信號集集,阻塞sigalrm信號sigemptyset(&newmask);sigaddset(&newmask, SIGALRM);sigprocmask(SIG_BLOCK, &newmask, &oldmask);//定時alarm(seconds);//構造一個調用sigsuspend臨時有效的阻塞信號集,在臨時阻塞信號集里解除sigalrm的阻塞suspmask = oldmask;sigdelset(&suspmask, SIGALRM);//sigsuspend調用期間,采用臨時阻塞信號集suspmask替換原有的阻塞信號集;這個信號集中不包含sigalrm信號,同時掛起等待,當sigsuspend被信號喚醒時返回,恢復原有阻塞信號集。sigsuspend(&suspmask);ret = alarm(0);sigaction(SIGALRM, &oldact, NULL);//恢復alrm信號原處理方式sigprocmask(SIG_SETMASK, &oldmask, NULL);//解除SIG_ALRM的阻塞return ret; }int main(void) {while(1){sleepwx(5);printf("sleep end!!!!!!!!!!\n"); }return 0; }總結:
? 競態條件,跟系統負載有很緊密的關系,體現出信號的不可靠性。系統負載越嚴重,信號不可靠性越強。
? 不可靠由其實現原理所致。信號是通過軟件方式(跟內核調度高度依賴,延時性強),每次系統調用結束后,或中斷處理結束后,需通過掃描PCB中的未決信號集,來判斷是否應該處理某個信號,當系統負載過重時,會出現時序混亂。
? 這種意外情況只能在編寫程序過程中,提早遇見,主動避免,而無法通過gdb程序調試等其他手段彌補。且由于該錯誤不具規律性,后期捕捉和重現十分困難。
全局變量異步IO
? 如下例子,去掉回調函數中的sleep后將會出現錯誤。
#include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<signal.h> #include<errno.h>int n = 0, flag = 0;void sys_err(char *str) {perror(str);exit(1); }void do_sig_child(int num) {printf("i am child %d\t%d\n",getpid(), n);n += 2;flag = 1;sleep(1); }void do_sig_parent(int num) {printf("i am parent %d\t%d\n",getpid(), n);n += 2;flag = 1;sleep(1); }int main(void) {pid_t pid;struct sigaction act;if((pid = fork())<0)sys_err("fork");else if(pid > 0){n = 1;sleep(1);//子進程可以注冊完信號act.sa_handler = do_sig_parent;sigemptyset(&act.sa_mask);act.sa_flags = 0;//注冊自己的信號捕捉函數 父進程使用sigusr2信號sigaction(SIGUSR2, &act, NULL);do_sig_parent(0);while(1){if(flag == 1){//父進程數數完畢kill(pid, SIGUSR1);flag = 0;//標志已經給子進程發送完信號}}}else if(pid == 0){n = 2;act.sa_handler = do_sig_child;sigemptyset(&act.sa_mask);act.sa_flags = 0;sigaction(SIGUSR1, &act, NULL);while(1){if(flag == 1){//進程數數完畢kill(pid, SIGUSR2);//如果此處失去cpu,且父進程已經處理完并發送來信號,子進程數完數不會再次發送信號,而是執行下面的flag=0;flag = 0;//標志已經給父進程發送完信號}}}return 0; }? 例子通過flag變量標記進程執行進度。flag置1表述處理完信號(數數+2)。flag置0表示給對方發送信號完成。
? 問題出現的位置,在父、子進程kill函數之后需要緊接著調用flag,復位0,標記信號已經發送。但是,這期間有可能被內核調度,失去執行權力,而對方獲取了執行時間,通過發送信號回調捕捉函數,從而修改了全局flag。
? 避免全局變量,在多個時序中進行全局變量進行修改。可在回調函數中發送信號。
可/不可重入函數
? 一個函數在被調用執行期間(尚未調用結束),由于某種時序又被重復調用,稱之為“重入“(類似遞歸)。根據函數實現的方法可分為”可重入函數“和”不可重入函數“兩種。
使用了靜態數據結構
調用了 malloc 和 free
標準的I/O函數
SIGCHLD信號
SIGCHLD的產生條件
? 子進程終止時
? 子進程收到SIGSTOP信號停止時
? 子進程處在停止態,接收到SIGCONT后喚醒時
借助SIGCHLD信號回收子進程
? 子進程結束運行,其父進程會收到SIGCHLD信號。該信號的默認處理動作是忽略。可以捕捉該信號,在捕捉函數中完成子進程狀態的回收。
#include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<signal.h> #include<errno.h> #include<sys/types.h> #include<sys/wait.h>void sys_err(char *str) {perror(str);exit(1); } //一次捕捉,回收所有死亡進程 void do_sig_child(int signo) {int status;pid_t pid;// if((pid = waitpid(0, &status, WNOHANG)) > 0){//可能同時死亡,只有一個信號while((pid = waitpid(0, &status, WNOHANG)) > 0){//盡量循環調用if(WIFEXITED(status))printf("-----------child %d exit %d \n",pid, WEXITSTATUS(status));else if(WIFSIGNALED(status))printf("child %d cancel signal %d\n",pid, WTERMSIG(status));} }int main(void) {pid_t pid;int i;//阻塞SIGCHLDfor(i = 0; i < 10; i++){if((pid = fork()) == 0){break;}else if(pid < 0)sys_err("fork");}if(0 == pid){int n = 1;while(n--){printf("child ID %d\n", getpid());sleep(1);}return i + 1;}else if(pid > 0){//阻塞SIGCHLDstruct sigaction act;act.sa_handler = do_sig_child;sigemptyset(&act.sa_mask);act.sa_flags = 0;sigaction(SIGCHLD, &act, NULL);//解除SIGCHLD阻塞while(1){printf("parent id %d \n",getpid());sleep(1);}}return 0; } #include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<signal.h> #include<errno.h> #include<sys/wait.h> #include<string.h> void do_wait(int signo) {pid_t pid;int status;while((pid = (waitpid(0, &status, WNOHANG)))>0){printf("-----------------wait child \n");if(WIFEXITED(status)){printf("child exit with %d/n", WEXITSTATUS(status));}else if(WIFSIGNALED(status)){printf("child killed by %d/n", WTERMSIG(status));}} }int main(void) {pid_t pid;int fd[2];pipe(fd);pid = fork();if(0 == pid){close(fd[1]); //子進程從管道中讀數據,關閉寫端// dup2(fd[0], STDIN_FILENO); //讓wc從管道中讀數據char bufout[256] = {'0'};int read_ret = read(fd[0],bufout,256);if(read_ret == -1){perror("write error!");exit(-1);}else{printf("read %d bytes :%s\n",read_ret,bufout);}return 0;//execlp("wc", "wc", "-l, NULL");//wc命令默認從標準讀入取數據}else{struct sigaction act;char bufin[256] = {"hello child"};act.sa_handler = do_wait;sigemptyset(&act.sa_mask);act.sa_flags = 0;sigaction(SIGCHLD, &act, NULL);close(fd[0]);//父進程向通道中寫數據,關閉讀端// dup2(fd[1], STDOUT_FILENO);write(fd[1],bufin,strlen(bufin));//execlp("ls", "ls", NULL);//ls輸出結果默認對應屏幕sleep(5);}return 0; }信號傳參
發送信號傳參
sigqueue函數對應kill函數,但可向指定進程發送信號的同時攜帶參數
int sigqueue(pid_t pid, int sig, const union sigval value); union sigval{int sival_int;void *sival_ptr; }? pid :要發送進程的進程ID
? sig :要發送的信號
? value:攜帶的數據
返回:成功:0;失敗:-1,設置errno;
? 向指定進程發送指定信號的同時,攜帶參數。但,如傳地址,需注意,不同進程之間虛擬地址空間各自獨立,將當前進程地址傳遞給另一進程沒有實際意義(可以應用在給本進程發送信號)。
捕捉函數傳參
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); struct sigaction{void (*sa_handler)(int);void (*sa_sigaction)(int, siginfo_t*, void*);sigset_t sa_mask;int sa_flags;void (*sa_restorer)(void); };? 當注冊信號捕捉函數,希望獲得更多信號相關信息,不應使用sa_handler而應該使用sa_sigaction。但此時的sa_flags必須指定為SA_SIGINFO。siginfo_t是一個成員十分豐富的結構體類型,可以攜帶各種與信號相關的數據。
中斷系統調用
系統調用可分為兩類:慢速系統調用和其他系統調用。
結合pause,回顧慢速系統調用:
? 慢速系統調用被中斷的相關行為,實際上就是pause的行為:如,read
1. 想中斷pause,信號不能被屏蔽 1. 信號的處理方式必須時捕捉(默認和忽略都不可以) 1. 中斷后返回-1,設置errno為eintr(表示“被信號中斷”)? 可修改sa_flags參數來設置被信號中斷后系統調用是否重啟。SA_INTERRURT不重啟。SA_RESTART重啟。
? sa_flags有許多可選參數,適用于不同情況。如:捕捉到信號后,在執行捕捉函數期間,不希望自動阻塞該信號,可將sa_flags設置為SA_NODEFER,除非sa_mask中包含該信號。
4.守護進程
終端
? 輸入輸出設備總稱
? 在unix系統中,用戶通過終端登錄系統后得到一個shell進程,這個終端稱為shell進程的控制終端,在進程中,控制終端是保存在pcb中的信息,而fork會復制pcb中的信息,因此由shell進程啟動的其他進程的控制終端也是這個終端。默認情況下(沒有重定向),每個進程的標準輸入輸出和標準錯誤輸出都指向控制終端,進程從標準輸入讀也就是用戶的鍵盤輸入,進程往標準輸出和錯誤輸出寫也就是輸出到顯示器上。信號中,在控制終端輸入一些特殊控制鍵可以給前臺進程發信號,如ctrl + c 表示SIGINT。
? Alt+Ctl+F1、F2、F3、F4、F5、F6 字符終端
? pts(pseudo terminal slave)指偽終端
? Alt+F7 圖形終端
? SSH Telnet 網絡終端
終端的啟動流程
? 每個進程都可以通過一個特殊的設備文件/dev/tty 訪問他的控制終端。事實上每個終端設備都對應一個不同的設備文件,/dev/tty提供了一個通用的接口,一個進程要訪問他的控制終端既可以通過/dev/tty,也可以通過該終端設備對應的設備文件來訪問。ttyname函數可以由文件描述符查出對應的文件名,該文件描述符必須指向一個終端設備而不能是任意文件。
? 其步驟如下:
? init→fork→exec→getty→用戶輸入賬號→login→輸入密碼→exec→bash
? 硬件驅動程序負責讀寫實際的硬件設備,比如從鍵盤讀入字符和把字符輸出到顯示器,線路規程(line disciline,用來過濾鍵盤輸入的內容)像一個過濾器,對于某些特殊字符并不是讓他直接通過,而是做特殊處理,比如在鍵盤上按下ctrl + z,對應的字符并不會被用戶程序的read讀到,而是被線路規程截獲,解釋成SIGTSTP信號發送給前臺進程,通常會使該進程停止。線路規程應該過濾哪些字符和哪些特殊處理是可以配置的。
ttyname
? 由文件描述符查出對應的文件名
? char *ttyname(int fd); 成功:終端名稱;失敗NULL,設置errno
借助ttyname查看不同終端設備的名稱:
#include<stdio.h> #include<unistd.h>int main(void) {printf("fd 0:%s\n",ttyname(0));//printf("fd 1:%s\n",ttyname(1));printf("fd 2:%s\n",ttyname(2));return 0; }網絡終端
? 虛擬終端或串口終端的數目是有限的,虛擬終端(字符控制終端)一般就是/dev/tty1 - /dev/tty6六個,串口終端的數目也不超過串口的數據。然而網絡終端或圖形終端窗口的數目卻是不受限制的,這是通過偽終端(Pseudo TTY)實現的。一套偽終端由一個主設備(PTY Master)和一個從設備(PTY Slave)組成。主設備在概念上相當于鍵盤和顯示器,只不過他不是真正的硬件而是一個內核模塊,操作它的也不是用戶而是另外一個進程。從設備和上面介紹的/dev/tty1 這樣的終端設備模塊類似,只不過它的底層驅動程序不是訪問硬件,而是訪問主設備。網絡終端或圖形終端窗口的shell進程以及它啟動的其他進程都會認為自己控制終端是偽終端從設備。例如/dev/pts/0、/dev/pts/1等。
? TCP/IP協議棧:在數據包上添加報頭。
? 如果telnet客戶端和服務器之間的網絡延遲較大,我們會觀察到按下一個鍵之后要過幾秒鐘才能回顯到屏幕上。這說明我們沒按一個鍵,telnet客戶端都會立刻把該字符發送到服務器,然后這個字符經過偽終端主設備和從設備之后被shell進程讀取,同時回顯到偽終端從設備,回顯字符再經過偽終端主設備、telnet服務器和網絡發回給telnet客戶端,顯示給用戶看。每一個按鍵都會走一個來回。
進程組
? 進程組,也叫作業。代表一個或多個進程的集合。每個進程都屬于一個進程組。在waitpid函數和kill函數的參數中都可以使用。操作系統設計進程組的概念,是為了簡化對多個進程的管理。
? 當父進程,創建子進程的時候,默認子進程與父進程屬于同一進程組。進程組ID等于第一個進程ID(組長進程)。所有,組長進程標識:其進程組ID==其進程ID。
? 可以使用kill -SIGKILL -進程組ID(負數)來將整個進程組內的進程全部殺死。
? 組長進程可以創建一個進程組,創建該進程組中的進程,然后終止。只要進程組中有一個進程存在,進程組就存在,與組長進程是否終止無關。
? 進程組生存期:進程組創建到最后一個進程離開(終止或轉移到另一個進程組)。
? 一個進程可以為自己或子進程設置進程組ID。
進程操作函數
getpgrp
? 獲取當前進程的進程組ID
? pid_t getpgrp(void);總是返回調用者的進程組ID。
getpgid
? 獲取指定進程的進程組ID
? pid_t getpgid(pid_t pid);成功:0;失敗:-1,設置errno
? 如果pid=0,那么該函數作用和getpgrp一樣
setpgid
? 改變進程默認所屬的進程組。通常可用來加入一個現有的進程組或創建一個新的進程組。
? int setpgid(pid_t pid,pid_t pgid);成功:0;失敗:-1,設置errno
將參數1對應的進程,加入參數2對應的進程組中。
注意:
1. 如改變子進程為新的組,應forkhou,exec前。 2. 權級問題。非root進程只能改變自己創建的子進程,或有權限操作的進程(如自己)。 #include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<signal.h> #include<errno.h> #include<sys/types.h> #include<sys/wait.h>void sys_err(char *str) {perror(str);exit(1); }int main(int argc, char *argv[]) {pid_t pid;if(argc < 2){printf("./a.out num\n");return 0;}int count = atoi(argv[1]);int i = 0;//阻塞SIGCHLDfor(i = 0; i < count; i++){if((pid = fork()) == 0){break;}else if(pid < 0)sys_err("fork");}if(0 == pid){printf("child ID %d,parent ID %d,group id %d\n", getpid(),getppid(),getpgid(0));sleep(2);}else if(pid > 0){printf("parent id %d,parent ID %d,group id %d \n",getpid(),getppid(),getpgid(0));sleep(4);pid_t cpid;while ((cpid = wait(NULL))>0){printf("child %d is over\n",cpid);}}return 0; }會話
可以理解一組進程組為會話。
創建會話
注意事項:
getsid:
獲取進程所屬的會話ID
? pid_t getsid(pid_t pid);成功:返回調用進程的會話ID;失敗:-1,設置errno
pid為0表示查看當前進程session ID
ps ajx命令查看系統中的進程。參數a表示不僅列當前用戶的進程,也列出所有其他用戶的進程,參數x表示不僅列出有控制終端的進程,也列出所有無控制終端的進程,參數j表示列出與作業控制相關的信息。
組長進程不能成為新會話首進程,新會話首進程必定會成為組長進程。
setsid
創建一個會話,并以自己的ID設置進程組ID,同時也是新會話的ID。
? pid_t setsid(void):成功,返回調用進程的會話ID,失敗:-1,設置errno
調用了setsid函數的進程,即使新的會長,也是新的組長。
#include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<signal.h> #include<errno.h> #include<sys/types.h> #include<sys/wait.h>void sys_err(char *str) {perror(str);exit(1); }int main(void) {pid_t pid;if((pid = fork()) < 0){sys_err("fork");}else if(pid == 0){sleep(2);printf("child id %d,group id %d,sid %d\n",getpid(),getpgrp(),getsid(0));setsid();printf("child id %d,group id %d,sid %d\n",getpid(),getpgrp(),getsid(0));}return 0; }守護進程
? Daemon(精靈)進程,是linux中的后臺服務進程,通常獨立于控制終端并且周期性的執行某種任務或等待處理某些發生的事件。一般采用以d結尾的名字。
? linux后臺的一些系統服務進程,沒有控制終端,不能直接和用戶交互。不受用戶登錄、注銷的影響,一直在運行著,他們都是守護進程。如:預讀入緩輸出機制的實現;ftp服務器;nfs服務器等。
? 創建守護進程,最關鍵的一步是調用setsid函數創建一個新的session,并稱為session leader。
創建守護進程模型
所有工作在子進程中進行,形式上脫離了控制終端
setsid()函數
使子進程完全獨立出來,脫離控制
chdir()函數
防止占用可卸載的文件系統
也可以換成其他路徑
umask()函數
防止繼承的文件創建屏蔽字拒絕某些權限
繼承的打開文件不會用到,浪費系統資源,無法卸載
.bashrc修改設置程序自動啟動。
5.線程
線程的概念
什么是線程
? LWP:light weight process 輕量級的進程,本質仍然是進程(在linux環境下)
? 進程:獨立地址空間,擁有PCB
? 線程:也有PCB,但沒有獨立的地址空間(共享)
? 區別:在于是否共享地址空間 獨居(進程);合租(線程)
? linux下:線程:最小的執行單位
? 進程:最小的分配資源單位,可看成是只有一個線程的進程。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ukl7IkYQ-1639009614674)(G:\學習資料\視頻\linux服務器開發二-系統編程視頻\day06\day07.zip)]
linux內核線程實現原理
? 類似unix系統中,早期沒有線程的概念,80年代引入,借助進程機制實現了線程的概念。因此在這類系統中,進程和線程關系密切。
查看lwp號(劃分給線程時間片的依據):ps -L pid查看指定線程的lwp號。線程ID和線程號不同。
ps -Lf 進程ID 查看進程內的線程。
線程共享資源
線程非共享資源
線程優缺點
優點:
缺點:
總結:
? 優點相對突出,缺點不是硬傷。linux下由于實現方法導致進程、線程差別不是很大。
線程控制原語
pthread_self
獲取線程ID。其作用對應進程中getpid()函數。
pthread_t pthread_self(void);返回值:成功:0;失敗:無
線程ID:pthread_t類型,本質:在linux下為無符號整數(%lu),其他系統中可能是結構體實現。
線程ID是進程內部,識別標志。(兩個進程間,線程ID允許相同)
注意:不應使用全局變量pthread_t tid,在子線程中通過pthread_create傳出參數來獲取線程ID,而應使用pthread_self。gcc編譯時,應使用-pthread參數調用線程庫。
pthread_create
創建一個新線程。其作用,對應進程中的fork()函數。
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *( *start_routine)(void * ),void * arg);返回值:成功:0;失敗:錯誤號。linux環境下,所有線程特點,失敗均直接返回錯誤號。
參數:
? pthread_t: 當前linux中可理解為 typedef unsigned long int pthread_t;
? 參數1:傳出參數,保存系統為我們分配好的線程ID
? 參數2:通常傳NULL,表示使用線程默認屬性。若想使用具體屬性,可修改該參數。
? 參數3:函數指針,指向線程主函數(線程體),該函數運行結束,則線程結束。
? 參數4:線程主函數執行期間所使用的參數。
#include<stdlib.h> #include<stdio.h> #include<pthread.h> #include<unistd.h>void * thread_func(void* arg) {printf("in thread, thread_id = %lu, pid=%d\n",pthread_self(),getpid());return NULL; }int main() {pthread_t tid;int ret;printf("in main, thread_id = %lu, pid=%d\n",pthread_self(),getpid());ret = pthread_create(&tid,NULL,thread_func,NULL);if(ret != 0){printf("pthread_create error=%s\n",strerror(ret));perror("pthread_create");exit(1);}sleep(1);printf("in main, thread_id = %lu, pid=%d,son thread ID=%lu\n",pthread_self(),getpid(),tid);return 0; }編譯:gcc creat.c -o creat -pthread
線程默認共享數據段、代碼段等地址空間,常用的全局變量。而進程不共享全局變量,只能借助mmap。
#include<stdlib.h> #include<stdio.h> #include<pthread.h> #include<unistd.h>int var = 100; void * thread_func(void* arg) {var = 200;int i = (int)arg;//*((int *)arg) 此種情況回去主函數地址中取i值,i的值不定printf("in thread %d, thread_id = %lu, pid=%d\n",i,pthread_self(),getpid());return NULL; }int main() {pthread_t tid;int ret,i;for( i = 0; i < 5 ; i++){ret = pthread_create(&tid,NULL,thread_func,(void *)i);//(void *)&iif(ret != 0){perror("pthread_create");exit(1);}}sleep(1);printf("in main, var = %d\n",var);return 0; }pthread_exit
將當個線程退出
void pthread_exit(void *retval);
參數:retval表示線程退出狀態,通常傳NULL
線程中,禁止使用exit函數,會導致進程內所有線程全部退出。
? 在不添加sleep控制輸出順序情況下。pthread_create在循環中,近乎瞬間創建了五個線程,但只有一個線程有機會輸出(或者兩個,也可能沒有,取決于內核調度)如果第三個線程執行了exit,將整個進程退出了,全部的線程就退出了。
#include<stdlib.h> #include<stdio.h> #include<pthread.h> #include<unistd.h>void * thread_func(void* arg) {int i = (int)arg;printf("in thread %d, thread_id = %lu, pid=%d\n",i,pthread_self(),getpid());return NULL;//退出線程,類似 pthread_exit(NULL)// pthread_exit(NULL);//退出線程 }int main() {pthread_t tid;int ret,i;for( i = 0; i < 5 ; i++){ret = pthread_create(&tid,NULL,thread_func,(void *)i);if(ret != 0){perror("pthread_create");exit(1);}}printf("in main thread_id = %lu, pid=%d\n",pthread_self(),getpid());pthread_exit((void*)1);//main所在的線程退出// return 0;//h會使主控進程退出//exit(1);//也是退出進程,在其他線程中也會退出整個進程 }? 多線程環境中,應盡量少用,或者不適用exit函數,取而代之使用pthread_exit函數,將單個線程退出。任何線程里exit導致進程退出,其他線程未工作結束,主控線程退出時不能return或exit。
? pthread_exit或者return返回的指針所指向的內存單元必須時全局的或者是用malloc分配的,不能在線程函數的棧上分配,因為當其他線程得到這個返回指針時,線程函數已經退出了。
? return:返回到調用者那里去。
? pthread_exit():將調用該函數的線程退出
? exit:將進程退出。
pthread_join
? 阻塞等待線程推出,獲取線程退出狀態。他的作用對應進程中的waitpid()函數。
int pthread_join(pthread_t thread, void ** retval);成功:0;失敗:錯誤號
參數:thread:線程ID(不是指針);retval:存儲線程結束狀態。
對比記憶:
? 進程中:main返回值、exit參數–>int;等待子進程結束wait函數參數–>int*
? 線程中:線程主函數返回值、pthread_exit–>void*;等待線程結束pthread_join函數參數–>void **
調用該函數的線程將掛起等待,知道id為thread的線程終止。通過pthread_join得到的終止狀態是不同的,總結如下:
1. 如果thread線程通過return返回,retval所指向的單元里存放的是thread線程函數的返回值。 1. 如果thread線程被別的線程調用pthread_cancel異常終止掉,retval所指向的單元里存放的是常數PTHREAD_CANCLED 1. 如果pthread線程自己調用pthread_exit終止,retval所指向的單元里存放的是傳給pthread_exit的參數。 1. 如果thread線程的終止狀態不感興趣,可以傳NULL給retval參數。 1. 也可以在兄弟線程中回收其他線程 #include<stdlib.h> #include<stdio.h> #include<pthread.h> #include<unistd.h>typedef struct{int a;int b; }exit_t;void * tfn(void* arg) {exit_t *ret;ret = malloc(sizeof(exit_t));ret->a = 100;ret->b = 300;pthread_exit((void *)ret); }void * tfn2(void* arg) {exit_t *ret = (exit_t *)arg;ret->a = 100;ret->b = 300;pthread_exit((void *)ret); }int main(void) {pthread_t tid;exit_t *retval;pthread_create(&tid,NULL,tfn,NULL);pthread_join(tid, (void **)&retval);printf("a = %d, b=%d\n",retval->a,retval->b);free(retval);exit_t * ret = malloc(sizeof(exit_t));pthread_create(&tid,NULL,tfn2,(void *)ret);pthread_join(tid, (void **)&ret);printf("tfn2 a = %d, b=%d\n",ret->a,ret->b);free(ret);return 0; }pthread_detach
實現線程分離
int pthread_detach(pthread_t thread);成功:0;失敗:錯誤號
? 線程分離狀態:指定該狀態,線程主動與主控線程斷開關系。線程結束后,其退出狀態不由其他線程獲取,而直接自己自動釋放。網絡、多線程服務器常用。
? 進程如果有該機制,將不會產生僵尸進程。僵尸進程的產生主要由于進程死后,大部分資源被釋放,一點殘留資源仍然存在于系統中,導致內核認為該進程仍存在。
? 也可以使用pthread_create 函數參數2(線程屬性)來設置線程分離。
? 一般情況下,線程終止后,其終止狀態一直保留到其他線程調用pthread_join獲取他的狀態為止。但是線程也可以被置為detach狀態,這樣的線程一旦終止就立刻回收它占用的所有資源,而不保留終止狀態。不能對一個已經處于detach狀態的線程調用pthread_join,這樣的調用將返回einval錯誤。也就是說,如果已經對一個線程調用了pthread_detach就不能再調用pthread_join了。
#include<stdlib.h> #include<stdio.h> #include<pthread.h> #include<unistd.h> #include<string.h>void * tfn(void* arg) {int n = 3;while(n--){printf("thread count %d\n",n);sleep(1);}return (void *)1; }int main(void) {pthread_t tid;void * tret;int err;#if 0pthread_attr_t attr;pthread_attr_init(&attr);pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);pthread_create(&tid, &attr, tfn,NULL); #elsepthread_create(&tid, NULL, tfn, NULL);pthread_detach(tid); #endifwhile(1){err = pthread_join(tid,&tret);printf("----------------------err=%d\n",err);if(err != 0)fprintf(stderr,"thread exit %s \n",strerror(err));elsefprintf(stderr,"thread exit code %d\n",(int)tret);sleep(1);}return 0; }優點:可以自動清理pcb。
pthread_cancel
殺死(取消)線程,其作用,對應進程中的kill()函數
int pthread_cancel(pthread_t thread); 成功:0;失敗:錯誤號
? 線程的取消不是實時的,而有一定的延時。需要等待線程到達某個取消點(檢查點)。
? 類似于玩游戲必須存檔,必須到達指定的場所才能存儲進度。殺死線程也不是立刻就能完成,必須要到達取消點。
? 取消點:是線程檢查是否被取消,并按請求進行動作的一個位置。通常是一些系統調用creat,open,pause,close,read,write…執行命令man 7 pthreads可以查看具備這些取消點的系統調用列表。也可參閱APUE.12.7取消選項小節。
? 可粗略認為一個系統調用(進入內核)即為一個取消點。如線程中沒有取消點,可以通過pthread_testcancel函數自行設置幾個取消點。
? 被取消的線程,退出值定義在linux的pthread庫中。常數PTHREAD_CANCELED的值是-1.頭文件中pthread.h中他的定義:``define PTHREAD_CANCELED((void *)-1)。因此我們對一個已經被取消的線程使用pthread_join回收時,得到的返回值是-1。
#include<stdlib.h> #include<stdio.h> #include<pthread.h> #include<unistd.h>void * tfn(void* arg) {printf("thread 1 return\n");return (void *)111; }void * tfn2(void* arg) {printf("thread 2 return\n");pthread_exit((void *)222); }void * tfn3(void* arg) {while (1){printf("thread 3 : gonging to die\n");sleep(1);//以上兩句屏蔽后,線程不會被殺死// pthread_testcancel();//函數內如果不存在取消點,可手動加上這個取消點}return (void*)666; }int main(void) {pthread_t tid;exit_t *retval;pthread_create(&tid,NULL,tfn,NULL);pthread_join(tid, (void **)&retval);printf("pthread1 return=%d\n",(int)retval);pthread_create(&tid,NULL,tfn2,NULL);pthread_join(tid, (void **)&retval);printf("pthread2 return=%d\n",(int)retval);pthread_create(&tid,NULL,tfn2,NULL);sleep(3);pthread_cancel(tid);pthread_join(tid, (void **)&retval);printf("pthread3 return=%d\n",(int)retval);return 0; }pthread_equal
比較兩個線程ID是否相等
int pthread_equal(pthread_t t1,pthread_t t2);
有可能linux在未來線程ID pthread_t 類型被修改為結構體實現。
控制原語對比
進程 線程
fork pthread_create
exit pthread_exit
wait pthread_join
kill pthread_cancel
getpid pthread_self
線程屬性
? linux下線程的屬性時可以根據實際項目需要,進行設置,之前我們討論的線程都是采用線程的默認屬性,默認屬性已經可以解決絕大多數開發時遇到的問題。如果我們對程序的性能提出更高的要求,那么需要設置線程屬性,比如 可以通過設置線程棧的大小來降低內存的使用,增加最大線程個數。
typedef struct {int etachstate; //線程的分離狀態int schedpolicy; //線程調度策略struct sched_param schedparam; //線程的調度參數int inheritsched; //線程的繼承性int scope; //線程的作用域size_t guardsize; //線程棧末尾的警戒緩沖區大小int stackaddr_set; //線程的棧設置void* stackaddr; //線程棧的位置size_t stacksize; //線程棧的大小 }pthread_attr_t;主要結構體成員:
? 屬性值不能直接設置,必須使用相關函數進行操作,初始化的函數為 pthread_attr_init,這個函數必須在 pthread_create 函數之前調用。之后需用 pthread_attr_destory 函數來釋放資源。
? 線程屬性主要包括如下屬性:作用域(scope)、棧尺寸(stack size)、棧地址(stack address)、優先級(priority)、分離的狀態(detached state)、調度策略和參數(scheduling policy and parameters)。默認的屬性為非綁定、非分離、缺省的堆棧、與父進程同樣級別的優先級。
線程屬性初始化
注意:應先初始化線程屬性,再pthread_create創建線程
初始化線程屬性
int pthread_attr_init(pthread_attr_t *attr); 成功:0;失敗:錯誤號
銷毀線程屬性所占用的資源
int pthread_attr_destroy(pthread_attr_t *attr); 成功:0;失敗:錯誤號
線程的分離狀態
? 線程的分離狀態決定了一個線程以什么樣的方式來終止自己。
? 非分離狀態:線程的默認屬性是非分離狀態,這種情況下,原有的線程等待創建線程結束。只有當pthread_join()函數返回時,創建的線程才算終止,才能釋放自己占用的系統資源。
? 分離狀態:分離線程沒有被其他的線程所等待,自己運行結束了,線程也就終止了,馬上釋放系統資源。應該根據自己的需要,選擇適當的分離狀態。
? 線程分離狀態的函數:
? 設置線程屬性,分離or非分離
? int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
? 獲取線程屬性,分離or非分離
? int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate);
? 參數:attr:已初始化的線程屬性
? detachstate:PTHREAD_CREATE_DETACHED(分離線程)PTHREAD_CREATE_JOINABLE(結合)
? 這里需要注意的一點是,如果設置一個線程為分離線程,而這個線程運行又非常快,他很可能在pthread_create函數返回之前就終止了,他終止以后就可能將線程號和系統資源移交給其他的線程使用,這樣調用pthread_create的線程就得到了錯誤的線程號。要避免這種情況可以采取一定的同步措施,最簡單的方法之一是可以在被創建的線程里調用pthread_cond_timedwait函數,讓這個線程等待一會,留出足夠的時間讓函數pthread_create返回。設置一段等待時間,是在多線程編程里常用方法。但是注意不要使用諸如wait()之類的函數,他們是使整個進程睡眠,并不能解決同步線程的問題。
線程的棧地址
? POSIX.1定義了兩個常量_POSIX_THREAD_ATTR_STACKADDR 和 _POSIX_THREAD_ATTR_STACKSIZE 檢測系統是否支持棧屬性。也可以給sysconf函數傳遞_SC_THREAD_ATTR_STACKADDR 或 _SC_THREAD_ATTR_STACKSIZE來進行檢測。
? 當進程棧地址空間不夠用時,指定新建線程使用由malloc分配的空間作為自己的棧空間。通過pthread_attr_setstack 和 pthread_attr_getstack 兩個函數分別設置和獲取線程的棧地址。
? int pthread_attr_setstack(pthread_attr_t *attr, void * stackaddt, size_t stacksize);成功:0;失敗:錯誤號;
? int pthread_attr_getstack(pthread_attr_t *attr, void **stackaddr, size_t *stacksize);成功:0;失敗錯誤碼;
參數:
? attr: 指向一個線程屬性的指針
? stackaddr:返回獲取的棧地址
? stacksize:返回獲取的棧大小。
線程的棧大小
? 當系統中有很多線程時,可能需要減少每個線程棧的默認大小,防止進程地址空間不夠用,當線程調用的函數會分配很大的局部變量或者函數調用層次很深時,可能需要增加線程棧默認大小。
? 函數 pthread_attr_getstacksize 和 pthread_attr_setstacksize 提供設置。
? int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize); 成功:0;失敗:錯誤號
? int pthread_attr_getstacksize(pthread_attr_t *attr, size_t stacksize); 成功:0;失敗:錯誤號
? 參數:
? ? attr:指向一個線程屬性的指針
? ? stacksize:返回線程的堆棧大小
NPTL
1. 查看當前 pthread 庫版本 `getconf GNU_LIBPTHREAD_VERSION` 2. NPTL 實現機制(POSIX),Native POSIX Thread Library 3. 使用線程庫時gcc指定 -lpthread線程注意事項
1. 主線程退出其他線程不退出,主線程應調用 pthread_exit 2. 避免僵尸線程? ? ? pthread_join
? ? ? pthread_detach
? ? ? pthread_create指定分離屬性
? ? ? 被join線程可能在join函數返回前釋放完自己的所有內存資源,所以不應當返回回收線程中的值;
? 3. malloc和mmap 申請的內存可以被其他線程釋放
? 4. 應避免在多線程模型中調用fork,除非馬上exec,子進程中只有調用fork的線程存在,其他線程在子進程中均為pthread_exit;
? 5. 信號的復雜語義很難和多線程共存,應避免在多線程引入信號機制
線程同步
同步概念
? 所謂同步,即同時起步,協調一致。不同的對象,對同步的理解方式略有不同。如,設備同步,是指在兩個設 備之間規定一個同步的時間參考;數據庫同步,是指讓兩個或多個數據庫內容保持一致,或者按照需要部分一致。
? 在編程和通信中所說的同步,指協同、協助、互相配合。主旨在系統步調,按預定的先后次序運行。
線程同步
? 同步即協同步調,按預定的先后次序運行。
? 線程同步,指一個線程發出某一功能調用時,在沒有得到結果之前,該調用不返回。同時其他線程為保證數據一致性,不能調用該功能。
? 競爭方式訪問共享資源。
? 產生的現象叫做“與時間有關的錯誤”。為了避免這種數據混亂,線程需要同步。
? 同步的目的,是為了避免數據混亂,解決與時間有關的錯誤。實際上,不僅線程間需要同步,進程間、信號間都需要同步機制。
? 所有多個控制流,共同操作一個共享資源的情況,都需要同步
數據混亂原因
? 以上3點鐘,前兩點不能改變,想要提高效率,傳遞數據,資源必須共享。只要共享資源,就一定會出現競爭。只要存在競爭關系,數據就很容易出現混亂。所以只能從第三點著手,使多個線程在訪問共享資源的時候,出現互斥。
互斥量mutex(互斥鎖)
linux中提供一把互斥鎖mutex(也稱之為互斥量)。
每個線程在對資源操作前都嘗試先加鎖,成功加鎖才能操作,操作結束解鎖。
主要應用函數
? pthread_mutex_t 類型,其本質是一個結構體。為簡化理解,可以忽略實現細節,當成整數看待。
? pthread_mutex_t mutex;變量mutex只有兩種取值1 、 0;
pthread_mutex_init
? 初始化一個互斥鎖(互斥量),初值可看做1
int pthread_mutex_int(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t * restrict attr);
參數1:傳出參數,調用時應傳&mutex
? restrict關鍵字:只用于限制指針,告訴編譯器,所有修改該指針指向內存中的內容的操作,只能通過本指針完成。不能通過除本指針以外的其他變量或指針修改。
參數2:互斥量屬性。是一個傳入參數,通常傳NULL,選用默認屬性(線程間共享)。
pthread_mutex_destroy
銷毀一個互斥鎖
int pthread_mutex_destroy(pthread_mutex_t *mutex);
pthread_mutex_lock
加鎖。可理解為將mutex–(或者-1)
int pthread_mutex_lock(pthread_mutex_t *mutex)
pthread_mutex_unlock
解鎖。可理解為將mutex++(或者+1)
int pthread_mutex_unlock(pthread_mutex_t *mutex);
pthread_mutex_trylock
嘗試加鎖,不阻塞
int pthread_mutex_trylock(pthread_mutex_t *mutex);
加鎖與解鎖
lock與unlock
? lock嘗試加鎖,如果加鎖不成功,線程阻塞,阻塞到持有該互斥量的其他線程解鎖為止。
? unlock主動解鎖函數,同時將阻塞該鎖上所有線程全部喚醒,至于哪個線程先被喚醒,取決于優先級、調度。默認先阻塞,先喚醒。
? 可假想mutex鎖init成功初值為1 。lock功能時將mutex–。unlock將mutex++
lock與trylock
? lock加鎖失敗會阻塞,等待鎖釋放
? trylock加鎖失敗直接返回錯誤號(如,EBUSY),不阻塞。
加鎖步驟測試
#include<stdio.h> #include<stdlib.h> #include<pthread.h> #include<unistd.h>pthread_mutex_t mutex;void *tfn(void *arg){srand(time(NULL));while(1){pthread_mutex_lock(&mutex);printf("hello ");//模擬長時間操作共享資源,導致cpu易主,產生與時間有關的錯誤sleep(rand()%3);printf("world\n");pthread_mutex_unlock(&mutex);sleep(rand()%3);// pthread_mutex_unlock(&mutex);}return NULL; }int main(void){pthread_t tid;int flag = 5;srand(time(NULL));pthread_mutex_init(&mutex,NULL);//調用成功mutex = 1pthread_create(&tid, NULL, tfn, NULL);while(flag--){pthread_mutex_lock(&mutex);printf("HELLO ");sleep(rand()%3);printf("WORLD\n");pthread_mutex_unlock(&mutex);sleep(rand()%3);// pthread_mutex_unlock(&mutex);}pthread_cancel(tid);pthread_join(tid,NULL);pthread_mutex_destroy(&mutex);return 0; }//stdout 是共享資源線程在操作完共享資源后本應該立即解鎖,但修改后,線程抱著鎖睡眠。睡醒解鎖后又立即加鎖,這兩個庫函數本身不會阻塞。
所以在這兩行代碼之間失去cpu的概率很小。因此,另外一個線程很難得到加鎖的機會
結論:在訪問共享資源前加鎖,訪問結束后立即解鎖。鎖的“粒度”應越小越好。
死鎖
讀寫鎖
? 與互斥量類似,但讀寫鎖允許更高的并行性。其特征為:寫獨占,讀共享。
讀寫鎖狀態
讀寫鎖具備三種狀態:
讀寫鎖特性
? 讀寫鎖也叫共享-獨占鎖。當讀寫鎖以讀模式鎖住時,他是以共享模式鎖住的;當它以寫鎖模式鎖住時,他是以獨占模式鎖住的。寫獨占,讀共享。
? 讀寫鎖非常適合對數據結構讀的次數遠大于寫的情況。
主要應用函數
函數返回都是 成功0;失敗返回錯誤號。
pthread_rwlock_t 類型 用于定義一個讀寫鎖變量
pthread_rwlock_t rwlock;
pthread_rwlock_init
? 初始化一把讀寫鎖
int pthread_rwlock_init(pthread_rwlock_t * restrict rwlock, const pthread_rwlockattr_t *restrict attr);
? 參數2:attr表示讀寫鎖屬性,通常使用默認屬性,傳NULL即可。
pthread_rwlock_destroy
? 銷毀一把鎖
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
pthread_rwlock_rdlock
? 加讀鎖
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
pthread_rwlock_wrlock
? 加寫鎖
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
pthread_rwlock_unlock
? 解鎖
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
pthread_rwlock_tryrdlock
? 嘗試加讀鎖,不阻塞
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
pthread_rwlock_trywrlock
? 嘗試加寫鎖,不阻塞
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
讀寫鎖實例
#include<stdio.h> #include<stdlib.h> #include<pthread.h> #include<unistd.h>int counter; pthread_rwlock_t rwlock;void *th_write(void *arg){int t;int i = (int)arg;while(1){t = counter;usleep(1000);pthread_rwlock_wrlock(&rwlock);printf("write %d:%lu counter=%d ++counter=%d\n",i,pthread_self(),t,++counter);pthread_rwlock_unlock(&rwlock);usleep(5000);}return NULL; }void * th_read(void *arg){int i = (int)arg;while (1){pthread_rwlock_rdlock(&rwlock);printf("--------------read %d:%lu:%d\n",i,pthread_self(),counter);pthread_rwlock_unlock(&rwlock);usleep(900);}return NULL; }int main(void){pthread_t tid[8];int i;pthread_rwlock_init(&rwlock,NULL);for ( i = 0; i < 3; i++){pthread_create(&tid[i],NULL,th_write,(void *)i);}for ( i = 0; i < 5; i++){pthread_create(&tid[i+3],NULL,th_read,(void *)i);}for ( i = 0; i < 8; i++){pthread_join(tid[i],NULL);}pthread_rwlock_destroy(&rwlock);return 0; }條件變量
? 條件變量本身不是鎖,但它可以造成線程阻塞。通常與互斥鎖配合使用,給多線程提供一個回合的場所(共享數據)。
主要應用函數:
以下函數都是成功返回0,失敗直接返回錯誤號。
pthread_cond_t類型 用于定義條件變量。
pthread_cond_t cond;
pthread_cond_init
初始化條件變量
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
pthread_cond_destroy
銷毀條件變量
int pthread_cond_destroy(pthread_cond_t *cond);
pthread_cond_wait
阻塞等待一個條件變量
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t * restrict mutex);
函數作用
兩步為一個原子操作。
pthread_cond_timedwait
限時等待一個條件變量
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutext_t *restrict mutex, const struct timespec *restrict abstime);
參數3:
struct timespec{time_t tv_sec;//秒long tv_nsec;//納秒 }形參 abstime:絕對時間。
如:time(NULL)返回的就是絕對時間。而alarm(1)是相對時間,相對當前時間定時1秒鐘。
struct timespec t = {1,0};
pthread_cond_timedwait(&cond, &mutex, &t);只能定時到1970年1月1日00:00:01秒
正確用戶:
time_t cur=time(NULL); //獲取當前時間。 struct timespec t; //定義timespec結構體變量t t.tv_sec = cur+1; //定時1秒 pthread_cond_timedwait(&cond, &mutex, &t);還有另外一個時間類型
struct timeval{time_t tv_sec;//秒susecond_t tv_usec;//微妙 }pthread_cond_signal
喚醒至少一個阻塞在條件變量上的線程
int pthread_cond_signal(pthread_cond_t *cond);
pthread_cond_broadcast
喚醒所有阻塞在條件變量上的線程
int pthread_cond_broadcast(pthread_cond_t * cond);
生產者消費者條件變量模型
? 線程同步典型的案例即為生產者消費者模型,而借助條件變量來實現這一模型,是比較常見的一種方法。假定有兩個線程,一個模擬生產者行為,一個模擬消費者行為。兩個線程同時操作一個共享資源(一般稱之為匯聚),生產向其中添加產品,消費者從中消費掉產品。
#include<stdio.h> #include<stdlib.h> #include<pthread.h> #include<unistd.h>struct msg{struct msg*next;int num; };struct msg* head; struct msg* mp;//靜態初始化 一個條件變量和一個互斥量 pthread_cond_t has_product = PTHREAD_COND_INITIALIZER; pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;void *consumer(void *p) {for(;;){pthread_mutex_lock(&lock);while(head==NULL){//頭指針為空,說明沒有節點printf("start wait\n");pthread_cond_wait(&has_product, &lock);printf("begin gon\n");}mp = head;head = mp->next;//模擬消費掉一個產品printf("consume----%d\n",mp->num);free(mp);pthread_mutex_unlock(&lock);sleep(rand()%5);} }void *producer(void *p){for(;;){pthread_mutex_lock(&lock);mp = malloc(sizeof(struct msg));mp->num = rand()%1000 + 1;//模擬生產一個產品printf("produce ----%d\n",mp->num);mp->next = head;head = mp;pthread_mutex_unlock(&lock);pthread_cond_signal(&has_product);sleep(rand()%5);} }int main(void){pthread_t pid,cid;srand(time(NULL));pthread_create(&pid, NULL, producer, NULL);pthread_create(&cid, NULL, consumer, NULL);pthread_join(pid,NULL);pthread_join(cid,NULL);return 0; }//stdout 是共享資源? 在head為空時,pthread_cond_wait 解鎖,阻塞;當head有值時,生產線程發出pthread_cond_signal pthread_cond_wait 喚醒加鎖,向后執行。
? 執行截圖:
條件變量的優點
? 相較于mutex而言,條件變量可以減少競爭。
? 如果直接使用mutex,除了生產者、消費者之間要競爭互斥量以外,消費者之間也需要競爭互斥量,但如果匯聚(鏈表)中沒有數據,消費者之間競爭互斥鎖是無意義的。有了條件變量機制以后,只有生產者完成生產,才會引起消費者之間的競爭。提升了程序效率。
信號量
進化版互斥鎖
? 由于互斥鎖粒度比較大,如果我們希望在多個線程間對某一對象的部分數據進行共享,使用互斥鎖是沒有辦法實現的,只能將整個數據對象鎖住。這樣雖然達到了多線程操作共享數據時保證數據正確性的目的,卻在無形中導致線程的并發性下降。線程從并行執行,變成了串行執行。與直接使用單進程無異。
? 信號量,是相對折中的一種處理方式,既能保證同步,數據不混亂,又能提高線程并發。
主要應用函數
成功返回0,失敗返回-1,同時設置errno
sem_t 類型,本質仍是結構體。但應用期間可簡單看作為整數,忽略實現細節(類似于使用文件描述符)
sem_t sem; 規定信號量sem不能小于0,頭文件<semaphore.h>
信號量基本操作
sem_init
int sem_init(sem_t *sem, int pshared, unsigned int value);
參數:
sem:信號量
pshared:是否允許進程間同步;取0用于線程間;取非0用于進程間
value:指定信號量初值,最大線程數
sem_destroy
int sem_destroy(sem_t *sem);
銷毀信號量
sem_wait
int sem_wait(sem_t *sem);
加鎖,類比pthread_mutex_lock
sem_post
int sem_post(sem_t *sem);
將信號量++,同時喚醒阻塞在信號量上的線程,類比pthread_mutex_unlock
但是,由于sem_t的實現對用戶隱藏,所以所謂的++、–操作只能通過函數來實現,而不能直接++、–符號。
信號量的初值,決定了占用信號量的線程的個數。
sem_trywait
int sem_trywait(sem_t *sem);
嘗試對信號量加鎖–(與sem_wait的區別類比lock和trylock)
sem_timedwait
生產者消費者信號量模型
#include<stdio.h> #include<stdlib.h> #include<pthread.h> #include<unistd.h> #include<semaphore.h>#define NUM 5int queue[NUM]; //全局數組實現環形隊列 sem_t blank_number,product_number;//空格信號量,產品信號量void *consumer(void *p) {int i = 0;for(;;){sem_wait(&product_number); //消費者將產品--,為0則阻塞等待printf("-----donsume-----%d\n",queue[i]);queue[i]=0; //模擬消費一個產品sem_post(&blank_number); //將空格數量++i = (i+1)%NUM; // 借助下標實現環形sleep(rand()%3);} }void *producer(void *p){int i = 0;for(;;){sem_wait(&blank_number); //生產者將空格--,為0則阻塞等待queue[i] = rand()%1000 + 1; //模擬生產一個產品printf("-----Produce-----%d\n",queue[i]);sem_post(&product_number); //將產品數量++i = (i+1)%NUM; // 借助下標實現環形sleep(rand()%1);} }int main(void){pthread_t pid,cid;srand(time(NULL));sem_init(&blank_number,0,NUM);sem_init(&product_number,0,0);pthread_create(&pid, NULL, producer, NULL);pthread_create(&cid, NULL, consumer, NULL);pthread_join(pid,NULL);pthread_join(cid,NULL);sem_destroy(&blank_number);sem_destroy(&product_number);return 0; }//stdout 是共享資源進程間同步
互斥量mutex
? 進程間也可以使用互斥鎖,來達到同步的目的。但應在pthread_mutex_init初始化之前,修改其屬性為進程間共享。mutex的屬性修改函數主要有以下幾個。
主要應用函數
pthread_mutexattr_t mattr;//用于定義mutex鎖的屬性 int pthread_mutexattr_init(pthread_mutexattr_t *attr);//初始化一個mutex屬性對象 int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);//銷毀mutex屬性 對象 int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared);//修改mutex屬性 /* pshared取值:線程鎖:PTHREAD_PROCESS_PRIVATE(mutex的默認屬性即為線程鎖,進程間私有)進程鎖:PTHREAD_PROCESS_SHARED */進程間mutex實例
#include<stdio.h> #include<stdlib.h> #include<pthread.h> #include<unistd.h> #include<fcntl.h> #include<string.h> #include<sys/mman.h> #include<sys/wait.h>struct mt{int num;pthread_mutex_t mutex;pthread_mutexattr_t mutexattr; };int main(void){int i;struct mt *mm;pid_t pid; /*int fd = open("mttest",O_CREAT | O_RDWR,0777);ftruncate(fd, sizeof(*mm));mm = mmap(NULL,sizeof(*mm),PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);close(fd);unlink("mttest"); */mm = mmap(NULL, sizeof(*mm),PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANON, -1,0);memset(mm, 0, sizeof(*mm));pthread_mutexattr_init(&mm->mutexattr);pthread_mutexattr_setpshared(&mm->mutexattr,PTHREAD_PROCESS_SHARED);pthread_mutex_init(&mm->mutex,&mm->mutexattr);pid = fork();if(pid == 0){for(i = 0;i<10;i++){pthread_mutex_lock(&mm->mutex);(mm->num)++;printf("child--------------------num++ %d\n",mm->num);pthread_mutex_unlock(&mm->mutex);sleep(1);}}else if(pid > 0){for(i = 0; i < 10 ; i++){sleep(1);pthread_mutex_lock(&mm->mutex);mm->num +=2;printf("father ------------------num+=2 %d\n",mm->num);pthread_mutex_unlock(&mm->mutex);}wait(NULL);}pthread_mutexattr_destroy(&mm->mutexattr);pthread_mutex_destroy(&mm->mutex);munmap(mm,sizeof(*mm)); //釋放映射區return 0; }文件鎖
? 借助fcntl函數來實現鎖機制。操作文件的進程沒有獲得鎖時,可以打開,但無法執行read、write操作。
fcntl函數:獲取、設置文件訪問控制屬性。
int fcntl(int fd, int cmd,../ * arg * /);
參數2:
? F_SETLK(struct flock*); 設置文件鎖(trylock)
? F_SETLKW(struct flock*) 設置文件鎖(lock)w–>wait
? F_GETLK(struct flock*); 獲取文件鎖
參數3:
struct flock{short l_type; //鎖的類型:F_RDLCK F_WRLCK F_UNLCKshort l_whence; //偏移位置:SEEK_SET SEEK_CUR SEEK_ENDoff_t l_start; //起始偏移:1000off_t l_len; //長度:0表示整個文件加鎖pid_t l_pid; //持有該鎖的進程ID:(f_getlk only) }進程間文件鎖實例
#include<stdio.h> #include<stdlib.h> #include<pthread.h> #include<unistd.h> #include<fcntl.h> #include<string.h> #include<sys/stat.h> #include<sys/types.h>void sys_err(char *str){perror(str);exit(1); };int main(int argc, char *argv[]){int fd;struct flock f_lock;if(argc < 2){printf("./a.out filename\n");exit(1);}if((fd = open(argv[1],O_RDWR))<0)sys_err("open");f_lock.l_type = F_WRLCK; //寫鎖// f_lock.l_type = F_RDLCK; //讀鎖f_lock.l_whence = SEEK_SET; //開始位置f_lock.l_start = 0;f_lock.l_len = 0; //整個文件加鎖fcntl(fd, F_SETLKW, &f_lock);printf("set lock\n");sleep(10);f_lock.l_type = F_UNLCK; //解鎖fcntl(fd, F_SETLKW, &f_lock);printf("unlock\n");close(fd);return 0; }打開兩個終端,同時操作相同的文件,可以看到第一個終端打印unlock后,第二個終端菜打印set lock。
? 多線程共享文件描述符,而給文件加鎖,是通過修改文件描述符所指向的文件結構體中的成員變量來實現的。因此,多線程中無法使用文件鎖。
其他
在 vi 中對應的函數 shift + k,進入man文檔。
ps aux查看進程信息
ps ajx查看進程信息,包括組ID
雙向半雙工(像微信),雙向全雙工(像電話)
? 段錯誤追蹤,使用-g方式編譯文件,gdb方式運行調試程序,直接run,程序停留的地方就是方式錯誤的位置。(strcpy,沒有寫權限時會產生段錯誤).
strace ./a.out查看可執行程序,執行過程中使用的系統調用。
ls > out輸出重定向,輸入內容寫入到out文件中。
cat < t.c輸入重定向
文件存儲
? inode屬性,存放文件 存儲信息,包括大小、權限、類型、所有者、文件存儲指針地址(指向磁盤存儲的位置)。
? denty 目錄項,包括文件名,inode編號。
? 每次創建一個硬鏈接,則會創建一個denty,這些denty里的inode編號相同,指向同一個inode節點;
UNIX
1969 unix 肯湯姆森
? 商業:IBM、APPLE、惠普、sun
? linux:BSD–freeBSD、紅帽、debain-ubuntu
優化
從io入口,不打印到終端。
總結
以上是生活随笔為你收集整理的linux学习笔记 -- 系统编程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 产品经理需要掌握的十大知识模块
- 下一篇: 计算机网络的 89 个核心概念【转自微信