Linux高并发服务器开发---笔记2(多进程)
0630
第4章 項目制作與技能提升
- 4.0 視頻課鏈接
- 4.1 項目介紹與環境搭建
- 4.2 Linux系統編程1、4.3 Linux系統編程2
- 4.4 多進程
- 1-9
- 10.進程間通信☆☆☆
- 進程間通信的概念(IPC)
- Linux 進程間通信的方式(七種)
- ①匿名管道(管道)--- 親緣關系的進程
- 查看管道緩沖大小命令:
- 查看管道緩沖大小函數:fpathconf()函數
- 示例:
- ①有名管道(命名管道,FIFO)
- 補充:管道的讀寫特點
- 示例:(將管道設置為非阻塞)--- fcntl()函數☆☆☆
- ②內存映射 --- mmap()函數
- 示例1(匿名映射)--- 只能用在有親緣關系的進程通信
- 示例2(有名映射)
- ③共享內存
- 使用步驟:
- 共享內存相關的函數:
- 問題1:操作系統如何知道一塊共享內存被多少個進程關聯?
- 問題2:可不可以對共享內存進行多次刪除 `shmctl`
- ☆☆☆共享內存和內存映射的區別
- 共享內存操作命令(打印當前系統中所有的 進程間通信方式 的信息)
- 示例
- ④信號signal
- 信號的基本概念
- 信號相關的函數1:kill()、raise()、abort()、alarm()、setitimer()
- ①kill()函數:給任何的進程發送任何的信號 sig
- ②raise()函數:給當前進程發送信號
- ③abort()函數:殺死當前進程
- ☆☆☆④alarm()函數:設置定時器(鬧鐘),定時時間到了就終止當前的進程
- 示例1:設置一個定時器
- 示例2:電腦1秒鐘能數多少個數?
- ⑤setitimer()函數:(既可以用來延時執行,也可定時執行;可以實現周期性定時)
- 信號相關的函數2:(信號捕捉函數)signal()函數、sigaction()函數
- 信號集(信號的阻塞 --- 防止信號打斷敏感的操作)
- 信號集相關操作函數
- 內核實現信號捕捉的過程
- 補充:SIGCHLD信號(解決僵尸進程的問題)
- 4.5 多線程
4.0 視頻課鏈接
4.1 項目介紹與環境搭建
4.2 Linux系統編程1、4.3 Linux系統編程2
4.4 多進程
1-9
Linux高并發服務器開發—筆記1
10.進程間通信☆☆☆
(視頻課從01:30:30開始)
進程間通信的概念(IPC)
進程是一個獨立的資源分配單元,不同進程(這里所說的進程通常指的是用戶進程)之間的資源是獨立的,沒有關聯,不能在一個進程中直接訪問另一個進程的資源。
但是,進程不是孤立的,不同的進程需要進行信息的交互和狀態的傳遞等,因此需要進程間通信( IPC:Inter Processes Communication )。
進程間通信的目的:
- 數據傳輸:一個進程需要將它的數據發送給另一個進程。
- 通知事件:一個進程需要向另一個或一組進程發送消息,通知它(它們)發生了某種事件(如進程終止時要通知父進程)。
- 資源共享:多個進程之間共享同樣的資源。為了做到這一點,需要內核提供互斥和同步機制。
- 進程控制:有些進程希望完全控制另一個進程的執行(如 Debug 進程),此時控制進程希望能夠攔截另一個進程的所有陷入和異常,并能夠及時知道它的狀態改變。
Linux 進程間通信的方式(七種)
管道(匿名管道、有名管道)
信號量(互斥鎖)
共享內存
內存映射(匿名映射、內存映射)
消息隊列
信號
socket
①匿名管道(管道)— 親緣關系的進程
管道也叫無名(匿名)管道,它是 UNIX 系統 IPC(進程間通信)的最古老形式,所有的 UNIX 系統都支持這種通信機制。
統計一個目錄中文件的數目命令:ls | wc –l,為了執行該命令,shell 創建了兩個進程來分別執行 ls 和 wc。
管道其實是一個在內核內存中維護的緩沖器,這個緩沖器的存儲能力是有限的,不同的操作系統大小不一定相同。
管道擁有文件的特質:讀操作、寫操作,匿名管道沒有文件實體,有名管道有文件實體,但不存儲數據。可以按照操作文件的方式對管道進行操作。
一個管道是一個字節流,使用管道時不存在消息或者消息邊界的概念,從管道讀取數據的進程可以讀取任意大小的數據塊,而不管寫入進程寫入管道的數據塊的大小是多少。
通過管道傳遞的數據是順序的,從管道中讀取出來的字節的順序和它們被寫入管道的順序是完全一樣的。
在管道中的數據的傳遞方向是單向的,一端用于寫入,一端用于讀取,管道是半雙工的。
從管道讀數據是一次性操作,數據一旦被讀走,它就從管道中被拋棄,釋放空間以便寫更多的數據,在管道中無法使用 lseek() 來隨機的訪問數據。
匿名管道只能在具有公共祖先(父進程與子進程,或者兩個兄弟進程,具有親緣關系)的進程之間使用。
創建匿名管道:
#include <unistd.h> int pipe(int pipefd[2]);功能:創建一個匿名管道,用來進程間通信。
參數:int pipefd[2] 這個數組是一個傳出參數。
- pipefd[0] 對應的是管道的讀端
- pipefd[1] 對應的是管道的寫端
返回值: 成功 0;失敗 -1
管道默認是阻塞的:如果管道中沒有數據,read阻塞;如果管道滿了,write阻塞;
注意:匿名管道只能用于具有關系的進程之間的通信(父子進程,兄弟進程)
示例:
// 子進程發送數據給父進程,父進程讀取到數據輸出 #include <unistd.h> #include <sys/types.h> #include <stdio.h> #include <stdlib.h> #include <string.h>int main() {// 在fork之前創建管道int pipefd[2];int ret = pipe(pipefd);if(ret == -1) {perror("pipe");exit(0);}// 創建子進程pid_t pid = fork();if(pid > 0) {// 父進程printf("i am parent process, pid : %d\n", getpid());// 關閉寫端close(pipefd[1]);// 從管道的讀取端讀取數據char buf[1024] = {0};while(1) {int len = read(pipefd[0], buf, sizeof(buf));printf("parent recv : %s, pid : %d\n", buf, getpid());// 向管道中寫入數據// const char * str = "hello,i am parent";// write(pipefd[1], str, strlen(str));// sleep(3);}} else if(pid == 0){// 子進程printf("i am child process, pid : %d\n", getpid());// 關閉讀端close(pipefd[0]);char buf[1024] = {0};while(1) {// 向管道中寫入數據const char *str = "hello,i am child";write(pipefd[1], str, strlen(str));sleep(3);// int len = read(pipefd[0], buf, sizeof(buf));// printf("child recv : %s, pid : %d\n", buf, getpid());// bzero(buf, 1024);}}return 0; }查看管道緩沖大小命令:
ulimit –a root@VM-16-2-ubuntu:~# ulimit -a core file size (blocks, -c) 0 data seg size (kbytes, -d) unlimited scheduling priority (-e) 0 file size (blocks, -f) unlimited pending signals (-i) 15343 max locked memory (kbytes, -l) 65536 max memory size (kbytes, -m) unlimited open files (-n) 1024 pipe size (512 bytes, -p) 8 POSIX message queues (bytes, -q) 819200 real-time priority (-r) 0 stack size (kbytes, -s) 8192 cpu time (seconds, -t) unlimited max user processes (-u) 15343 virtual memory (kbytes, -v) unlimited file locks (-x) unlimited其中pipe size (512 bytes, -p) 8表示管道大小為512B* 8 = 4KB。
查看管道緩沖大小函數:fpathconf()函數
#include <unistd.h> long fpathconf(int fd, int name); //gets a value for the configuration option name for the open file descriptor fd.示例:
#include <unistd.h> #include <sys/types.h> #include <stdio.h> #include <stdlib.h> #include <string.h>int main() {int pipefd[2];int ret = pipe(pipefd);if(ret == -1) {perror("pipe");exit(0);}// 獲取管道的大小long size = fpathconf(pipefd[0], _PC_PIPE_BUF);printf("pipe size : %ld\n", size);return 0; }結果:
pipe size : 40964096字節 = 4KB
示例:
// 子進程發送數據給父進程,父進程讀取到數據輸出 #include <unistd.h> #include <sys/types.h> #include <stdio.h> #include <stdlib.h> #include <string.h>int main() {// 在fork之前創建管道int pipefd[2];int ret = pipe(pipefd);if(ret == -1) {perror("pipe");exit(0);}// 創建子進程pid_t pid = fork();if(pid > 0) {// 父進程printf("i am parent process, pid : %d\n", getpid());// 關閉寫端close(pipefd[1]);// 從管道的讀取端讀取數據:char buf[1024] = {0};while(1) {int len = read(pipefd[0], buf, sizeof(buf));printf("parent recv : %s, pid : %d\n", buf, getpid());// 向管道中寫入數據:// const char * str = "hello,i am parent";// write(pipefd[1], str, strlen(str));// sleep(3);}} else if(pid == 0){// 子進程printf("i am child process, pid : %d\n", getpid());// 關閉讀端close(pipefd[0]);char buf[1024] = {0};while(1) {// 向管道中寫入數據:const char *str = "hello,i am child";write(pipefd[1], str, strlen(str));sleep(3);// 從管道的讀取端讀取數據:// int len = read(pipefd[0], buf, sizeof(buf));// printf("child recv : %s, pid : %d\n", buf, getpid());// bzero(buf, 1024);}}return 0; }編譯運行:
i am child process, pid : 2799345 i am parent process, pid : 2799340 parent recv : hello,i am child, pid : 2799340 parent recv : hello,i am child, pid : 2799340 parent recv : hello,i am child, pid : 2799340 parent recv : hello,i am child, pid : 2799340①有名管道(命名管道,FIFO)
FIFO 在文件系統中作為一個特殊文件存在,但 FIFO 中的內容卻存放在內存中;
當使用 FIFO 的進程退出后,FIFO 文件將繼續保存在文件系統中以便以后使用;
FIFO 有名字,不相關的進程可以通過打開有名管道進行通信。
通過命令創建有名管道:
mkfifo 名字通過函數創建有名管道:
#include <sys/types.h> #include <sys/stat.h> int mkfifo(const char *pathname, mode_t mode);參數:
- pathname: 管道名稱的路徑
- mode: 文件的權限, 和 open 的參數 mode 是一樣的,是一個八進制的數
返回值:成功返回0,失敗返回-1,并設置錯誤號
一旦使用 mkfifo 創建了一個 FIFO,就可以使用 open 打開它,常見的文件I/O 函數都可用于 fifo。如:close、read、write、unlink 等。
FIFO 嚴格遵循先進先出(First in First out),對管道及 FIFO 的讀總是從開始處返回數據,對它們的寫則把數據添加到末尾。它們不支持諸如 lseek() 等文件定位操作。
示例:
mkfifo.c
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <stdlib.h> #include <unistd.h>int main() {// 判斷文件是否存在int ret = access("fifo1", F_OK);if(ret == -1) {printf("管道不存在,創建管道\n"); ret = mkfifo("fifo1", 0664);//0664是權限if(ret == -1) {perror("mkfifo");exit(0);} }return 0; }read.c
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h>// 從管道中讀取數據 int main() {// 1.打開管道文件int fd = open(" ", O_RDONLY);if(fd == -1) {perror("open");exit(0);}// 讀數據while(1) {char buf[1024] = {0};int len = read(fd, buf, sizeof(buf));if(len == 0) {printf("寫端斷開連接了...\n");break;}printf("recv buf : %s\n", buf);}close(fd);return 0; }write.c
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <string.h>int main() {// 3.以只寫的方式打開管道int fd = open("fifo1", O_WRONLY);if(fd == -1) {perror("open");exit(0);}// 寫數據for(int i = 0; i < 100; i++) {char buf[1024];sprintf(buf, "hello, %d\n", i);printf("write data : %s\n", buf);write(fd, buf, strlen(buf));sleep(1);}close(fd);return 0; }分別生成對應的以.o結尾的可執行文件,然后在兩個終端分別運行讀和寫:
補充:
有名管道的注意事項:1.一個為只讀而打開一個管道的進程會阻塞,直到另外一個進程為只寫打開管道2.一個為只寫而打開一個管道的進程會阻塞,直到另外一個進程為只讀打開管道讀管道:管道中有數據,`read`返回**實際讀到的字節數**管道中無數據:管道寫端被全部關閉,read返回0,(相當于讀到文件末尾)寫端沒有全部被關閉,read阻塞等待寫管道:管道讀端被全部關閉,進行異常終止(收到一個SIGPIPE信號)管道讀端沒有全部關閉:管道已經滿了,write會阻塞管道沒有滿,write將數據寫入,并返回實際寫入的字節數。補充:管道的讀寫特點
管道的讀寫特點:
使用管道時,需要注意以下幾種特殊的情況(假設都是阻塞I/O操作)
1.所有的指向管道寫端的文件描述符都關閉了(管道寫端引用計數為0),有進程從管道的讀端讀數據,那么管道中剩余的數據被讀取以后,再次read會返回0,就像讀到文件末尾一樣。
2.如果有指向管道寫端的文件描述符沒有關閉(管道的寫端引用計數大于0),而持有管道寫端的進程也沒有往管道中寫數據,這個時候有進程從管道中讀取數據,那么管道中剩余的數據被讀取后,再次read會阻塞,直到管道中有數據可以讀了才讀取數據并返回。
3.如果所有指向管道讀端的文件描述符都關閉了(管道的讀端引用計數為0),這個時候有進程
向管道中寫數據,那么該進程會收到一個信號SIGPIPE, 通常會導致進程異常終止。
4.如果有指向管道讀端的文件描述符沒有關閉(管道的讀端引用計數大于0),而持有管道讀端的進程也沒有從管道中讀數據,這時有進程向管道中寫數據,那么在管道被寫滿的時候再次write會阻塞,直到管道中有空位置才能再次寫入數據并返回。
總結:
讀管道:
- 管道中有數據,read返回實際讀到的字節數。
- 管道中無數據:
寫端被全部關閉,read返回0(相當于讀到文件的末尾);
寫端沒有完全關閉,read阻塞等待。
寫管道:
- 管道讀端全部被關閉,進程異常終止(進程收到SIGPIPE信號)。
- 管道讀端沒有全部關閉:
管道已滿,write阻塞;
管道沒有滿,write將數據寫入,并返回實際寫入的字節數。
示例:(將管道設置為非阻塞)— fcntl()函數☆☆☆
#include <unistd.h> #include <sys/types.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> /*設置管道非阻塞int flags = fcntl(fd[0], F_GETFL); // 獲取原來的flagflags |= O_NONBLOCK; // 修改flag的值fcntl(fd[0], F_SETFL, flags); // 設置新的flag */ int main() {// 在fork之前創建管道int pipefd[2];int ret = pipe(pipefd);if(ret == -1) {perror("pipe");exit(0);}// 創建子進程pid_t pid = fork();if(pid > 0) {// 父進程printf("i am parent process, pid : %d\n", getpid());// 關閉寫端close(pipefd[1]);// 從管道的讀取端讀取數據char buf[1024] = {0};int flags = fcntl(pipefd[0], F_GETFL); // 獲取原來的flagflags |= O_NONBLOCK; // 修改flag的值fcntl(pipefd[0], F_SETFL, flags); // 設置新的flagwhile(1) {int len = read(pipefd[0], buf, sizeof(buf));printf("len : %d\n", len);printf("parent recv : %s, pid : %d\n", buf, getpid());memset(buf, 0, 1024);sleep(1);}} else if(pid == 0){// 子進程printf("i am child process, pid : %d\n", getpid());// 關閉讀端close(pipefd[0]);char buf[1024] = {0};while(1) {// 向管道中寫入數據char * str = "hello,i am child";write(pipefd[1], str, strlen(str));sleep(5);} }return 0; }結果:
②內存映射 — mmap()函數
可以看①Linux簡明系統編程(嵌入式公眾號的課)—總課時12h中的mmap函數(用在信號量中):
(在當前進程的虛擬地址空間中創建一個新的映射;
如果成功創建了共享映射,就返回此映射區域的指針(地址)void* ;)
內存映射(Memory-mapped I/O)是將磁盤文件的數據映射到內存,用戶通過修改內存就能修改磁盤文件。
修改內存映射區中的內容,內存映射會將修改后的內容同步到磁盤文件;這樣如果有多個進程映射的是同一個磁盤文件,這樣就可以通過以這個磁盤文件為中介實現進程間的通信(每個進程對映射到自己虛擬地址空間中的內存映射區進行操作即可),有點類似于有名管道中通過mkfifo創建的那個文件。
使用內存映射實現進程間通信:
1.有關系的進程(父子進程)間通信
還沒有子進程的時候,通過唯一的父進程,先創建內存映射區;
有了內存映射區以后,創建子進程;
父子進程共享創建的內存映射區
2.沒有關系的進程間通信
準備一個大小不是0的磁盤文件;
進程1 通過磁盤文件創建內存映射區,得到一個操作這塊內存的指針;
進程2 通過磁盤文件創建內存映射區,得到一個操作這塊內存的指針;
使用內存映射區通信;
注意:內存映射區通信,是非阻塞。
①mmap()函數:
#include <sys/mman.h> void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);功能:將一個文件或者設備的數據映射到內存中
參數:
- void *addr:寫 NULL, 系統會自動分配一個空間來放這個共享映射
- length : 要映射的數據的長度,這個值不能為0。建議使用文件的長度。
獲取文件的長度(文件大小):stat() 或者 lseek() - prot : 對申請的內存映射區的操作權限
-PROT_EXEC :可執行的權限
-PROT_READ :讀權限
-PROT_WRITE :寫權限
-PROT_NONE :沒有權限
要操作映射內存,必須要有讀的權限。
PROT_READ、PROT_READ|PROT_WRITE - flags :
- MAP_SHARED : 映射區的數據會自動和磁盤文件進行同步,進程間通信,必須要設置這個選項
- MAP_PRIVATE :不同步,內存映射區的數據改變了,對原來的文件不會修改,會重新創建一個新的文件。(copy on write) - fd: 需要映射的那個文件的文件描述符
- 通過open得到,open的是一個磁盤文件
- 注意:文件的大小不能為0,open指定的權限不能和prot參數有沖突。
prot: PROT_READ open:只讀/讀寫
prot: PROT_READ | PROT_WRITE open:讀寫 - offset:偏移量,一般不用(即寫0)。如果要用就必須指定的是4k的整數倍,0表示不偏移。
返回值:返回創建的內存的首地址;失敗返回MAP_FAILED,(void *) -1
②munmap()函數:
#include <sys/mman.h> int munmap(void *addr, size_t length);功能:釋放內存映射
參數:
- addr : 要釋放的內存的首地址
- length : 要釋放的內存的大小,要和mmap函數中的length參數的值一樣。
示例1(匿名映射)— 只能用在有親緣關系的進程通信
匿名映射:不需要文件實體進程一個內存映射
#include <stdio.h> #include <sys/mman.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <string.h> #include <stdlib.h> #include <sys/wait.h>int main() {// 1.創建匿名內存映射區int len = 4096;void * ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);//如果是匿名映射MAP_ANONYMOUS,那么倒數第二個參數fd就寫-1if(ptr == MAP_FAILED) {perror("mmap");exit(0);}// 父子進程間通信pid_t pid = fork();if(pid > 0) {// 父進程strcpy((char *) ptr, "hello, world");wait(NULL);}else if(pid == 0) {// 子進程sleep(1);printf("%s\n", (char *)ptr);}// 釋放內存映射區int ret = munmap(ptr, len);if(ret == -1) {perror("munmap");exit(0);}return 0; }示例2(有名映射)
(共同映射到一個文件,如果這個文件不存在,就會返回錯誤mmap: Bad file descriptor)
#include <stdio.h> #include <sys/mman.h> #include <fcntl.h> #include <sys/types.h> #include <unistd.h> #include <string.h> #include <stdlib.h> #include <wait.h>int main() {// 1.打開一個文件int fd = open("test.txt", O_RDWR);int size = lseek(fd, 0, SEEK_END); // 獲取文件的大小// 2.創建內存映射區void *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);if(ptr == MAP_FAILED) {perror("mmap");exit(0);}// 3.創建子進程pid_t pid = fork();if(pid > 0) {wait(NULL);// 父進程:讀數據char buf[64];strcpy(buf, (char *)ptr);printf("read data : %s\n", buf);}else if(pid == 0){// 子進程:寫數據strcpy((char *)ptr, "nihao a, son!!!");}// 關閉內存映射區munmap(ptr, size);return 0; }③共享內存
共享內存允許兩個或者多個進程共享物理內存的同一塊區域(通常被稱為段)。由于一個共享內存段會成為一個進程用戶空間的一部分,因此這種 IPC 機制無需內核介入。所有需要做的就是讓一個進程將數據復制進共享內存中,并且這部分數據會對其他所有共享同一個段的進程可用。
與管道等要求發送進程 將數據從用戶空間的緩沖區復制進內核內存 和 接收進程將數據從內核內存復制進用戶空間的緩沖區 的做法相比,這種 IPC 技術的速度更快。
使用步驟:
- 1.調用 shmget() 創建一個新共享內存段或取得一個既有共享內存段的標識符(即由其他進程創建的共享內存段)。這個調用將返回后續調用中需要用到的共享內存標識符;
- 2.使用 shmat() 來附上(連接attach)共享內存段,即使該段成為調用進程的虛擬內存的一部分;
- 3.此刻在程序中可以像對待其他可用內存那樣對待這個共享內存段。為引用這塊共享內存,程序需要使用由 shmat()調用返回的 addr 值,它是一個指向進程的虛擬地址空間中該共享內存段的起點的指針。
- 4.調用 shmdt() 來分離detach共享內存段。在這個調用之后,進程就無法再引用這塊共享內存了。這一步是可選的,并且在進程終止時會自動完成這一步。
- 5.調用 shmctl() 來刪除control共享內存段。只有當當前所有附加內存段的進程都與之分離后內存段才會銷毀。只有一個進程需要執行這一步。
共享內存相關的函數:
頭文件:
#include <sys/ipc.h> #include <sys/shm.h>①shmget()函數
int shmget(key_t key, size_t size, int shmflg);功能:創建一個新的共享內存段,或者獲取一個已有的共享內存段的標識。新創建的內存段中的數據都會被初始化為0
參數:
- key : key_t類型是一個整形,通過這個找到或者創建一個共享內存。
一般使用16進制表示,非0值 - size: 共享內存的大小
- shmflg: 屬性
訪問權限
附加屬性:創建/判斷共享內存是不是存在
- 創建:IPC_CREAT
- 判斷共享內存是否存在: IPC_EXCL , 需要和IPC_CREAT一起使用
IPC_CREAT | IPC_EXCL | 0664
返回值:
- 失敗:-1 并設置錯誤號
- 成功:>0 返回共享內存的引用的ID,后面操作共享內存都是通過這個值。
②shmat()函數
void *shmat(int shmid, const void *shmaddr, int shmflg);功能:和當前的進程進行關聯
參數:
- shmid : 共享內存的標識(ID),由shmget返回值獲取
- shmaddr: 申請的共享內存的起始地址,指定NULL,內核指定
- shmflg : 對共享內存的操作
- 讀 : SHM_RDONLY, 必須要有讀權限
- 讀寫: 0
返回值:
成功:返回共享內存的首(起始)地址。 失敗(void *) -1
③shmdt()函數
int shmdt(const void *shmaddr);功能:解除當前進程和共享內存的關聯
參數:shmaddr,共享內存的首地址
返回值:成功 0, 失敗 -1
④shmctl()函數
int shmctl(int shmid, int cmd, struct shmid_ds *buf);功能:對共享內存進行操作。刪除共享內存,共享內存要刪除才會消失,創建共享內存的進行被銷毀了對共享內存是沒有任何影響。
參數:
- shmid: 共享內存的ID
- cmd : 要做的操作
- IPC_STAT : 獲取共享內存的當前的狀態
- IPC_SET : 設置共享內存的狀態
- IPC_RMID: 標記共享內存被銷毀 - buf:需要設置或者獲取的共享內存的屬性信息
- IPC_STAT : buf存儲數據
- IPC_SET : buf中需要初始化數據,設置到內核中
- IPC_RMID : 沒有用,NULL
⑤ftok()函數
key_t ftok(const char *pathname, int proj_id);功能:根據指定的路徑名,和int值,生成一個共享內存的key
參數:
- pathname:指定一個存在的路徑
/home/nowcoder/Linux/a.txt
/ - proj_id: int類型的值,但是這系統調用只會使用其中的1個字節
范圍 : 0-255 一般指定一個字符 ‘a’
問題1:操作系統如何知道一塊共享內存被多少個進程關聯?
共享內存維護了一個結構體struct shmid_ds 這個結構體中有一個成員 shm_nattch;
shm_nattach 記錄了關聯的進程個數。
問題2:可不可以對共享內存進行多次刪除 shmctl
(這塊可以看視頻課中的02:20:45)
可以的,因為shmctl 標記刪除共享內存,不是直接刪除;
那么什么時候真正刪除呢?
當和共享內存關聯的進程數為0的時候,就真正被刪除;
當共享內存的key為0的時候,表示共享內存被標記刪除了;
如果一個進程和共享內存取消關聯,那么這個進程就不能繼續操作這個共享內存,也不能進行關聯。
☆☆☆共享內存和內存映射的區別
(參考mmap映射區和shm共享內存的區別總結)
所有的進程操作的是同一塊共享內存;
內存映射:每個進程在自己的虛擬地址空間中有一個獨立的內存。
進程突然退出:共享內存還存在;內存映射區消失;
運行進程的電腦死機,宕機了:數據存在在共享內存中,沒有了;內存映射區的數據 ,由于磁盤文件中的數據還在,所以內存映射區的數據還存在。
內存映射區:進程退出,內存映射區銷毀;
共享內存:進程退出,共享內存還在,標記刪除(所有的關聯的進程數為0),或者關機;
如果一個進程退出,會自動和共享內存進行取消關聯。
linux中的兩種共享內存:一種是我們的IPC通信System V版本的共享內存shm,另外的一種就是我們今天提到的存儲映射I/O(mmap函數);
(一共有三種方式創建共享內存:POSIX共享內存對象、System V共享內存段、使用mmap()函數創建的共享映射區)
總結mmap和shm:
1、mmap是在磁盤上建立一個文件,每個進程的地址空間中開辟出一塊空間進行映射;
而對于shm而言,shm每個進程最終會映射到同一塊物理內存。shm保存在物理內存,這樣讀寫的速度要比磁盤要快,但是存儲量不是特別大。
2、相對于shm來說,mmap更加簡單,調用更加方便,所以這也是大家都喜歡用的原因。
3、另外mmap有一個好處是當機器重啟,因為mmap把文件保存在磁盤上,這個文件還保存了操作系統同步的映像,所以mmap不會丟失,但是shmget就會丟失。
mmp的中介是磁盤上的一個文件,每個進程在自己的虛擬地址空間中有一個獨立的內存,所以涉及到i/O操作,因此讀寫速度慢,但電腦宕機后文件依然存在;
shm的中介是同一塊物理內存,所以讀寫速度快,但如果電腦重啟,創建的共享內存就沒了。
(參考鏈接:點這里)
可以看到內存映射中需要的一個參數是int fd(文件的標識符),可見函數是通過fd將文件內容映射到一個內存空間;
訪問共享內存的執行速度比直接訪問文件的快N倍(N>>10),這對于要求快速輸入輸出的場合非常有效;
共享內存主要是為了提高程序的執行速度,方便多個進程進行快速的大數據量的交換;
內存映射是用來加快對文件/設備的訪問(如果是大文件,而且還想提高讀寫速度的話,建議使用內存映射);
共享內存是用來在多個進程間進行快速的大數據量的交換;
可以在程序中指定要將文件內容映射到哪塊內存。對于多個進程打開同一個文件,不同的內存映射可以開辟多塊內存區域。
內存映射是為了加快對文件/設備的訪問速度,不是用來進行數據通信的;
我對內存映射的理解就是通過操作內存來實現對文件的操作,這樣可以加快執行速度,因為操作內存比操作文件的速度快多了!
共享內存,顧名思義,就是預留出的內存區域,它允許一組進程對其訪問。
共享內存是system vIPC中三種通信機制最快的一種,也是最簡單的一種。對于進程來說,
獲得共享內存后,他對內存的使用和其他的內存是一樣的。由一個進程對共享內存所進行的
操作對其他進程來說都是立即可見的,因為每個進程只需要通過一個指向共享內存空間的指針就可以來讀取
共享內存中的內容(說白了就好比申請了一塊內存,每個需要的進程都有一個指針指向這個內存)
就可以輕松獲得結果。使用共享內存要注意的問題:共享內存不能確保對內存操作的互斥性。
一個進程可以向共享內存中的給定地址寫入,而同時另一個進程從相同的地址讀出,這將會導致不一致的數據。
因此使用共享內存的進程必須自己保證讀操作和寫操作的的嚴格互斥。
可使用鎖和原子操作解決這一問題。也可使用信號量保證互斥訪問共享內存區域。
共享內存在一些情況下可以代替消息隊列,而且共享內存的讀/寫比使用消息隊列要快!
共享內存操作命令(打印當前系統中所有的 進程間通信方式 的信息)
ipcs 用法:
ipcs -a // 打印當前系統中所有的進程間通信方式的信息 ipcs -m // 打印出使用共享內存進行進程間通信的信息 ipcs -q // 打印出使用消息隊列進行進程間通信的信息 ipcs -s // 打印出使用信號進行進程間通信的信息ipcrm 用法:
ipcrm -M shmkey // 移除用shmkey創建的共享內存段 ipcrm -m shmid // 移除用shmid標識的共享內存段 ipcrm -Q msgkey // 移除用msqkey創建的消息隊列 ipcrm -q msqid // 移除用msqid標識的消息隊列 ipcrm -S semkey // 移除用semkey創建的信號 ipcrm -s semid // 移除用semid標識的信號示例
這塊的示例不好,可以看①Linux簡明系統編程(嵌入式公眾號的課)—總課時12h中的 ☆☆☆②任務間的通信 之 共享內存 shared memory。
read_shm.c
#include <stdio.h> #include <sys/ipc.h> #include <sys/shm.h> #include <string.h>int main() { // 1.獲取一個共享內存int shmid = shmget(100, 0, IPC_CREAT);printf("shmid : %d\n", shmid);// 2.和當前進程進行關聯void * ptr = shmat(shmid, NULL, 0);// 3.讀數據printf("%s\n", (char *)ptr);printf("按任意鍵繼續\n");getchar();// 4.解除關聯shmdt(ptr);// 5.刪除共享內存shmctl(shmid, IPC_RMID, NULL);return 0; }write_shm.c
#include <stdio.h> #include <sys/ipc.h> #include <sys/shm.h> #include <string.h>int main() { // 1.創建一個共享內存int shmid = shmget(100, 4096, IPC_CREAT|0664);printf("shmid : %d\n", shmid);// 2.和當前進程進行關聯void * ptr = shmat(shmid, NULL, 0);char * str = "helloworld";// 3.寫數據memcpy(ptr, str, strlen(str) + 1);printf("按任意鍵繼續\n");getchar();//按任意鍵// 4.解除關聯shmdt(ptr);// 5.刪除共享內存shmctl(shmid, IPC_RMID, NULL);return 0; }分別生成對應的可執行文件,用兩個終端分別打開。
④信號signal
(視頻課從12:34開始)
(可以參考①Linux簡明系統編程(嵌入式公眾號的課)—總課時12h中的 第20節課:信號signal)
信號的基本概念
信號是 Linux 進程間通信的最古老的方式之一,是事件發生時對進程的通知機制,有時也稱之為軟件中斷,它是在軟件層次上對中斷機制的一種模擬,是一種異步通信的方式。信號可以導致一個正在運行的進程被另一個正在運行的異步進程中斷,轉而處理某一個突發事件。
發往進程的諸多信號,通常都是源于內核。引發內核為進程產生信號的各類事件如下:
- 對于前臺進程,用戶可以通過輸入特殊的終端字符來給它發送信號。比如輸入Ctrl+C 通常會給進程發送一個中斷信號;
- 硬件發生異常,即硬件檢測到一個錯誤條件并通知內核,隨即再由內核發送相應信號給相關進程;比如執行一條異常的機器語言指令,諸如被 0 除,或者引用了無法訪問的內存區域。
- 系統狀態變化,比如 alarm 定時器到期將引起 SIGALRM 信號,進程執行的 CPU 時間超限,或者該進程的某個子進程退出。
- 運行 kill 命令或調用 kill 函數。
(前臺進程./a.out; 后臺進程 ./a.out &)
使用信號的兩個主要目的是:
- 讓進程知道已經發生了一個特定的事情;
- 強迫進程執行它自己代碼中的信號處理程序。
信號的特點:
- 簡單;
- 不能攜帶大量信息;
- 滿足某個特定條件才發送;
- 優先級比較高。
查看系統定義的信號列表:
kill –l前 31 個信號為常規信號,其余為實時信號。
$ kill -l1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP6) 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常用的信號:
| 2 | SIGINT interrupt | 當用戶按下了<Ctrl+C>組合鍵時,用戶終端向正在運行中的由該終端啟動的程序發出此信號 | 終止進程 |
| 3 | SIGQUIT | 用戶按下<Ctrl+\>組合鍵時產生該信號,用戶終端向正在運行中的由該終端啟動的程序發出些信號 | 終止進程 |
| 9 | SIGKILL | 無條件終止進程。該信號不能被忽略,處理和阻塞 | 終止進程,可以殺死任何進程(除了僵尸進程) |
| 11 | SIGSEGV | 指示進程進行了無效內存訪問(段錯誤segment fault) | 終止進程,并產生core文件 (視頻課中26:45到31:13) |
| 13 | SIGPIPE | Broken pipe向一個沒有讀端的管道寫數據 | 終止進程 |
| 14 | SIGALARM | 定時器超時,超時的時間 由系統調用alarm設置 | 終止進程 |
| 17 | SIGCHLD child | 子進程結束時,父進程會收到這個信號 | 忽略這個信號 |
| 18 | SIGCONT continue | 如果進程已停止,則使其繼續運行 | 繼續/忽略 |
| 19 | SIGSTOP | 停止進程的執行。該信號不能被忽略,處理和阻塞 | 終止進程 |
如果程序產生了段錯誤(Segment Fault),一般是因為訪問了非法內存,那么就可以通過生成core文件的方式來找到程序具體哪里出了問題;
先修改core文件大小;
編譯生成可執行程序時加上GDB選項;
然后運行可執行程序時就會生成一個core文件;
通過GDB調試,輸入core-file core,就可以看出來是哪里的代碼導致了段錯誤。
core.c
#include <stdio.h> #include <string.h>int main() {char * buf;strcpy(buf, "hello");return 0; }
查看信號的詳細信息:
man 7 signal信號的 5 種默認處理動作:
- Term 終止進程
- Ign 當前進程忽略掉這個信號
- Core 終止進程,并生成一個Core文件
- Stop 暫停當前進程
- Cont 繼續執行當前被暫停的進程
信號的幾種狀態:產生、未決、遞達
SIGKILL(kill -9) 和 SIGSTOP(kill -19) 信號不能被捕捉、阻塞或者忽略,只能執行默認動作。
信號相關的函數1:kill()、raise()、abort()、alarm()、setitimer()
int kill(pid_t pid, int sig); int raise(int sig); void abort(void); unsigned int alarm(unsigned int seconds); int setitimer(int which, const struct itimerval *new_val, struct itimerval *old_value);①kill()函數:給任何的進程發送任何的信號 sig
#include <sys/types.h> #include <signal.h> int kill(pid_t pid, int sig);功能:給任何的進程或者進程組pid, 發送任何的信號 sig
參數:
- pid :
>0:將信號發送給指定的進程
= 0 : 將信號發送給當前的進程組
= -1 : 將信號發送給每一個有權限接收這個信號的進程
< -1 : 這個pid=某個進程組的ID取反 (-12345) - sig : 需要發送的信號的編號或者是宏值,0表示不發送任何信號
- 例如:kill(getppid(), 9); 和 kill(getpid(), 9);
示例:
#include <stdio.h> #include <sys/types.h> #include <signal.h> #include <unistd.h>int main() {pid_t pid = fork();if(pid == 0) {// 子進程int i = 0;for(i = 0; i < 5; i++) {printf("child process\n");sleep(1);}} else if(pid > 0) {// 父進程printf("parent process\n");sleep(6);printf("kill child process now\n");kill(pid, SIGINT);}return 0; }結果:
child process parent process child process child process child process child process kill child process now②raise()函數:給當前進程發送信號
int raise(int sig);功能:給當前進程發送信號
參數: sig : 要發送的信號
返回值: 成功 0;失敗 非0
相當于是kill(getpid(), sig);
③abort()函數:殺死當前進程
void abort(void);功能: 發送SIGABRT信號給當前的進程,殺死當前進程
相當于是kill(getpid(), SIGABRT);
☆☆☆④alarm()函數:設置定時器(鬧鐘),定時時間到了就終止當前的進程
#include <unistd.h> unsigned int alarm(unsigned int seconds);功能:設置定時器(鬧鐘)。函數調用,開始倒計時,當倒計時為0的時候,函數會給當前的進程發送一個信號:SIGALARM,
參數:seconds 倒計時的時長,單位:秒。如果參數為0,定時器無效(不進行倒計時,不發信號),就相當于是取消一個定時器,通過alarm(0)。
返回值:
- 之前沒有定時器,返回0;
- 之前有定時器,返回之前的定時器剩余的時間。
信號SIGALARM :默認終止當前的進程,每一個進程都有且只有唯一的一個定時器。
例如:
alarm(10); -> 返回0
過了1秒
alarm(5); -> 返回9
alarm(100) -> 該函數是不阻塞的
示例1:設置一個定時器
#include <stdio.h> #include <unistd.h>int main() { 設置一個dingint seconds = alarm(5);printf("seconds = %d\n", seconds); // 0 之前沒有定時器,所以返回0sleep(2);seconds = alarm(2); // 不阻塞printf("seconds = %d\n", seconds); // 3 之前有定時器,所以返回之前的定時器剩余的時間while(1) {}//這里雖然是死循環,但是當定時時間到了的時候就進程就會結束return 0; }示例2:電腦1秒鐘能數多少個數?
#include <stdio.h> #include <unistd.h>/*實際的時間 = 內核時間 + 用戶時間 + 消耗的時間進行文件IO操作的時候比較浪費時間定時器,與進程的狀態無關(自然定時法)。無論進程處于什么狀態,alarm都會計時。 */int main() { alarm(1);//定時1秒int i = 0;while(1) {printf("%i\n", i++);}return 0; }由于打印操作很耗費時間,所以結果不準確(才93361),可以直接將數字存到一個文件中./a.out >> a.txt(>>重定向操作),試了一下文件中有500多萬行數據,文件大概70多M。
⑤setitimer()函數:(既可以用來延時執行,也可定時執行;可以實現周期性定時)
(見linux c setitimer用法說明)
#include <sys/time.h>int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);功能:設置定時器(鬧鐘)。可以替代alarm函數。精度微妙us,可以實現周期性定時。
參數:
- which : 定時器以什么時間計時
ITIMER_REAL: 真實時間,時間到達,發送 SIGALRM;(常用)
ITIMER_VIRTUAL: 用戶時間,時間到達,發送 SIGVTALRM;
ITIMER_PROF: 以該進程在用戶態和內核態下所消耗的時間來計算,時間到達,發送 SIGPROF - new_value: 設置定時器的屬性
- old_value :記錄上一次的定時的時間參數,一般不使用,直接寫NULL
返回值:成功 0;失敗 -1,并設置錯誤號。
結構體struct itimerval:
// 定時器的結構體 struct itimerval { struct timeval it_interval; // 每個階段的時間,間隔時間struct timeval it_value; // 延遲多長時間執行定時器 };struct timeval { // 時間的結構體time_t tv_sec; // 秒數 suseconds_t tv_usec; // 微秒 };例如:it_value設定為10s,it_interval設定為2s,表示先倒計時10秒,然后循環倒計時2秒。
示例:
#include <stdio.h> #include <signal.h> #include <sys/time.h> #include<unistd.h>void signalHandler(int signo) {switch (signo){case SIGALRM:printf("Caught the SIGALRM signal!\n");break;} } //先倒計時5s,然后循環倒計時10s int main(int argc, char *argv[]) {//捕捉SIGALRM信號:signal(SIGALRM, signalHandler);struct itimerval new_value, old_value;//延遲的時間,5s之后開始第一次定時new_value.it_value.tv_sec = 5;new_value.it_value.tv_usec = 0;//間隔的時間:每次定時10snew_value.it_interval.tv_sec = 10;new_value.it_interval.tv_usec = 0;int ret = setitimer(ITIMER_REAL, &new_value, &old_value);//printf("定時器開始了...\n");if(ret == -1) {perror("setitimer");exit(0);}for(int i = 0; i < 30; +i) {printf("%d\n", ++i);sleep(1);}return 0; }結果:先倒計時5s,然后循環倒計時10s
1 2 3 4 5 Caught the SIGALRM signal! 6 7 8 9 10 11 12 13 14 15 Caught the SIGALRM signal! 16 17 18 19 20 21 22 23 24 25 Caught the SIGALRM signal! 26 27 28 29 30信號相關的函數2:(信號捕捉函數)signal()函數、sigaction()函數
①信號捕捉函數 — signal()函數:
(見①Linux簡明系統編程(嵌入式公眾號的課)—總課時12h中的博客1)
功能:設置某個信號的捕捉行為
參數:
- signum: 要捕捉的信號
- handler: 捕捉到信號要如何處理
①SIG_IGN : 忽略信號
②SIG_DFL : 使用信號默認的行為
③回調函數handler : 這個函數是內核調用,程序員只負責寫,捕捉到信號后如何去處理信號。
回調函數:
- 需要程序員實現,提前準備好的,函數的類型根據實際需求,看函數指針的定義
- 不是程序員調用,而是當信號產生,由內核調用
- 函數指針是實現回調的手段,函數實現之后,將函數名放到函數指針的位置就可以了。
返回值:
- 成功,返回上一次注冊的信號處理函數的地址。第一次調用返回NULL;
- 失敗,返回SIG_ERR,設置錯誤號。
注意:SIGKILL 和 SIGSTOP不能被捕捉,不能被忽略。
示例:
(見上面setitimer()函數中的示例)
②sigaction()函數:
#include <signal.h> int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);功能:檢查或者改變信號的處理;信號捕捉
參數:
- signum : 需要捕捉的信號的編號或者宏值(信號的名稱)
- act :捕捉到信號之后的處理動作
- oldact : 上一次對信號捕捉相關的設置,一般不使用,直接寫NULL
返回值: 成功 0;失敗 -1
結構體struct sigaction:
struct sigaction {// 函數指針,指向的函數就是信號捕捉到之后的處理函數void (*sa_handler)(int);// 不常用void (*sa_sigaction)(int, siginfo_t *, void *);// 臨時阻塞信號集,在信號捕捉函數執行過程中,臨時阻塞某些信號。sigset_t sa_mask;// 使用哪一個信號處理對捕捉到的信號進行處理// 這個值可以是0,表示使用sa_handler;也可以是SA_SIGINFO,表示使用sa_sigactionint sa_flags;// 被廢棄掉了void (*sa_restorer)(void); };示例:
#include <sys/time.h> #include <stdio.h> #include <stdlib.h> #include <signal.h> #include<unistd.h>void myalarm(int num) {printf("捕捉到了信號的編號是:%d\n", num);printf("xxxxxxx\n"); } void signalHandler(int signo) {switch (signo){case SIGALRM:printf("Caught the SIGALRM signal!\n");break;} }// 過3秒以后,每隔2秒鐘定時一次 int main() {struct sigaction act;act.sa_flags = 0;act.sa_handler = myalarm;//signalHandler;//sigemptyset(&act.sa_mask); // 清空臨時阻塞信號集// 注冊信號捕捉sigaction(SIGALRM, &act, NULL);struct itimerval new_value;// 設置間隔的時間new_value.it_interval.tv_sec = 2;new_value.it_interval.tv_usec = 0;// 設置延遲的時間,3秒之后開始第一次定時new_value.it_value.tv_sec = 3;new_value.it_value.tv_usec = 0;int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞的//printf("定時器開始了...\n");if(ret == -1) {perror("setitimer");exit(0);}//while(1);for(int i = 0; i < 30; +i) {printf("%d\n", ++i);sleep(1);}return 0; }結果:
1 2 3 捕捉到了信號的編號是:14 xxxxxxx 4 5 捕捉到了信號的編號是:14 xxxxxxx 6 7 捕捉到了信號的編號是:14 xxxxxxx 8 9 捕捉到了信號的編號是:14 xxxxxxx 10 11 捕捉到了信號的編號是:14 xxxxxxx信號集(信號的阻塞 — 防止信號打斷敏感的操作)
(視頻課從56:50開始到01:02:10)
許多信號相關的系統調用都需要能表示一組不同的信號,多個信號可使用一個稱之為信號集的數據結構來表示,其系統數據類型為 sigset_t。
在 PCB 中有兩個非常重要的信號集。一個稱之為 “阻塞信號集” ,另一個稱之為 “未決信號集” 。這兩個信號集都是內核使用位圖機制來實現的,但操作系統不允許我們直接對這兩個信號集進行位操作,而需要自定義另外一個集合,借助信號集操作函數來對 PCB 中的這兩個信號集進行修改。
- 信號的 “未決” 是一種狀態,指的是從信號的產生到信號被處理前的這一段時間;
- 信號的 “阻塞” 是一個開關動作,指的是阻止信號被處理,但不是阻止信號產生。
信號的阻塞就是讓系統暫時保留信號留待以后發送。由于另外有辦法讓系統忽略信號,所以一般情況下信號的阻塞只是暫時的,只是為了防止信號打斷敏感的操作。
阻塞信號機 & 未決信號機:
- 在內核中將所有沒被處理的信號存儲在一個集合中; (未決信號集)
- SIGINT信號狀態被存儲在第二個標志位上;
— 這個標志位的值為0, 說明信號不是未決狀態;
— 這個標志位的值為1, 說明信號處于未決狀態 ;
- 阻塞信號集默認不阻塞任何的信號;
- 如果想要阻塞某些信號需要用戶調用系統的API;(見下面的信號集相關操作函數)
- 如果沒有阻塞,這個信號就被處理;
- 如果阻塞了,這個信號就繼續處于未決狀態,直到阻塞解除,這個信號才會被處理。
信號集相關操作函數
一、對自定義的信號集進行修改: int sigemptyset(sigset_t *set);//清空臨時阻塞信號集 int sigfillset(sigset_t *set);//全部置一 int sigaddset(sigset_t *set, int signum);//將某個信號置一 int sigdelset(sigset_t *set, int signum);//將某個信號清零 int sigismember(const sigset_t *set, int signum);//判斷某個信號的狀態是否為1二、對內核中的兩個信號集進行修改: int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); int sigpending(sigset_t *set);下面分別介紹一和二:
一、對自定義的信號集進行修改
以下信號集相關的函數都是對自定義的信號集進行操作。
①sigemptyset()函數
功能:清空信號集中的數據,將信號集中的所有的標志位 置為0
參數:set,傳出參數,需要操作的信號集
返回值:成功返回0, 失敗返回-1
②sigfillset()函數
int sigfillset(sigset_t *set);功能:將信號集中的所有的標志位置為1
參數:set,傳出參數,需要操作的信號集
返回值:成功返回0, 失敗返回-1
③sigaddset()函數
int sigaddset(sigset_t *set, int signum);功能:設置信號集中的某一個信號對應的標志位為1,表示阻塞這個信號
參數:
- set:傳出參數,需要操作的信號集
- signum:需要設置阻塞的那個信號
返回值:成功返回0, 失敗返回-1
④sigdelset()函數
int sigdelset(sigset_t *set, int signum);功能:設置信號集中的某一個信號對應的標志位為0,表示不阻塞這個信號
參數:
- set:傳出參數,需要操作的信號集
- signum:需要設置不阻塞的那個信號
返回值:成功返回0, 失敗返回-1
⑤sigismember()函數
int sigismember(const sigset_t *set, int signum);功能:判斷某個信號是否阻塞,即它對應的標志位是否為1
參數:
- set:需要操作的信號集
- signum:需要判斷的那個信號
返回值:
- 1 : signum被阻塞
- 0 : signum不阻塞
- -1 : 失敗
⑥示例:對自定義的信號集進行修改
#include <signal.h> #include <stdio.h>int main() {// 創建一個信號集sigset_t set;// 清空信號集的內容sigemptyset(&set);// 判斷 SIGINT 是否在信號集 set 里int ret = sigismember(&set, SIGINT);if(ret == 0) {printf("SIGINT 不阻塞\n");} else if(ret == 1) {printf("SIGINT 阻塞\n");}// 添加幾個信號到信號集中sigaddset(&set, SIGINT);//Ctrl+csigaddset(&set, SIGQUIT);//Ctrl+\ // 判斷SIGINT是否在信號集中ret = sigismember(&set, SIGINT);if(ret == 0) {printf("SIGINT 不阻塞\n");} else if(ret == 1) {printf("SIGINT 阻塞\n");}// 判斷SIGQUIT是否在信號集中ret = sigismember(&set, SIGQUIT);if(ret == 0) {printf("SIGQUIT 不阻塞\n");} else if(ret == 1) {printf("SIGQUIT 阻塞\n");}// 從信號集中刪除一個信號sigdelset(&set, SIGQUIT);// 判斷SIGQUIT是否在信號集中ret = sigismember(&set, SIGQUIT);if(ret == 0) {printf("SIGQUIT 不阻塞\n");} else if(ret == 1) {printf("SIGQUIT 阻塞\n");}return 0; }編譯運行:
SIGINT 不阻塞 SIGINT 阻塞 SIGQUIT 阻塞 SIGQUIT 不阻塞二、對內核中的兩個信號集進行修改
①sigprocmask()函數:
功能:將自定義信號集中的數據設置到內核中(設置阻塞,解除阻塞,替換)
參數:
- how : 如何對內核阻塞信號集進行處理
<1>SIG_BLOCK: 將用戶設置的阻塞信號集添加到內核中,內核中原來的數據不變
假設內核中默認的阻塞信號集是mask, mask | set
<2>SIG_UNBLOCK: 根據用戶設置的數據,對內核中的數據進行解除阻塞
mask &= ~set
<3>SIG_SETMASK:覆蓋內核中原來的值 - set :已經初始化好的用戶自定義的信號集
- oldset : 保存設置之前的內核中的阻塞信號集的狀態,可以是 NULL
返回值:成功:0;失敗:-1,設置錯誤號:EFAULT、EINVAL
②sigpending()函數:
int sigpending(sigset_t *set);功能:獲取內核中的未決信號集
參數:set,傳出參數,保存的是內核中的未決信號集中的信息。
③示例:對內核中的兩個信號集進行修改
// 編寫一個程序,把所有的常規信號(1-31)的未決狀態打印到屏幕 // 設置某些信號是阻塞的,通過鍵盤產生這些信號 #include <stdio.h> #include <signal.h> #include <stdlib.h> #include <unistd.h>int main() {// 設置2、3號信號阻塞sigset_t set;//自定義信號集sigemptyset(&set);//全部清零// 將2號和3號信號添加到信號集中sigaddset(&set, SIGINT);//Ctrl+csigaddset(&set, SIGQUIT);//Ctrl+\ // 修改內核中的阻塞信號集sigprocmask(SIG_BLOCK, &set, NULL);int num = 0;while(1) {num++;// 獲取當前的未決信號集的數據sigset_t pendingset;sigemptyset(&pendingset);//全部清零sigpending(&pendingset);//獲取內核中的未決信號集中的信息// 遍歷前32位for(int i = 1; i <= 31; i++) {if(sigismember(&pendingset, i) == 1) {printf("1");}else if(sigismember(&pendingset, i) == 0) {printf("0");}else {perror("sigismember");exit(0);}}printf("\n");sleep(1);if(num == 10) {//循環10次// 解除阻塞,就只執行信號,而執行信號的結果就是結束進程sigprocmask(SIG_UNBLOCK, &set, NULL);}}return 0; }編譯運行:
內核實現信號捕捉的過程
補充:SIGCHLD信號(解決僵尸進程的問題)
SIGCHLD信號產生的條件:
- 子進程終止時;
- 子進程接收到 SIGSTOP 信號時變為停止態;
- 子進程處在停止態,接受到SIGCONT后被喚醒時;
以上三種條件都會給父進程發送 SIGCHLD 信號,父進程默認會忽略該信號。
☆☆☆使用SIGCHLD信號解決僵尸進程的問題:
示例:
父進程會執行一個while死循環,而子進程輸出自己的pid就結束了,這樣子進程會變成僵尸進程,無法通過kill -9指令殺死,只能通過殺死父進程或者讓父進程循環調用wait()函數或者waitpid()函數來回收子進程。
但是我們希望有更好的解決方法:父進程可以執行它自己的內容,而不用專門等待子進程結束然后對其進行回收,我們可以通過信號捕捉的方式,當有子進程結束時會向父進程發送SIGCHLD信號,捕捉到這個信號時對其進行回收。
結果:
child process pid : 2928710 child process pid : 2928711 child process pid : 2928712 child process pid : 2928713 捕捉到的信號 :17 child die , pid = 2928710 child die , pid = 2928711 child die , pid = 2928712 child die , pid = 2928713 child process pid : 2928714 child die , pid = 2928714 捕捉到的信號 :17 parent process pid : 2928701 parent process pid : 2928701 parent process pid : 2928701 parent process pid : 29287014.5 多線程
見Linux高并發服務器開發—筆記3
總結
以上是生活随笔為你收集整理的Linux高并发服务器开发---笔记2(多进程)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 谁是IPFS中国区“奶王”?IPFS.F
- 下一篇: 义乌一院校专门培养“网络小贩”90后年入