Linux系统编程
前言
- linux系統編程的坑,終于這里填完了,這里記錄一下。
- 推薦一個老師的課程:史上最強最細膩的linux嵌入式C語言學習教程【李慧芹老師】
- APUE以后工作中會用到嗎?不太會吧。
# 介紹
01什么是系統編程
金庫->銀行->辦事窗口(客戶)
系統編程就是利用系統調用提供的這些接口、或者說函數、去操作磁盤、終端、網絡等硬件。
系統調用:system call類比 銀行的辦事窗口
02系統編程的特點
03系統編程課程目錄
問你原理性的東西,不會問你那個參數是干什么的。
《 Linux:系統編程》的前置知識有《 Linux操作系統基礎》、《C語言程序設計》、《數據結構》
本課程將帶你一步一步學會在 Linux操作系統下編程,使用系統底層提供給我們的各種接口和函數,井深入內核,體驗系統底層的精妙之處。
《 Linux E網絡編程基礎》的前置課程是《 Linux:系統編程》,在本課程中,我們需要重點學習計算機網絡知識,特別是運輸層的TCP與UDP協議,網絡層的路由協議與IP協議。在學習了基礎的計算機網絡知識后,我們會從 socket入手,學習基于TCP和UDP的多種網絡通訊模型
# 文件與IO
學完本節課程后,同學將掌握文件的打開、關閉、讀寫,阻塞與非阻塞IO,同步1IO,文件系統,標準IO,流的打開、關閉與讀寫,控制緩沖,線程安全:對文件加鎖等內容
01 標準庫函數與系統調用
fopen
fgetc
File *stream ;就是句柄,就可以叫做上下文
fput
fclose
1.菜鳥驛站(帶緩沖區的)
2.一切皆文件,需要實時操作的內容最好直接使用系統調用
全緩沖
行緩沖: stdout是行緩沖
無緩沖:stderr是無緩沖
02 open/close/read/write
open
umask 一般設置為002
close
read
write
#include <unistd.h>ssize_t write(int fd, const void *buf, size_t count);阻塞與非阻塞
標準IO實現mycat
- 自行實現
標準IO實現mycp
- 自行實現
- rewind()返回到文件頭
- fseek():SEEK_SET/SEEK_CUR/SEEK_END
使用移動文件描述符位置,判斷文件大小
#include<stdio.h>int main(int argc, char **argv) {FILE *fp = fopen(argv[1], "r");if (!fp) {perror("open file");return 1;}fseek(fp, 0, SEEK_END);printf("Size = %ld", ftell(fp));fclose(fp);return 0; }03 lseek/fcntl/ioctl/mmap
lseek
fcntl
ioctl
重定向流
#include<stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <errno.h> #include <unistd.h> #include <sys/ioctl.h> int main(){struct winsize size;if (!isatty(1)) {perror("1 is not tty\n");exit(1);}if (ioctl(1, TIOCGWINSZ,&size) < 0) {perror("ioctl");exit(1);}printf("%d rows, %d colums\n", size.ws_row, size.ws_col);return 0;}mmap
04 虛擬文件系統VFS
ext2文件系統
文件系統中存儲的最小單位是塊(Block, ー個塊究竟多大是在格式化時確定的,例如mke2fs的-b選項可以設定塊大小為1024、2048或4096字節。
- 啟動塊( Boot Block)
大小就是1KB,由PC標準規定,用來存儲磁盤分區信息和啟動信息,任何文件系統都不能使用該塊
- 超級塊
- inode位圖(inode Bitmap)
- inode表(inode Table)
stat
- stat的返回值只有一個int,但是需要查詢的文件屬性卻很多,用的就是結構體傳值這個功能(傳入的是一個地址),
一個stat的使用案例
#include<stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h>int main(int argc, char** argv){if (argc < 2) {printf("usage : cmd + filename/dirname\n");exit(1);}struct stat st;/***這兩行代碼互換,有什么區別?*stat和lstat有什么區別?*///stat(argv[1], &st);lstat(argv[1], &st);/*if (S_ISDIR(st.st_mode)) {printf("directory\n");} else {printf("other file type\n");}*//***使用stat族函數,可以獲取文件的詳細信息,*進一步得到自己想要的操作*此處就是通過stat解析之后,判斷文件的類型*/switch(st.st_mode & S_IFMT) {case S_IFREG:printf("regular file\n");break;case S_IFDIR:printf("directory\n");break;case S_IFCHR:printf("charactor device\n");break;default:printf("other file type\n");}return 0; }opendir(3)/readdir(3)/closedir(3)
VFS
Linux支持各種各樣的文件系統格式,然而這些文件系統都可以 mount到某個目錄下,使我們看到一個統一的目錄樹,各種文件系統上的目錄和文件我們用ls命令看起來是一樣的,讀寫操作用起來也都是一樣的,這是怎么做到的呢? Linux內核在各種不同的文件系統格式之上做了一個抽象層,使得文件、目錄、讀寫訪問等概念成為抽象層的概念,因此各種文件系統看起來用起來都一樣,這個抽象層稱為虛擬文件系統(VFS, Virtualfilesystem)
dup 和 dup2
實現ls -al
# 進程
01. 進程控制塊PCB
task struct結構體:ps aux
- 進程id。系統中每個進程有唯一的id,在C語言中用pid_t類型表示,其實就是一個非負整數。
- 進程的狀態,有運行、掛起、停止、僵尸等狀態。
- 進程切換時需要保存和恢復的一些GPU寄存器。
- 描述虛擬地址空間的信息。
- 描述控制終端的信息。
- 當前工作目錄( Current Working Directory)
- umask掩碼。
- 文件描述符表,包含很多指向file結構體的指針。
- 和信號相關的信息。
- 用戶id和組id.
- 控制終端、 Session和進程組。
- 進程可以使用的資源上限( Resource Limit)。
02. 進程控制fork
pstree
- fork的作用是根據一個現有的進程復制出一個新進程,原來的進程稱為父進程( ParentProcess),新進程稱為子進程( Child Process)。系統中同時運行著很多進程,這些進程都是從最初只有一個進程開始一個ー個復制出來的
- 在Shel下輸入命令可以運行一個程序,是因為She進程在讀取用戶輸入的命令之后會調用fork復制出一個新的 Shel li進程。
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
example1
/**forkOpt.c *注意思考fork的作用*/ #include <stdio.h> #include <sys/types.h> #include <unistd.h> #include <stdlib.h>int main() {char *message;int n;pid_t pid = fork();if (pid < 0) {perror("fork failed");exit(1);} else if (pid == 0) {//sprintf(message,"This is the child, pid = %d\n", pid);message = "child processs";n = 6;} else {//sprintf(message,"This is the parent, pid = %d\n", pid);message = "parent processs";n = 3;}for (;n > 0; n--) {printf("%s, n = %d\n", message, n); sleep(1);}return 0; }- 運行結果如下:注意思考,子父進程調度順序?
example:思考父子進程的關系,父進程死后,子進程怎么辦?
#include <stdio.h> #include <sys/types.h> #include <unistd.h> #include <stdlib.h>int main() {char *message;int n;pid_t pid = fork();if (pid < 0) {perror("fork failed");exit(1);} else if (pid == 0) {n = 6;for (;n > 0; n--) {printf("\033[31;47mc_pid self\033[0m = %d, parent pid = %d\n", getpid(), getppid()); sleep(1);}} else {n = 3;for (;n > 0; n--) {printf("p_pid self = %d, parent pid = %d\n", getpid(), getppid()); sleep(1);}}return 0; }example3:創建10個子進程,并打印他們的pid和ppid
#include <stdio.h> #include <sys/types.h> #include <unistd.h> #include <stdlib.h>int main() {int i;for (i = 0; i < 100; i++) {pid_t pid = fork();if (pid < 0) {perror("fork");exit(1);} if (pid == 0) {printf("\033[31;47mchild[%d]\033[0m, self = %d, parent = %d\n", i, getpid(), getppid()); sleep(1);break;}}return 0; }- 在不同的平臺運行,父子進程的調度順序確實不一樣。
- 運行結果1:(這是WSL平臺的)
- 運行結果2:(這是Ubuntu20.04 平臺的)
gdb如何調試多進程?
- 挖坑
03. exec函數族
exec函數族
-
當進程調用一種exec函數時,該進程的用戶空間代碼和數據完全被新程序替換,從新程序的啟動例程開始執行。
-
這些函數如果調用成功則加載新的程序從啟動代碼開始執行,不再返回,如果調用出錯則返回-1,所以exec函數只有出錯的返回值而沒有成功的返回值
-
帶有字母l(表示list)的exec函數要求將新程序的每個命令行參數都當作一個參數傳給它,命令行參數的個數是可變的,最后一個可變參數應該是NULL,起 sentinel的作用。
-
對于帶有字母v(表示 vector)的函數,則應該先構造一個指向各參數的指針數組,然后將該數組的首地址當作參數傳給它,數組中的最后一個指針也應該是NULL,像main函數的argv參數或者環境變量表一樣。
-
不帶字母p(表示path)的exec函數第一個參數必須是程序的相對路徑或絕對路徑,例如"/bin/ls"或"./a.out"。
-
對于帶字母p的函數:如果參數中包含/,則將其視為路徑名。否則視為不帶路徑的程序名,在PATH環境變量的目錄列表中搜索這個程序。
-
對于以e(表示 environment)結尾的exec函數,可以把一份新的環境變量表傳給其他exec函數仍使用當前的環境變量表執行新程序
example:用exec族函數調用命令
#include<stdio.h> #include<unistd.h> #include<stdlib.h>int main() {execlp("ls", "", "-a", "-l", NULL);//第二個參數沒有起作用,此處留空了perror("exex");exit(1);return 0; }example:實現流的重定向
- 此處要做的是:1.先實現一個將輸入的小寫字母轉換為大寫字母
- 2.再實現一個將程序的輸入和輸出重定向到指定文件中去,再調用1.實現的程序,從1個文件中讀取,再輸出到另一個文件
callback.c :重定向輸入輸出,用execl調用自己程序執行
#include<stdio.h> #include<sys/types.h> #include<fcntl.h> #include<stdlib.h> #include<errno.h> #include<unistd.h>int main(int argc,char **argv) {if(argc != 3) {printf("Usage:cmd + inputfile + outputfile\n");exit(1);}int fd = open(argv[1], O_RDONLY);if (fd < 0) {perror("open inputfile");exit(1);}dup2(fd, 0);//標準輸入 重定向 到inputfileclose(fd);fd = open(argv[2], O_WRONLY | O_CREAT, 0644);if (fd < 0) {perror("open outputfile");exit(1);}dup2(fd, 1);//標準輸出 重定向 到outputfileclose(fd);//execl("/bin/ls", "/bin/ls", "-a", "-l", NULL);execl("./upper", "./upper", NULL);//調用下面編譯生成的upper可執行文件perror("exec");exit(0); }upper.c :編譯只有得到upper可執行文件,在上個程序中調用
#include<stdio.h> #include<ctype.h> int main() {int ch;while((ch = getchar()) != EOF) {putchar(toupper(ch));}return 0; }環境變量
#include<stdio.h>/* *循環打印環境變量 */int main(void) {extern char **environ;for(int i = 0;environ[i];i++){printf("%s\n",environ[i]);}return 0; }- 思考:下面修改環境變量會一直生效嗎?
04.wait和waitpid函數
-
一個進程在終止時會關閉所有文件描述符,釋放在用戶空間分配的內存,但它的PCB還保留著,內核在其中保存了一些信息:如果是正常終止則保存著退出狀態,如果是異常終止則保存著導致該進程終止的信號是哪個。
-
父進程可以調用wait或 waitpid獲取這些信息,然后徹底清除掉這個進程。
-
例如:一個進程的退出狀態可以在SheI中用特殊變量$?查看,因為 She l I是它的父進程,當它終止時SheI調用wait或 waitpid得到它的退出狀態同時徹底清除掉這個進程。
zomb.c制造僵尸:編譯運行,用ps -u查看僵尸進程
- 當你用ctrl+c強制停止,你會發現,這兩個都被收尸了;a.out是被調用a.out的bash先收尸的,然后子進程就變成了孤兒僵尸,被1號進程(孤兒院)收尸了。
example:使用waitpid判斷子進程結束的狀態
#include<stdio.h> #include<stdlib.h> #include<sys/types.h> #include<sys/wait.h> #include<unistd.h>int main() {pid_t pid = fork();if (pid < 0) {perror("fork");exit(1);}if (pid == 0) {int n = 5;while (n > 0) {printf("this is child process\n");sleep(1);n--;}exit(4);} else {int stat_val;waitpid(pid, &stat_val, 0);if (WIFEXITED(stat_val)) {printf("Child exited with code %d\n", WEXITSTATUS(stat_val));} else if(WIFSIGNALED(stat_val)) {printf("CHild terminated abnormally, signal %d\n", WEXITSTATUS(stat_val));}}exit(0); }05. 進程間通信
- 每個進程各自有不同的用戶地址空間,任何一個進程的全局變量在另一個進程中都看不到,所以進程之間要交換數據必須通過內核,在內核中開辟一塊緩沖區,進程1把數據從用戶空間拷到內核緩沖區,進程2再從內核緩沖區把數據讀走,內核提供的這種機制稱為進程間通信(IPC, Interprocess Communication)
管道pipe
example:pipeOpt.c
#include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<sys/types.h> #include<sys/wait.h>int main () {pid_t pid;int fd[2];int n;char buf[20];if(pipe(fd) < 0) {perror("pipe");exit(1);}/***前面先創建一個管道,后面fork*父進程往管道里面寫,子進程從管道里讀*/pid = fork();if (pid < 0) {perror("fork");exit(1);}if (pid > 0) {close(fd[0]);write(fd[1], "hello pipe\n", 11);wait(NULL);} else {close(fd[1]);sleep(1);n = read(fd[0], buf, 20);write(1, buf, n);}return 0; }- 上面的例子是父進程把文件描述符傳給子進程之后父子進程之間通信,也可以父進程fork兩次,把文件描述符傳給兩個子進程,然后兩個子進程之間通信,總之需要通過fork傳遞文件描述符使兩個進程都能訪問同一管道,它們オ能通信。
- 使用管道需要注意以下4種特殊情況(假設都是阻塞I/0操作,沒有設置 O_NONBLOCK標志)
如果所有指向管道寫端的文件描述符都關閉了,而仍然有進程從管道的讀端讀數據,那么管道中剩余的數據都被讀取后,再次read會返回0,就像讀到文件末尾一樣。
如果有指向管道寫端的文件描述符沒關閉,而持有管道寫端的進程也沒有向管道中寫數據,這時有進程從管道讀端讀數據,那么管道中剩余的數據都被讀取后,再次read會阻塞,直到管道中有數據可讀了才讀取數據并返回。
如果所有指向管道讀端的文件描述符都關閉了,這時有進程向管道的寫端 write,那么該進程會收到信號SIGPIPE,通常會導致進程異常終止。
如果有指向管道讀端的文件描述符沒關閉,而持有管道讀端的進程也沒有從管道中讀數據,這時有進程向管道寫端寫數據,那么在管道被寫滿時再次write會阻塞, 直到管道中有空位置了才寫入數據并返回。
管道popen和pclose
-
這兩個函數實現的操作是:創建一個管道,forkー個子進程,關閉管道的不使用端,exec一個cmd命令,等待命令終止
-
函數 popen先執行fork,然后調用exec以執行 command,并且返回一個標準I/O文件指針。
如果type是"r",則文件指針連接到cmd的標準輸出。
如果type是"w",則文件指針連接到cmd的標準輸入。 -
函數pclose關閉標準I/O流,等待命令執行結束,然后返回cmd的終止狀態。如果cmd不能被執行,則 pclose返回的終止狀態與 shell執行exit一樣。
popenOptwrite.c
#include<stdio.h> #include<stdlib.h> #include<ctype.h>int main() {FILE *fp = popen("./upper", "w");if (!fp) {perror("popen");exit(1);}/*用popen打開的,fp占據著標準輸出* 此fp的內容處理完會輸出到終端*/fprintf(fp, "hello world 3 \n ttt survive thrive\n");pclose(fp);return 0; }popenOptread.c
#include<stdio.h> #include<stdlib.h> #include<ctype.h>int main() {FILE *fp = popen("cat ./out.txt", "r");if (!fp) {perror("popen");exit(1);}int c;while(~(c = fgetc(fp)))putchar(toupper(c));pclose(fp);return 0; }共享內存
- 進程間通信之共享內存
- 共享存儲允許兩個或多個進程共享一給定的存儲區。因為數據不需要在客戶機和服務器之間復制,所以這是最快的一種IPC。
- 其中,key可由fork()生成。pathname必須為調用進程可以訪問的。proj_id的bit是否有效。
- pathname和proj_id共同組成一個key.
shmgetOpt.c
#include<stdio.h> #include<stdlib.h> #include<sys/ipc.h> #include<sys/shm.h>int main() {key_t key = ftok("./callback.c", 9);if (key < 0) {perror("ftok");exit(1);}printf("key = ox%x\n", key);//創建共享內存,此處IPC_EXCL表示必須自己創建int shmid = shmget(key, 20, IPC_CREAT /*| IPC_EXCL*/ | 0666);if (shmid < 0) {perror("shmget");exit(1);}printf("shmid = %d\n", shmid);return 0; }- 一般應指定addr為0,以便由內核選擇地址
1. shmOpt.c
#include<stdio.h> #include<stdlib.h> #include<sys/ipc.h> #include<string.h> #include<sys/shm.h>int main() {key_t key = ftok("./callback.c", 9);if (key < 0) {perror("ftok");exit(1);}printf("key = ox%x\n", key);int shmid = shmget(key, 20, IPC_CREAT /*| IPC_EXCL*/ | 0666);if (shmid < 0) {perror("shmget");exit(1);}printf("shmid = %d\n", shmid);char *shmp = shmat(shmid, NULL, 0);if (shmp < 0) {perror("shmat");exit(1);}printf("shmp = %p\n", shmp);//往共享內存中寫數據//snprintf(shmp, 20, "hello\n");printf("%s", shmp);shmdt(shmp);//取消內存映射關系//如果再此處訪問共享內存會怎樣?//printf("%s", shmp);return 0; }2.shmOpt.c:體會進程間通信的流程
- 注意wsl和 ubuntu有細微的區別,注意。
使用ipcs -m shmid釋放共享內存
ipcs [0]--------- 消息隊列 ----------- 鍵 msqid 擁有者 權限 已用字節數 消息 ------------ 共享內存段 -------------- 鍵 shmid 擁有者 權限 字節 連接數 狀態 0x00000000 4 ubuntu 666 1024 0 0x0a050002 5 ubuntu 666 20 0 0x0a050001 6 ubuntu 666 20 0 0x0001e240 7 ubuntu 666 20 2 ~ % ipcrm -m 4 ~ % ipcrm -m 5 ~ % ipcrm -m 6 ~ % ipcrm -m 7 ~ % ipcs [0]--------- 消息隊列 ----------- 鍵 msqid 擁有者 權限 已用字節數 消息 ------------ 共享內存段 -------------- 鍵 shmid 擁有者 權限 字節 連接數 狀態 --------- 信號量數組 ----------- 鍵 semid 擁有者 權限 nsems消息隊列
# 信號
01. 信號的基本概念
- 用戶輸入命令,在 Shel l下啟動一個前臺進程。
- 用戶按下Ctrl-C,這個鍵盤輸入產生一個硬件中斷。
- 如果GPU當前正在執行這個進程的代碼,則該進程的用戶空間代碼暫停執行,GPU從用戶態切換到內核態處理硬件中斷。
- 終端驅動程序將Ctrl-解釋成一個 SIGINTT信號,記在該進程的PGB中(也可以說發送了ー個 SIGINT信號給該進程)。
- 當某個時刻要從內核返回到該進程的用戶空間代碼繼續執行之前,首先處理PGB中記錄的信號,發現有一個S1GINT信號待處理,而這個信號的默認處理動作是終止進程,所以直接終止進程而不再返回它的用戶空間代碼執行。
- kill -l命令可以察看系統定義的信號列表
- 這些信號各自在什么條件下產生,默認的處理動作是什么,在 signa l(7)中都有詳細說明
- Term表示終止當前進程,Core表示終止當前進程并且Core Dump,Ign表示忽略該信號,Stop表示停止當前進程,Cont表示繼續執行先前停止的進程
02. 如何產生信號
- 當一個進程要異常終止時,可以選擇把進程的用戶空間內存數據全部保存到磁盤上,文件名通常是core,這叫做 Core Dump。
03. 如何阻塞信號
04. 如何捕捉信號
- 如果信號的處理動作是用戶自定義函數,在信號遞達時就調用這個函數,這稱為捕捉信號。由于信號處理函數的代碼是在用戶空間的,處理過程比較復雜
# 線程
01線程的概念
- 有些情況需要在一個進程中同時執行多個控制流程,比如實現一個圖形界面的下載軟件,一方面需要和用戶交互,等待和處理用戶的鼠標鍵盤事件,另一方面又需要同時下載多個文件,等待和處理從多個網絡主機發來的數據,這些任務都需要一個“等待一處理”的循環,那么如何才能同時進行多項任務呢?
- 線程( thread):
是操作系統能夠進行運算調度的最小單位。它被包含在進程之中,是進程中的實際運作單位。一條線程指的是進程中一個單一順序的控制流,一個進程中可以并發多個線程,每條線程并行執行不同的任務。
由于同一進程的多個線程共享同一地址空間,因此 Text Segment、 Data Segment都是共享的,如果定義一個函數,在各線程中都可以調用,如果定義一個全局變量,在各線程中都可以訪問到,除此之外,各線程還共享以下進程資源和環境:
但有些資源是每個線程各有一份的
在 Linux上線程函數位于 Iibpthread共享庫中,因此在編譯時要加上-lpthread選項
02線程控制
- 程序設計中的回調函數是為了給后人開門。在早期的程序設計的時候,不知道后來人需要實現什么功能,這一部分就讓后來使用的人自己實現,
- 這個函數值的第一個參數是結果參數,充當函數返回值作用,類似fork創建進程直接返回pid;
createThread.c
#include<stdio.h> #include<pthread.h> #include<string.h> #include<stdlib.h> #include<unistd.h> #include<sys/types.h>void printid(char *); void *thr_fn(void *arg) {//todoprintid(arg);return NULL; }void printid(char *tip) {pid_t pid = getpid();pthread_t tid = pthread_self();printf("%s pid: %u tid:%u (%p)\n", tip, pid, tid, tid);// printf("%s thr_fn=%p\n", tip, thr_fn); }int main(){pthread_t tid;int ret = pthread_create(&tid, NULL, thr_fn, "new thread");if (ret) {printf("create thread err:%s\n", strerror(ret));exit(1);}sleep(1);printid("main thread");return 0; }- 多次運行后會發現,進程pid一直增加,線程tid相差也很大
- 思考:主線程在一個全局變量ntid中保存了新創建的線程的id,如果新創建的線程不調用pthread_self而是直接打印這個ntid,能不能達到同樣的效果?
- 線程如何退出?
exitThread.c:停止線程
#include<stdio.h> #include<pthread.h> #include<string.h> #include<stdlib.h> #include<unistd.h> #include<sys/types.h>void *thr_fn1(void *arg) {printf("thread 1 returning\n");return (void *) 1; } void *thr_fn2(void *arg) {printf("thread 2 exiting\n");pthread_exit((void *)2);return NULL; } void *thr_fn3(void *arg) {while(1) {printf("thread 3 sleeping\n");sleep(1);}return (void *) 1; }int main() {pthread_t tid;void *sts;pthread_create(&tid, NULL, thr_fn1, NULL);pthread_join(tid, &sts);printf("thread 1 exit code %ld\n", (long)sts);pthread_create(&tid, NULL, thr_fn2, NULL);pthread_join(tid, &sts);printf("thread 2 exit code %ld\n", (long)sts);pthread_create(&tid, NULL, thr_fn3, NULL);sleep(3);pthread_cancel(tid);pthread_join(tid, &sts);printf("thread 3 exit code %ld\n", (long)sts);return 0; }- 運行結果
03線程間同步
線程(thread)是允許應用程序并發的執行多個任務的一種機制。一個進程可以有多個線程,如果每個線程執行不同的任務,通過對線程的執行順序進行控制(調度)就可以實現任務的并發執行。當然了多進程也可以實現任務的并發處理,但是兩者之間是有區別的。最大的區別就是擁有的資源不同。進程擁有自己的獨立系統資源,而線程沒有獨立資源,只能和屬于同一進程的其他線程共享進程的系統資源。單個資源在多個用戶之間共享就會存在一致性的問題,因此需要通過一定的方法來對線程共享資源進行同步。
目前線程間同步主要有互斥量、讀寫鎖、條件變量、自旋鎖、屏障等5種方式。
互斥量(mutex):主要用于保護共享數據,確保同一時間只有一個線程訪問數據。互斥量從本質上來說是一把鎖,在訪問共享資源前對互斥量進行加鎖,訪問完成后釋放互斥量(解鎖)。對互斥量進行加鎖之后,任何其他試圖再次對互斥量加鎖的線程都會被阻塞直到當前線程釋放該互斥鎖。這樣就可以保證每次只有一個線程可以向前執行。
讀寫鎖(reader-writer lock):讀寫鎖也叫做共享互斥鎖(shared-exclusive lock),它有三種狀態:讀模式下加鎖狀態、寫模式下加鎖狀態、不加鎖狀態。一次只能有一個線程可以占有寫模式的讀寫鎖,但是多個線程可以同時戰友讀模式的讀寫鎖。因此與互斥量相比,讀寫鎖允許更高的并行性。讀寫鎖非常適合對數據結構讀的次數遠大于寫的情況。
條件變量:是線程可用的另一種同步機制。條件變量給多個線程提供了一個會合的場所。條件變量與互斥量一起使用時,允許線程以無競爭的方式等待特定的條件發生。條件本身是由互斥量保護的。線程在改變條件狀態之前必須首先鎖住互斥量。其他線程在獲得互斥量之前不會察覺到這種改變,因此互斥量必須在鎖住以后才能計算條件。
自旋鎖:自旋鎖與互斥量類似,但它不是通過休眠使進程阻塞,而是在獲取所之前一直處于忙等(自旋)阻塞狀態。自旋鎖可用于以下情況:鎖被持有的時間短,而且線程并不希望在重新調度上花費太多的成本。自旋鎖用在非搶占式內核中時是非常有用的,除了提供互斥機制以外,還可以阻塞中斷,這樣中斷處理程序就不會陷入死鎖狀態。
屏障(barrier):是用戶協調多個線程并行工作的同步機制。屏障允許每個線程等待,直到所有的合作線程都到達某一點,然后從該點繼續執行。pthread_join函數就是一種屏障,允許一個線程等待,直到另一個線程退出。
多個線程同時訪問共享數據時可能會沖突,這跟前面講信號時所說的可重入性是同樣的問題。比如兩個線程都要把某個全局變量增加1,這個操作在某平臺需要三條指令完成:
cntadd.c:線程之間累加,怎么沒有累加呢?
#include<stdio.h> #include<pthread.h> #include<string.h> #include<stdlib.h> #include<unistd.h> #include<sys/types.h>int cnt = 0; void *cntadd(void *arg) {int val, i;for(i = 0; i < 10; i++){val = cnt;printf("%p : val = %d cnt = %d\n", pthread_self(),val, cnt);cnt = val + 1;}return NULL; } int main(){pthread_t tida, tidb;pthread_create(&tida, NULL, cntadd, NULL);pthread_create(&tidb, NULL, cntadd, NULL);pthread_join(tida,NULL);pthread_join(tidb,NULL);return 0; }- 運行結果:為什么不是10或者20呢?見后面
cntadd.c:線程間同步引入鎖
#include<stdio.h> #include<pthread.h> #include<string.h> #include<stdlib.h> #include<unistd.h> #include<sys/types.h>pthread_mutex_t add_lock = PTHREAD_MUTEX_INITIALIZER;int cnt; void *cntadd(void *arg) {int val, i;for(i = 0; i < 10; i++){pthread_mutex_lock(&add_lock);val = cnt;printf("%p : val = %d cnt = %d\n", pthread_self(),val, cnt);cnt = val + 1;pthread_mutex_unlock(&add_lock);}return NULL; } int main(){pthread_t tida, tidb;pthread_create(&tida, NULL, cntadd, NULL);pthread_create(&tidb, NULL, cntadd, NULL);pthread_join(tida,NULL);pthread_join(tidb,NULL);return 0; }
condi.c:實現消費者生產者模型,生產者就如壓棧,消費者如出棧,類似后進先出
#include<stdio.h> #include<stdlib.h> #include<time.h> #include<unistd.h> #include<pthread.h>typedef struct Goods {int data;struct Goods *next; } Goods;Goods *head = NULL; pthread_mutex_t headlock = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t hasGoods = PTHREAD_COND_INITIALIZER;void *producer(void *arg) {Goods *ng;while (1) {ng = (Goods *)malloc(sizeof(Goods));ng->data = rand() % 100;pthread_mutex_lock(&headlock);ng->next = head;head = ng;pthread_mutex_unlock(&headlock);pthread_cond_signal(&hasGoods);printf("produce %d\n", ng->data);sleep(rand() % 2);} }void *consumer(void *arg) {Goods *k;while(1) {pthread_mutex_lock(&headlock);if(!head) {pthread_cond_wait(&hasGoods, &headlock);}k = head;head = head->next;pthread_mutex_unlock(&headlock);printf("\033[31;47mconsume\033[0m %d\n", k->data);free(k);sleep(rand() % 4);} }int main() {srand(time(NULL));pthread_t pid,cid;pthread_create(&pid, NULL, producer, NULL);pthread_create(&cid, NULL, consumer, NULL);pthread_join(pid, NULL);pthread_join(cid, NULL);return 0; }- 運行結果:
Semaphore
sem.c:生產者消費者模型:類似先進先出
#include<stdio.h> #include<stdlib.h> #include<time.h> #include<unistd.h> #include<pthread.h> #include<semaphore.h>#define NUM 5int q[NUM]; sem_t blank_number, goods_number;void *producer(void *arg) {int i = 0;while (1) {sem_wait(&blank_number);q[i] = rand() % 100 + 1;printf("produce %d\n", q[i]);sem_post(&goods_number);i = (i + 1) % NUM;sleep(rand() % 1);} }void *consumer(void *arg) {int i = 0;while(1) {sem_wait(&goods_number);printf("\033[31;47mconsume\033[0m %d\n", q[i]);q[i] = 0;sem_post(&blank_number);i = (i + 1) % NUM;sleep(rand() % 4);} }int main() {srand(time(NULL));pthread_t pid,cid;sem_init(&blank_number, 0, NUM);sem_init(&goods_number, 0, 0);pthread_create(&pid, NULL, producer, NULL);pthread_create(&cid, NULL, consumer, NULL);pthread_join(pid, NULL);pthread_join(cid, NULL);return 0; }- 運行結果:
后記
- 嗨,你好呀!
總結
- 上一篇: 腾讯地图 qq.map 设置鼠标样式
- 下一篇: 前端“黑话”polyfill