中文亚洲精品无码_熟女乱子伦免费_人人超碰人人爱国产_亚洲熟妇女综合网

當(dāng)前位置: 首頁 > news >正文

岳陽網(wǎng)站建設(shè)哪里便宜百度人工在線客服

岳陽網(wǎng)站建設(shè)哪里便宜,百度人工在線客服,網(wǎng)站建設(shè) 溫州,dw做網(wǎng)站是靜態(tài)還是動(dòng)態(tài)文章目錄 1. 進(jìn)程控制1.1 進(jìn)程概述1.1.1 并行和并發(fā)1.1.2 PCB1.1.4 進(jìn)程狀態(tài)1.1.5 進(jìn)程命令 1.2 進(jìn)程創(chuàng)建1.2.1 函數(shù)1.2.2 fork() 剖析 1.3 父子進(jìn)程1.3.1 進(jìn)程執(zhí)行位置1.3.2 循環(huán)創(chuàng)建子進(jìn)程1.3.3 終端顯示問題1.3.4 進(jìn)程數(shù)數(shù) 1.4 execl和execlp函數(shù)1.4.1 execl()1.4.2 execlp(…

文章目錄

  • 1. 進(jìn)程控制
    • 1.1 進(jìn)程概述
      • 1.1.1 并行和并發(fā)
      • 1.1.2 PCB
      • 1.1.4 進(jìn)程狀態(tài)
      • 1.1.5 進(jìn)程命令
    • 1.2 進(jìn)程創(chuàng)建
      • 1.2.1 函數(shù)
      • 1.2.2 fork() 剖析
    • 1.3 父子進(jìn)程
      • 1.3.1 進(jìn)程執(zhí)行位置
      • 1.3.2 循環(huán)創(chuàng)建子進(jìn)程
      • 1.3.3 終端顯示問題
      • 1.3.4 進(jìn)程數(shù)數(shù)
    • 1.4 execl和execlp函數(shù)
      • 1.4.1 execl()
      • 1.4.2 execlp()
      • 1.4.3 函數(shù)的使用
    • 1.5 進(jìn)程控制
      • 1.5.1 結(jié)束進(jìn)程
      • 1.5.2 孤兒進(jìn)程
      • 1.5.3 僵尸進(jìn)程
      • 1.5.4 進(jìn)程回收
        • 1.5.4.1 wait()
        • 1.5.4.2 waitpid()
  • 2. 管道
    • 2.1 管道
    • 2.2 匿名管道
      • 2.2.1 創(chuàng)建匿名管道
      • 2.2.2 進(jìn)程間通信
    • 2.3 有名管道
      • 2.3.1 創(chuàng)建有名管道
      • 2.3.2 進(jìn)程間通信
    • 2.4 管道的讀寫行為
  • 3. 內(nèi)存映射(mmap)
    • 3.1 創(chuàng)建內(nèi)存映射區(qū)
    • 3.2 進(jìn)程間通信
      • 3.2.1 有血緣關(guān)系
      • 3.2.2 沒有血緣關(guān)系
    • 3.3 拷貝文件
  • 4. 共享內(nèi)存
    • 4.1 創(chuàng)建/打開共享內(nèi)存
      • 4.1.1 shmget
      • 4.1.2 ftok
    • 4.2 關(guān)聯(lián)和解除關(guān)聯(lián)
      • 4.2.1 shmat
    • 4.2.2 shmdt
    • 4.3 刪除共享內(nèi)存
      • 4.3.1 shmctl
      • 4.3.2 相關(guān)shell命令
      • 4.3.3 共享內(nèi)存狀態(tài)
    • 4.4 進(jìn)程間通信
    • 4.5 shm和mmap的區(qū)別
  • 5. 信號(hào)
    • 5.1 信號(hào)概述
      • 5.1.1 信號(hào)編號(hào)
      • 5.1.2 查看信號(hào)信息
      • 5.1.3 信號(hào)的狀態(tài)
    • 5.2 信號(hào)相關(guān)函數(shù)
      • 5.2.1 kill/raise/abort
      • 5.2.2 定時(shí)器
        • 5.2.2.1 alarm
        • 5.2.2.2 setitimer
    • 5.3 信號(hào)集
      • 5.3.1 阻塞/未決信號(hào)集
      • 5.3.2 信號(hào)集函數(shù)
    • 5.4 信號(hào)捕捉
      • 5.4.1 signal
      • 5.4.2 sigaction
    • 5.5 SIGCHLD 信號(hào)
  • 6. 守護(hù)進(jìn)程
    • 6.1 進(jìn)程組
    • 6.2 會(huì)話
    • 6.3 創(chuàng)建守護(hù)進(jìn)程
    • 6.4 守護(hù)進(jìn)程的應(yīng)用


1. 進(jìn)程控制

1.1 進(jìn)程概述

從嚴(yán)格意義上來講,程序和進(jìn)程是兩個(gè)不同的概念,他們的狀態(tài),占用的系統(tǒng)資源都是不同的。

  • 程序:就是磁盤上的可執(zhí)行文件文件, 并且只占用磁盤上的空間,是一個(gè)靜態(tài)的概念。
  • 進(jìn)程:被執(zhí)行之后的程序叫做進(jìn)程,不占用磁盤空間,需要消耗系統(tǒng)的內(nèi)存,CPU資源,每個(gè)運(yùn)行的進(jìn)程的都對(duì)應(yīng)一個(gè)屬于自己的虛擬地址空間,這是一個(gè)動(dòng)態(tài)的概念。

1.1.1 并行和并發(fā)

  • CPU時(shí)間片
    CPU在某個(gè)時(shí)間點(diǎn)只能處理一個(gè)任務(wù),但是操作系統(tǒng)都支持多任務(wù)的,那么在計(jì)算機(jī)CPU只有一個(gè)的情況下是怎么完成多任務(wù)處理的呢?每個(gè)人分一點(diǎn),但是又不叫吃飽。
    CPU會(huì)給每個(gè)進(jìn)程被分配一個(gè)時(shí)間段,進(jìn)程得到這個(gè)時(shí)間片之后才可以運(yùn)行,使各個(gè)程序從表面上看是同時(shí)進(jìn)行的。
    如果在時(shí)間片結(jié)束時(shí)進(jìn)程還在運(yùn)行,CPU的使用權(quán)將被收回,該進(jìn)程將會(huì)被中斷掛起等待下一個(gè)時(shí)間片。
    如果進(jìn)程在時(shí)間片結(jié)束前阻塞或結(jié)束,則CPU當(dāng)即進(jìn)行切換,這樣就可避免CPU資源的浪費(fèi)。
    因此可以得知,在我們使用的計(jì)算機(jī)中啟動(dòng)的多個(gè)程序,從宏觀上看是同時(shí)運(yùn)行的,從微觀上看由于CPU一次只能處理一個(gè)進(jìn)程,所有它們是輪流執(zhí)行的,只不過切換速度太快,感覺不到,因此CPU的核數(shù)越多計(jì)算機(jī)的處理效率越高。

  • 并發(fā)和并行
    這兩個(gè)概念都可籠統(tǒng)的解釋為:多個(gè)進(jìn)程同時(shí)運(yùn)行. 但是他們兩個(gè)的同時(shí)并不是一個(gè)概念。Erlang 之父 Joe Armstrong 用一張小孩能看懂的圖解釋了并發(fā)與并行的區(qū)別:
    在這里插入圖片描述

并發(fā):第一幅圖是并發(fā)。

  • 并發(fā)的同時(shí)運(yùn)行是一個(gè)假象,咖啡機(jī)也好CPU也好在某一個(gè)時(shí)間點(diǎn)只能為某一個(gè)個(gè)體來服務(wù),因此不可能同時(shí)處理多任務(wù),這是通過上圖的咖啡機(jī)/計(jì)算機(jī)的CPU快速的時(shí)間片切換實(shí)現(xiàn)的。
  • 并發(fā)是針對(duì)某一個(gè)硬件資源而言的,在某個(gè)時(shí)間段之內(nèi)處理的任務(wù)的總量,量越大效率越高。
  • 并發(fā)也可以理解為是不斷努力自我升華的結(jié)果。

并行:第二幅圖是并行。

  • 并行的多進(jìn)程同時(shí)運(yùn)行是真實(shí)存在的,可以在同一時(shí)刻同時(shí)運(yùn)行多個(gè)進(jìn)程
  • 并行需要依賴多個(gè)硬件資源,單個(gè)是無法實(shí)現(xiàn)的(圖中有兩臺(tái)咖啡機(jī))。
  • 并行可以理解為出生就有天然的硬件優(yōu)勢(shì),資源多辦事效率就高。

1.1.2 PCB

PCB - 進(jìn)程控制塊(Processing Control Block),Linux內(nèi)核的進(jìn)程控制塊本質(zhì)上是一個(gè)叫做 task_struct 的結(jié)構(gòu)體。
在這個(gè)結(jié)構(gòu)體中記錄了進(jìn)程運(yùn)行相關(guān)的一些信息,介紹一些常用的信息:

  • 進(jìn)程id:每一個(gè)進(jìn)程都一個(gè)唯一的進(jìn)程ID,類型為 pid_t, 本質(zhì)是一個(gè)整形數(shù)

  • 進(jìn)程的狀態(tài):進(jìn)程有不同的狀態(tài), 狀態(tài)是一直在變化的,有就緒,運(yùn)行,掛起,停止等狀態(tài)。

  • 進(jìn)程對(duì)應(yīng)的虛擬地址空間的信息。

  • 描述控制終端的信息,進(jìn)程在哪個(gè)終端啟動(dòng)默認(rèn)就和哪個(gè)終端綁定。

  • 當(dāng)前工作目錄:默認(rèn)情況下, 啟動(dòng)進(jìn)程的目錄就是當(dāng)前的工作目錄

  • umask掩碼:在創(chuàng)建新文件的時(shí)候,通過這個(gè)掩碼屏蔽某些用于對(duì)文件的操作權(quán)限。

  • 文件描述符表:每個(gè)被分配的文件描述符都對(duì)應(yīng)一個(gè)已經(jīng)打開的磁盤文件

  • 和信號(hào)相關(guān)的信息:在Linux中 調(diào)用函數(shù), 鍵盤快捷鍵, 執(zhí)行shell命令等操作都會(huì)產(chǎn)生信號(hào)。

    • 阻塞信號(hào)集:記錄當(dāng)前進(jìn)程中阻塞哪些已產(chǎn)生的信號(hào),使其不能被處理
  • 未決信號(hào)集:記錄在當(dāng)前進(jìn)程中產(chǎn)生的哪些信號(hào)還沒有被處理掉。

  • 用戶id和組id:當(dāng)前進(jìn)程屬于哪個(gè)用戶, 屬于哪個(gè)用戶組

  • 會(huì)話(Session)和進(jìn)程組:多個(gè)進(jìn)程的集合叫進(jìn)程組,多個(gè)進(jìn)程組的集合叫會(huì)話。

  • 進(jìn)程可以使用的資源上限:可以使用shell命令ulimit -a查看詳細(xì)信息。


1.1.4 進(jìn)程狀態(tài)

進(jìn)程一共有五種狀態(tài)分別為:創(chuàng)建態(tài)就緒態(tài)運(yùn)行態(tài)阻塞態(tài)(掛起態(tài))退出態(tài)(終止態(tài))
其中 創(chuàng)建態(tài) 和 退出態(tài) 維持的時(shí)間是非常 短 的,稍縱即逝。
我們需將就緒態(tài), 運(yùn)行態(tài), 掛起態(tài),三者之間的狀態(tài)切換搞明白。
在這里插入圖片描述

  • 就緒態(tài): 等待CPU資源
    • 進(jìn)程被創(chuàng)建出來了,有運(yùn)行的資格但是還沒有運(yùn)行,需要搶CPU時(shí)間片
    • 得到CPU時(shí)間片,進(jìn)程開始運(yùn)行,從就緒態(tài)轉(zhuǎn)換為運(yùn)行態(tài)。
    • 進(jìn)程的CPU時(shí)間片用完了, 再次失去CPU, 從運(yùn)行態(tài)轉(zhuǎn)換為就緒態(tài)。
  • 運(yùn)行態(tài):獲取到CPU資源的進(jìn)程,進(jìn)程只有在這種狀態(tài)下才能運(yùn)行
    • 運(yùn)行態(tài)不會(huì)一直持續(xù),進(jìn)程的CPU時(shí)間片用完之后, 再次失去CPU,從運(yùn)行態(tài)轉(zhuǎn)換為就緒態(tài)
    • 只要進(jìn)程還沒有退出,就會(huì)在就緒態(tài)和運(yùn)行態(tài)之間不停的切換。
  • 阻塞態(tài):進(jìn)程被強(qiáng)制放棄CPU,并且沒有搶奪CPU時(shí)間片的資格
    • 比如: 在程序中調(diào)用了某些函數(shù)(比如: sleep()),進(jìn)程又運(yùn)行態(tài)轉(zhuǎn)換為阻塞態(tài)(掛起態(tài))
    • 當(dāng)某些條件被滿足了(比如:slee() 睡醒了),進(jìn)程的阻塞狀態(tài)也就被解除了,進(jìn)程從阻塞態(tài)轉(zhuǎn)換為就緒態(tài)。
  • 退出態(tài): 進(jìn)程被銷毀, 占用的系統(tǒng)資源被釋放了
    • 任何狀態(tài)的進(jìn)程都可以直接轉(zhuǎn)換為退出態(tài)。

1.1.5 進(jìn)程命令

在研究如何創(chuàng)建進(jìn)程之前,先來看一下如何在終端中通過命令完成進(jìn)程相關(guān)的操作。

  • 查看進(jìn)程
$ ps aux- a: 查看所有終端的信息- u: 查看用戶相關(guān)的信息- x: 顯示和終端無關(guān)的進(jìn)程信息

在這里插入圖片描述

如果特別想知道每個(gè)參數(shù)控制著哪些信息, 可以通過 ps a, ps u, ps x分別查看。

  • 殺死進(jìn)程

kill命令可以發(fā)送某個(gè)信號(hào)到對(duì)應(yīng)的進(jìn)程,進(jìn)程收到某些信號(hào)之后默認(rèn)的處理動(dòng)作就是退出進(jìn)程,如果要給進(jìn)程發(fā)送信號(hào),可以先查看一下Linux給我們提供了哪些標(biāo)準(zhǔn)信號(hào)。

查看Linux中的標(biāo)準(zhǔn)信號(hào):

$ 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

9號(hào)信號(hào)(SIGKILL)的行為是無條件殺死進(jìn)程,想要?dú)⑺滥膫€(gè)進(jìn)程就可以把這個(gè)信號(hào)發(fā)送給這個(gè)進(jìn)程,操作如下:

# 無條件殺死進(jìn)程, 進(jìn)程ID通過 ps aux 可以查看(PID)
$ kill -9 進(jìn)程ID
$ kill -SIGKILL 進(jìn)程ID

1.2 進(jìn)程創(chuàng)建

1.2.1 函數(shù)

Linux中進(jìn)程ID為 pid_t 類型,其本質(zhì)是一個(gè)正整數(shù)
通過上邊的ps aux命令已經(jīng)得到了驗(yàn)證。PID為1的進(jìn)程是Linux系統(tǒng)中創(chuàng)建的第一個(gè)進(jìn)程。

  • 獲取當(dāng)前進(jìn)程的進(jìn)程ID(PID)
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
  • 獲取當(dāng)前進(jìn)程的父進(jìn)程 ID(PPID)
#include <sys/types.h>
#include <unistd.h>
pid_t getppid(void);
  • 創(chuàng)建一個(gè)新的進(jìn)程
#include <unistd.h>
pid_t fork(void);

Linux中看似創(chuàng)建一個(gè)新的進(jìn)程非常簡(jiǎn)單,函數(shù)連參數(shù)都沒有
實(shí)際上如果想要真正理解這個(gè)函數(shù)還是要下功夫。


1.2.2 fork() 剖析

pid_t fork(void);

啟動(dòng)磁盤上的應(yīng)用程序, 得到一個(gè)進(jìn)程, 如果在這個(gè)啟動(dòng)的進(jìn)程中調(diào)用fork()函數(shù),就會(huì)得到一個(gè)新的進(jìn)程,我們習(xí)慣將其稱之為子進(jìn)程
前面說過每個(gè)進(jìn)程都對(duì)應(yīng)一個(gè)屬于自己的虛擬地址空間,子進(jìn)程的地址空間是基于父進(jìn)程的地址空間拷貝出來的,雖然是拷貝但是兩個(gè)地址空間中存儲(chǔ)的信息不可能是完全相同的,下圖是拷貝之后父子進(jìn)程各自的虛擬地址空間:
在這里插入圖片描述

  • 相同點(diǎn):
    拷貝完成之后(注意這個(gè)時(shí)間點(diǎn)),兩個(gè)地址空間中的用戶區(qū)數(shù)據(jù)是相同的。
    用戶區(qū)數(shù)據(jù)主要數(shù)據(jù)包括:
    • 代碼區(qū):默認(rèn)情況下父子進(jìn)程地址空間中的源代碼始終相同。
    • 全局?jǐn)?shù)據(jù)區(qū):父進(jìn)程中的全局變量和變量值全部被拷貝一份放到了子進(jìn)程地址空間中
    • 堆區(qū):父進(jìn)程中的堆區(qū)變量和變量值全部被拷貝一份放到了子進(jìn)程地址空間中
    • 動(dòng)態(tài)庫加載區(qū)(內(nèi)存映射區(qū)):父進(jìn)程中數(shù)據(jù)信息被拷貝一份放到了子進(jìn)程地址空間中
    • 棧區(qū):父進(jìn)程中的棧區(qū)變量和變量值全部被拷貝一份放到了子進(jìn)程地址空間中
    • 環(huán)境變量:默認(rèn)情況下,父子進(jìn)程地址空間中的環(huán)境變量始終相同。
    • 文件描述符表: 父進(jìn)程中被分配的文件描述符都會(huì)拷貝到子進(jìn)程中,在子進(jìn)程中可以使用它們打開對(duì)應(yīng)的文件
  • 區(qū)別:
    • 父子進(jìn)程各自的虛擬地址空間是相互獨(dú)立的,不會(huì)互相干擾和影響。
    • 父子進(jìn)程地址空間中代碼區(qū)代碼雖然相同,但是父子進(jìn)程執(zhí)行的代碼邏輯可能是不同的。
    • 由于父子進(jìn)程可能執(zhí)行不同的代碼邏輯,因此地址空間拷貝完成之后,全局?jǐn)?shù)據(jù)區(qū), 棧區(qū), 堆區(qū), 動(dòng)態(tài)庫加載區(qū)(內(nèi)存映射區(qū))數(shù)據(jù)會(huì)各自發(fā)生變化,由于地址空間是相互獨(dú)立的,因此不會(huì)互相覆蓋數(shù)據(jù)。
    • 由于每個(gè)進(jìn)都有自己的進(jìn)程ID,因此內(nèi)核區(qū)存儲(chǔ)的父子進(jìn)程ID是不同的。
    • 進(jìn)程啟動(dòng)之后進(jìn)入就緒態(tài),運(yùn)行需要爭(zhēng)搶CPU時(shí)間片而且可能執(zhí)行不同的業(yè)務(wù)邏輯,所以父子進(jìn)程的狀態(tài)可能是不同的。
    • fork() 調(diào)用成功之后,會(huì)返回兩個(gè)值,父子進(jìn)程的返回值是不同的。
      • 該函數(shù)調(diào)用成功之后,從一個(gè)虛擬地址空間變成了兩個(gè)虛擬地址空間,每個(gè)地址空間中都會(huì)將 fork() 的返回值記錄下來這就是為什么會(huì)得到兩個(gè)返回值的原因。
      • 父進(jìn)程的虛擬地址空間中將該返回值標(biāo)記為一個(gè)大于0的數(shù)(其實(shí)記錄的是子進(jìn)程的進(jìn)程ID)
      • 子進(jìn)程的虛擬地址空間中將該返回值標(biāo)記 0
      • 在程序中需要通過 fork() 的返回值來判斷當(dāng)前進(jìn)程是子進(jìn)程還是父進(jìn)程。
int main()
{// 在父進(jìn)程中創(chuàng)建子進(jìn)程pid_t pid = fork();printf("當(dāng)前進(jìn)程fork()的返回值: %d\n", pid);if(pid > 0){// 父進(jìn)程執(zhí)行的邏輯printf("我是父進(jìn)程, pid = %d\n", getpid());}else if(pid == 0){// 子進(jìn)程執(zhí)行的邏輯printf("我是子進(jìn)程, pid = %d, 我爹是: %d\n", getpid(), getppid());}else // pid == -1{// 創(chuàng)建子進(jìn)程失敗了}// 不加判斷, 父子進(jìn)程都會(huì)執(zhí)行這個(gè)循環(huán)for(int i=0; i<5; ++i){printf("%d\n", i);}return 0;
}

1.3 父子進(jìn)程

1.3.1 進(jìn)程執(zhí)行位置

在父進(jìn)程中成功創(chuàng)建了子進(jìn)程,子進(jìn)程就擁有父進(jìn)程代碼區(qū)的所有代碼,那么子進(jìn)程中的代碼是在什么位置開始運(yùn)行的呢?
父進(jìn)程是從main()函數(shù)開始運(yùn)行的,子進(jìn)程是在父進(jìn)程中調(diào)用fork()函數(shù)之后被創(chuàng)建, 子進(jìn)程就從fork()之后開始向下執(zhí)行代碼。
在這里插入圖片描述

上圖中演示了父子進(jìn)程中代碼的執(zhí)行流程,可以看到如果在程序中對(duì)fork()的返回值做了判斷,就可以控制父子進(jìn)程的行為,如果沒有做任何判斷這個(gè)代碼塊父子進(jìn)程都可以執(zhí)行。
在編寫多進(jìn)程程序的時(shí)候,一定要將代碼想象成多份進(jìn)行分析,因?yàn)橹庇^上看代碼就一份,但實(shí)際上數(shù)據(jù)都是多份,且多份數(shù)據(jù)中變量名都相同,但是他們的值卻不一定相同。


1.3.2 循環(huán)創(chuàng)建子進(jìn)程

掌握了進(jìn)程創(chuàng)建函數(shù)之后,實(shí)現(xiàn)一個(gè)簡(jiǎn)單的功能,在一個(gè)父進(jìn)程中循環(huán)創(chuàng)建3個(gè)子進(jìn)程,也就是最后需要得到4個(gè)進(jìn)程,1個(gè)父進(jìn)程,3個(gè)子進(jìn)程
為了方便驗(yàn)證程序的正確性,要求在程序中打印出每個(gè)進(jìn)程的進(jìn)程ID。

// process_loop.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>int main()
{for(int i=0; i<3; ++i){pid_t pid = fork();printf("當(dāng)前進(jìn)程pid: %d\n", getpid());}return 0;
}

編譯并執(zhí)行上面的代碼,得到了如下結(jié)果:

# 編譯
$ gcc process_loop.c# 執(zhí)行
$ ./a.out
# 最終得到了 8個(gè)進(jìn)程
當(dāng)前進(jìn)程pid: 18774     ------ 1
當(dāng)前進(jìn)程pid: 18774     ------ 1
當(dāng)前進(jìn)程pid: 18774     ------ 1
當(dāng)前進(jìn)程pid: 18777     ------ 2
當(dāng)前進(jìn)程pid: 18776     ------ 3
當(dāng)前進(jìn)程pid: 18776     ------ 3
當(dāng)前進(jìn)程pid: 18775     ------ 4
當(dāng)前進(jìn)程pid: 18775     ------ 4
當(dāng)前進(jìn)程pid: 18775     ------ 4
當(dāng)前進(jìn)程pid: 18778     ------ 5
當(dāng)前進(jìn)程pid: 18780     ------ 6
當(dāng)前進(jìn)程pid: 18779     ------ 7
當(dāng)前進(jìn)程pid: 18779     ------ 7
當(dāng)前進(jìn)程pid: 18781     ------ 8

通過程序打印的信息發(fā)現(xiàn)程序循環(huán)了三次,最終得到了8個(gè)進(jìn)程,也就是創(chuàng)建出了7個(gè)子進(jìn)程,沒有在程序中加條件控制,所有的代碼父子進(jìn)程都是有資格執(zhí)行的。
在這里插入圖片描述

上圖中的樹狀結(jié)構(gòu),藍(lán)色節(jié)點(diǎn)代表父進(jìn)程:

  • 循環(huán)第一次 i = 0,創(chuàng)建出一個(gè)子進(jìn)程,即紅色節(jié)點(diǎn),子進(jìn)程變量值來自父進(jìn)程拷貝,因此 i=0
  • 循環(huán)第二次 i = 1,藍(lán)色父進(jìn)程和紅色子進(jìn)程都去創(chuàng)建子進(jìn)程,得到兩個(gè)紫色進(jìn)程,子進(jìn)程變量值來自父進(jìn)程拷貝,因此 i=1
  • 循環(huán)第三次 i = 2,藍(lán)色父進(jìn)程和紅色、紫色子進(jìn)程都去創(chuàng)建子進(jìn)程,因此得到4個(gè)綠色子進(jìn)程,子進(jìn)程變量值來自父進(jìn)程拷貝,因此 i=2
  • 循環(huán)第四次 i = 3,所有進(jìn)程都不滿足條件 for(int i=0; i<3; ++i)因此不進(jìn)入循環(huán),退出了。

解決方案:可以只讓父進(jìn)程創(chuàng)建子進(jìn)程,如果是子進(jìn)程不讓其繼續(xù)創(chuàng)建子進(jìn)程,只需在程序中添加關(guān)于父子進(jìn)程的判斷即可。

// 需要在上邊的程序中控制不讓子進(jìn)程, 再創(chuàng)建子進(jìn)程即可
// process_loop.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>int main()
{pid_t pid;// 在循環(huán)中創(chuàng)建子進(jìn)程for(int i=0; i<3; ++i){pid = fork();if(pid == 0){// 不讓子進(jìn)程執(zhí)行循環(huán), 直接跳出break;}}printf("當(dāng)前進(jìn)程pid: %d\n", getpid());return 0;
}

最后編譯并執(zhí)行程序,查看最終結(jié)果,可以看到最后確實(shí)得到了4個(gè)不同的進(jìn)程
pid最小的為父進(jìn)程,其余為子進(jìn)程:

# 編譯
$ gcc process_loop.c# 執(zhí)行
$ ./a.out
當(dāng)前進(jìn)程pid: 2727
當(dāng)前進(jìn)程pid: 2730
當(dāng)前進(jìn)程pid: 2729
當(dāng)前進(jìn)程pid: 2728

在多進(jìn)程序中,進(jìn)程的執(zhí)行順序是沒有規(guī)律的,因?yàn)樗械倪M(jìn)程都需要在就緒態(tài)爭(zhēng)搶CPU時(shí)間片,搶到了就執(zhí)行,搶不到就不執(zhí)行
默認(rèn)進(jìn)程的優(yōu)先級(jí)是相同的,操作系統(tǒng)不會(huì)讓某一個(gè)進(jìn)程一直搶不到CPU時(shí)間片。


1.3.3 終端顯示問題

在執(zhí)行多進(jìn)程程序的時(shí)候,經(jīng)常會(huì)遇到下圖中的問題
看似進(jìn)程還沒有執(zhí)行完成,貌似因?yàn)槭裁幢蛔枞?#xff0c;實(shí)際上終端是正常的,通過鍵盤輸入一些命令,終端也能接受輸入并輸出相關(guān)信息,那為什么終端會(huì)顯示成這樣呢?
在這里插入圖片描述

  1. a.out 進(jìn)程啟動(dòng)之后,共創(chuàng)建了3個(gè)子進(jìn)程,其實(shí) a.out 也是有父進(jìn)程的就是當(dāng)前的終端

  2. 終端只能檢測(cè)到 a.out 進(jìn)程的狀態(tài),a.out執(zhí)行期間終端切換到后臺(tái),a.out執(zhí)行完畢之后終端切換回前臺(tái)

  3. 當(dāng)終端切換到前之后,a.out的子進(jìn)程還沒有執(zhí)行完畢,當(dāng)子進(jìn)程輸出的信息就顯示到終端命令提示符的后邊了,導(dǎo)致終端顯示有問題,但是此時(shí)終端是可以接收鍵盤輸入的,只是看起來不美觀而已。

  4. 想要解決這個(gè)問題,需要讓所有子進(jìn)程退出之后再退出父進(jìn)程,比如:在父進(jìn)程代碼中調(diào)用 sleep()

pid_t pid = fork();
if(pid > 0)
{sleep(3);	// 讓父進(jìn)程睡一會(huì)兒
}
else if(pid == 0)
{// 子進(jìn)程
}

1.3.4 進(jìn)程數(shù)數(shù)

當(dāng)父進(jìn)程創(chuàng)建一個(gè)子進(jìn)程,那么父子進(jìn)程之間可以通過全局變量互動(dòng),實(shí)現(xiàn)交替數(shù)數(shù)的功能嗎?

// number.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>// 定義全局變量
int number = 10;int main()
{printf("創(chuàng)建子進(jìn)程之前 number = %d\n", number);pid_t pid = fork();// 父子進(jìn)程都會(huì)執(zhí)行這一行printf("當(dāng)前進(jìn)程fork()的返回值: %d\n", pid);//如果是父進(jìn)程if(pid > 0){printf("我是父進(jìn)程, pid = %d, number = %d\n", getpid(), ++number);printf("父進(jìn)程的父進(jìn)程(終端進(jìn)程), pid = %d\n", getppid());sleep(1);}else if(pid == 0){// 子進(jìn)程number += 100;printf("我是子進(jìn)程, pid = %d, number = %d\n", getpid(), number);printf("子進(jìn)程的父進(jìn)程, pid = %d\n", getppid());}return 0;
}

編譯程序并測(cè)試:

$ gcc number.c
$ ./a.out 
創(chuàng)建子進(jìn)程之前 number = 10
當(dāng)前進(jìn)程fork()的返回值: 3513
當(dāng)前進(jìn)程fork()的返回值: 0
我是子進(jìn)程, pid = 3513, number = 110
子進(jìn)程的父進(jìn)程, pid = 3512我是父進(jìn)程, pid = 3512, number = 11	
#沒有接著子進(jìn)程的110繼續(xù)數(shù),父子進(jìn)程各玩各的,測(cè)試失敗
父進(jìn)程的父進(jìn)程(終端進(jìn)程), pid = 2175

通過驗(yàn)證得到結(jié)論:兩個(gè)進(jìn)程中是不能通過全局變量實(shí)現(xiàn)數(shù)據(jù)交互的
因?yàn)槊總€(gè)進(jìn)程都有自己的地址空間,兩個(gè)同名全局變量存儲(chǔ)在不同的虛擬地址空間中,二者沒有任何關(guān)聯(lián)性。
如果要進(jìn)行進(jìn)程間通信需要使用:管道,共享內(nèi)存,本地套接字,內(nèi)存映射區(qū),消息隊(duì)列等方式。


1.4 execl和execlp函數(shù)

在項(xiàng)目開發(fā)過程中,有時(shí)候有這種需求,需要通過現(xiàn)在運(yùn)行的進(jìn)程啟動(dòng)磁盤上的另一個(gè)可執(zhí)行程序,也就是通過一個(gè)進(jìn)程啟動(dòng)另一個(gè)進(jìn)程,這種情況下我們可以使用 exec族函數(shù)

//函數(shù)原型
#include <unistd.h>extern char **environ;
int execl(const char *path, const char *arg, .../* (char  *) NULL */);
int execlp(const char *file, const char *arg, .../* (char  *) NULL */);
int execle(const char *path, const char *arg, .../*, (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],char *const envp[]);

這些函數(shù)執(zhí)行成功后不會(huì)返回,因?yàn)檎{(diào)用進(jìn)程的實(shí)體,包括代碼段數(shù)據(jù)段堆棧等都已經(jīng)被新的內(nèi)容取代(也就是說用戶區(qū)數(shù)據(jù)基本全部被替換掉了),只留下進(jìn)程ID等一些表面上的信息仍保持原樣.
只有調(diào)用失敗了,它們才會(huì)返回一個(gè) -1,從原程序的調(diào)用點(diǎn)接著往下執(zhí)行。

也就是說exec族函數(shù)并沒有創(chuàng)建新進(jìn)程的能力
讓啟動(dòng)的新進(jìn)程寄生到自己虛擬地址空間之內(nèi),并挖空了自己的地址空間用戶區(qū),把新啟動(dòng)的進(jìn)程數(shù)據(jù)填充進(jìn)去。

exec族函數(shù)中最常用的有兩個(gè)execl()execlp(),這兩個(gè)函數(shù)是對(duì)其他4個(gè)函數(shù)做了進(jìn)一步的封裝,介紹一下。


1.4.1 execl()

該函數(shù)可用于執(zhí)行任意一個(gè)可執(zhí)行程序,函數(shù)需要通過指定的文件路徑才能找到這個(gè)可執(zhí)行程序。

#include <unistd.h>
// 變參函數(shù)
int execl(const char *path, const char *arg, ...);
  • 參數(shù):
    • path: 要啟動(dòng)的可執(zhí)行程序的路徑, 推薦使用絕對(duì)路徑
    • arg: ps aux 查看進(jìn)程的時(shí)候, 啟動(dòng)的進(jìn)程的名字, 可以隨意指定, 一般和要啟動(dòng)的可執(zhí)行程序名相同
    • ...: 要執(zhí)行的命令需要的參數(shù),可以寫多個(gè),最后以 NULL 結(jié)尾,表示參數(shù)指定完了。
  • 返回值:如果這個(gè)函數(shù)執(zhí)行成功, 沒有返回值,如果執(zhí)行失敗, 返回 -1

1.4.2 execlp()

該函數(shù)常用于執(zhí)行已經(jīng)設(shè)置了環(huán)境變量的可執(zhí)行程序,函數(shù)中的 p 就是path,也是說這個(gè)函數(shù)會(huì)自動(dòng)搜索系統(tǒng)的環(huán)境變量PATH
因此使用這個(gè)函數(shù)執(zhí)行可執(zhí)行程序不需要指定路徑,只需要指定出名字即可。

// p == path
int execlp(const char *file, const char *arg, ...);
  • 參數(shù):
    • file: 可執(zhí)行程序的名字
      • 在環(huán)境變量PATH中,可執(zhí)行程序可以不加路徑
      • 沒有在環(huán)境變量PATH中, 可執(zhí)行程序需要指定絕對(duì)路徑
    • arg: ps aux 查看進(jìn)程的時(shí)候, 啟動(dòng)的進(jìn)程的名字, 可以隨意指定, 一般和要啟動(dòng)的可執(zhí)行程序名相同
    • ...: 要執(zhí)行的命令需要的參數(shù),可以寫多個(gè),最后以 NULL 結(jié)尾,表示參數(shù)指定完了。
  • 返回值:如果這個(gè)函數(shù)執(zhí)行成功, 沒有返回值,如果執(zhí)行失敗, 返回 -1

1.4.3 函數(shù)的使用

一般不會(huì)在進(jìn)程中直接調(diào)用,如果直接調(diào)用這個(gè)進(jìn)程的代碼區(qū)代碼被替換也就不能按照原來的流程工作了。
一般在調(diào)用這些函數(shù)的時(shí)候都會(huì)先創(chuàng)建一個(gè)子進(jìn)程,在子進(jìn)程中調(diào)用 exec 族函數(shù),子進(jìn)程的用戶區(qū)數(shù)據(jù)被替換掉開始執(zhí)行新的程序中的代碼邏輯,但是父進(jìn)程不受任何影響仍然可以繼續(xù)正常工作。

execl() 或者 execlp() 函數(shù)的使用方法如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>int main()
{// 創(chuàng)建子進(jìn)程pid_t pid = fork();// 在子進(jìn)程中執(zhí)行磁盤上的可執(zhí)行程序if(pid == 0){// 磁盤上的可執(zhí)行程序 /bin/ps
#if 1execl("/bin/ps", "title", "aux", NULL);// 也可以這么寫// execl("/bin/ps", "title", "a", "u", "x", NULL);  
#elseexeclp("ps", "title", "aux", NULL);// 也可以這么寫// execl("ps", "title", "a", "u", "x", NULL);
#endif// 如果成功,當(dāng)前子進(jìn)程的代碼區(qū)被ps中的代碼區(qū)代碼替換// 下面的所有代碼都不會(huì)執(zhí)行// 如果函數(shù)調(diào)用失敗了,才會(huì)繼續(xù)執(zhí)行下面的代碼perror("execl");printf("++++++++++++++++++++++++\n");printf("++++++++++++++++++++++++\n");printf("++++++++++++++++++++++++\n");printf("++++++++++++++++++++++++\n");printf("++++++++++++++++++++++++\n");printf("++++++++++++++++++++++++\n");}else if(pid > 0){printf("我是父進(jìn)程.....\n");}return 0;
}

在這里插入圖片描述


1.5 進(jìn)程控制

進(jìn)程控制主要是指進(jìn)程的退出, 進(jìn)程的回收和進(jìn)程的特殊狀態(tài) 孤兒進(jìn)程僵尸進(jìn)程。

1.5.1 結(jié)束進(jìn)程

想要直接退出某個(gè)進(jìn)程可以在程序的任何位置調(diào)用exit()或者_exit()函數(shù)。
函數(shù)的參數(shù)相當(dāng)于退出碼, 如果參數(shù)值為 0 程序退出之后的狀態(tài)碼就是0, 如果是100退出的狀態(tài)碼就是100。

// 專門退出進(jìn)程的函數(shù), 在任何位置調(diào)用都可以
// 標(biāo)準(zhǔn)C庫函數(shù)
#include <stdlib.h>
void exit(int status);// Linux的系統(tǒng)函數(shù)
// 可以這么理解, 在linux中 exit() 函數(shù) 封裝了 _exit()
#include <unistd.h>
void _exit(int status);

在 main 函數(shù)中直接使用 return 也可以退出進(jìn)程,
假如是在一個(gè)普通函數(shù)中調(diào)用 return 只能返回到調(diào)用者的位置,而不能退出進(jìn)程。

// ***** return 必須要在main()函數(shù)中調(diào)用, 才能退出進(jìn)程 *****
// 舉例:
// 沒有問題的例子
int main()
{return 0;	// 進(jìn)程退出了
}// 不能退出的例子 //int func()
{return 666;	// 返回到調(diào)用者調(diào)用該函數(shù)的位置, 返回到 main() 函數(shù)的第19行
}int main()
{// 調(diào)用這個(gè)函數(shù), 當(dāng)前進(jìn)程不能退出int ret = func();
}

1.5.2 孤兒進(jìn)程

在一個(gè)啟動(dòng)的進(jìn)程中創(chuàng)建子進(jìn)程,這時(shí)候父子進(jìn)程同時(shí)運(yùn)行,但是父進(jìn)程由于某種原因先退出了,子進(jìn)程還在運(yùn)行,這時(shí)候這個(gè)子進(jìn)程就可以被稱之為孤兒進(jìn)程。

操作系統(tǒng)是非常關(guān)愛運(yùn)行的每一個(gè)進(jìn)程的,當(dāng)檢測(cè)到某一個(gè)進(jìn)程變成了孤兒進(jìn)程,這時(shí)候系統(tǒng)中就會(huì)有一個(gè)固定的進(jìn)程領(lǐng)養(yǎng)這個(gè)孤兒進(jìn)程。
如果使用Linux沒有桌面終端,這個(gè)領(lǐng)養(yǎng)孤兒進(jìn)程的進(jìn)程就是 init 進(jìn)程(PID=1)
如果有桌面終端,這個(gè)領(lǐng)養(yǎng)孤兒進(jìn)程就是桌面進(jìn)程。

那么問題來了,系統(tǒng)為什么要領(lǐng)養(yǎng)這個(gè)孤兒進(jìn)程呢?
在子進(jìn)程退出的時(shí)候, 進(jìn)程中的用戶區(qū)可以自己釋放, 但是進(jìn)程內(nèi)核區(qū)的pcb資源自己無法釋放,必須要由父進(jìn)程來釋放子進(jìn)程的pcb資源,孤兒進(jìn)程被領(lǐng)養(yǎng)之后,這件事爹就可以代勞,避免系統(tǒng)資源的浪費(fèi)。

下面這段代碼就可以得到一個(gè)孤兒進(jìn)程:

int main()
{// 創(chuàng)建子進(jìn)程pid_t pid = fork();// 父進(jìn)程if(pid > 0){printf("我是父進(jìn)程, pid=%d\n", getpid());}else if(pid == 0){sleep(1);	// 強(qiáng)迫子進(jìn)程睡眠1s, 這個(gè)期間, 父進(jìn)程退出, 當(dāng)前進(jìn)程變成了孤兒進(jìn)程// 子進(jìn)程printf("我是子進(jìn)程, pid=%d, 父進(jìn)程ID: %d\n", getpid(), getppid());}return 0;
}
# 程序輸出的結(jié)果
$ ./a.out 
我是父進(jìn)程, pid=22459
我是子進(jìn)程, pid=22460, 父進(jìn)程ID: 1		# 父進(jìn)程向退出, 子進(jìn)程變成孤兒進(jìn)程, 子進(jìn)程被1號(hào)進(jìn)程回收

1.5.3 僵尸進(jìn)程

在一個(gè)啟動(dòng)的進(jìn)程中創(chuàng)建子進(jìn)程,這時(shí)候就有了父子兩個(gè)進(jìn)程,父進(jìn)程正常運(yùn)行, 子進(jìn)程先與父進(jìn)程結(jié)束, 子進(jìn)程無法釋放自己的PCB資源, 需要父進(jìn)程來做這個(gè)件事兒, 但是如果父進(jìn)程也不管, 這時(shí)候子進(jìn)程就變成了僵尸進(jìn)程。

僵尸進(jìn)程不能將它看成是一個(gè)正常的進(jìn)程,這個(gè)進(jìn)程已經(jīng)死亡,用戶區(qū)資源已經(jīng)被釋放,只是還占用一些內(nèi)核資源(PCB)。

運(yùn)行下面的代碼就可以得到一個(gè)僵尸進(jìn)程了:

int main()
{pid_t pid;// 創(chuàng)建子進(jìn)程for(int i=0; i<5; ++i){pid = fork();if(pid == 0){break;}}// 父進(jìn)程if(pid > 0){// 需要保證父進(jìn)程一直在運(yùn)行// 一直運(yùn)行不退出, 并且也做回收, 就會(huì)出現(xiàn)僵尸進(jìn)程while(1){printf("我是父進(jìn)程, pid=%d\n", getpid());sleep(1);}}else if(pid == 0){// 子進(jìn)程, 執(zhí)行這句代碼之后, 子進(jìn)程退出了printf("我是子進(jìn)程, pid=%d, 父進(jìn)程ID: %d\n", getpid(), getppid());}return 0;
}
# ps aux 查看進(jìn)程信息
# Z+ --> 這個(gè)進(jìn)程是僵尸進(jìn)程, defunct, 表示進(jìn)程已經(jīng)死亡
robin     22598  0.0  0.0   4352   624 pts/2    S+   10:11   0:00 ./app
robin     22599  0.0  0.0      0     0 pts/2    Z+   10:11   0:00 [app] <defunct> # 子進(jìn)程
robin     22600  0.0  0.0      0     0 pts/2    Z+   10:11   0:00 [app] <defunct> # 子進(jìn)程
robin     22601  0.0  0.0      0     0 pts/2    Z+   10:11   0:00 [app] <defunct> # 子進(jìn)程
robin     22602  0.0  0.0      0     0 pts/2    Z+   10:11   0:00 [app] <defunct> # 子進(jìn)程
robin     22603  0.0  0.0      0     0 pts/2    Z+   10:11   0:00 [app] <defunct> # 子進(jìn)程

消滅僵尸進(jìn)程的方法是殺死這個(gè)僵尸進(jìn)程的父進(jìn)程,這樣僵尸進(jìn)程的資源就被系統(tǒng)回收了.
kill -9僵尸進(jìn)程PID的方式是不能消滅僵尸進(jìn)程的,這個(gè)命令只對(duì)活著的進(jìn)程有效,僵尸進(jìn)程已經(jīng)死了,鞭尸是不能解決問題的。


1.5.4 進(jìn)程回收

為了避免僵尸進(jìn)程的產(chǎn)生,一般我們會(huì)在父進(jìn)程中進(jìn)行子進(jìn)程的資源回收
回收方式有兩種,一種是阻塞方式wait(),一種是非阻塞方式waitpid()。

1.5.4.1 wait()

這是個(gè)阻塞函數(shù),如果沒有子進(jìn)程退出, 函數(shù)會(huì)一直阻塞等待
當(dāng)檢測(cè)到子進(jìn)程退出了, 該函數(shù)阻塞解除回收子進(jìn)程資源。
這個(gè)函數(shù)被調(diào)用一次, 只能回收一個(gè)子進(jìn)程的資源,如有多個(gè)子進(jìn)程需要資源回收, 函數(shù)需被調(diào)用多次。

// 函數(shù)原型
// man 2 wait
#include <sys/wait.h>pid_t wait(int *status);
  • 參數(shù):傳出參數(shù),通過傳遞出的信息判斷回收的進(jìn)程是怎么退出的,如果不需要該信息可以指定為 NULL。取出整形變量中的數(shù)據(jù)需要使用一些宏函數(shù),具體操作方式如下:
    • WIFEXITED(status): 返回1, 進(jìn)程是正常退出的
    • WEXITSTATUS(status):得到進(jìn)程退出時(shí)候的狀態(tài)碼,相當(dāng)于 return 后邊的數(shù)值, 或者 exit()函數(shù)的參數(shù)
    • WIFSIGNALED(status): 返回1, 進(jìn)程是被信號(hào)殺死了
    • WTERMSIG(status): 獲得進(jìn)程是被哪個(gè)信號(hào)殺死的,會(huì)得到信號(hào)的編號(hào)
  • 返回值:
    • 成功:返回被回收的子進(jìn)程的進(jìn)程ID
    • 失敗: -1
      • 沒有子進(jìn)程資源可以回收了, 函數(shù)的阻塞會(huì)自動(dòng)解除, 返回-1
      • 回收子進(jìn)程資源的時(shí)候出現(xiàn)了異常

下面代碼演示了如何通過 wait()回收多個(gè)子進(jìn)程資源:

// wait 函數(shù)回收子進(jìn)程資源
#include <sys/wait.h>int main()
{pid_t pid;// 創(chuàng)建子進(jìn)程for(int i=0; i<5; ++i){pid = fork();if(pid == 0){break;}}// 父進(jìn)程if(pid > 0){// 需要保證父進(jìn)程一直在運(yùn)行while(1){// 回收子進(jìn)程的資源// 子進(jìn)程由多個(gè), 需要循環(huán)回收子進(jìn)程資源pid_t ret = wait(NULL);if(ret > 0){printf("成功回收了子進(jìn)程資源, 子進(jìn)程PID: %d\n", ret);}else{printf("回收失敗, 或者是已經(jīng)沒有子進(jìn)程了...\n");break;}printf("我是父進(jìn)程, pid=%d\n", getpid());}}else if(pid == 0){// 子進(jìn)程, 執(zhí)行這句代碼之后, 子進(jìn)程退出了printf("我是子進(jìn)程, pid=%d, 父進(jìn)程ID: %d\n", getpid(), getppid());}return 0;
}

1.5.4.2 waitpid()

waitpid() 函數(shù)可以看做是 wait() 函數(shù)的升級(jí)版,通過該函數(shù)可以控制回收子進(jìn)程資源的方式是阻塞還是非阻塞,另外還可以通過該函數(shù)進(jìn)行精準(zhǔn)打擊,可以精確指定回收某個(gè)或者某一類或者是全部子進(jìn)程資源。

// 函數(shù)原型
// man 2 waitpid
#include <sys/wait.h>
// 這個(gè)函數(shù)可以設(shè)置阻塞, 也可以設(shè)置為非阻塞
// 這個(gè)函數(shù)可以指定回收哪些子進(jìn)程的資源
pid_t waitpid(pid_t pid, int *status, int options);
  • 參數(shù):
    • pid:

      • -1:回收所有的子進(jìn)程資源, 和wait()是一樣的, 無差別回收,并不是一次性就可以回收多個(gè), 也是需要循環(huán)回收的
      • 大于0:指定回收某一個(gè)進(jìn)程的資源 ,pid是要回收的子進(jìn)程的進(jìn)程ID
      • 0:回收當(dāng)前進(jìn)程組的所有子進(jìn)程ID
      • 小于 -1:pid 的絕對(duì)值代表進(jìn)程組ID,表示要回收這個(gè)進(jìn)程組的所有子進(jìn)程資源
    • status: NULL, 和wait的參數(shù)是一樣的

    • options: 控制函數(shù)是阻塞還是非阻塞

      • 0: 函數(shù)行為是阻塞的 ==> 和wait一樣
      • WNOHANG: 函數(shù)行為是非阻塞的
  • 返回值
    • 如果函數(shù)是非阻塞的, 并且子進(jìn)程還在運(yùn)行, 返回0
    • 成功: 得到子進(jìn)程的進(jìn)程ID
    • 失敗: -1
      • 沒有子進(jìn)程資源可以回收了, 函數(shù)如果是阻塞的, 阻塞會(huì)解除, 直接返回-1
      • 回收子進(jìn)程資源的時(shí)候出現(xiàn)了異常

下面代碼演示了如何通過 waitpid()阻塞回收多個(gè)子進(jìn)程資源:

// 和wait() 行為一樣, 阻塞
#include <sys/wait.h>int main()
{pid_t pid;// 創(chuàng)建子進(jìn)程for(int i=0; i<5; ++i){pid = fork();if(pid == 0){break;}}// 父進(jìn)程if(pid > 0){// 需要保證父進(jìn)程一直在運(yùn)行while(1){// 回收子進(jìn)程的資源// 子進(jìn)程由多個(gè), 需要循環(huán)回收子進(jìn)程資源int status;pid_t ret = waitpid(-1, &status, 0);  // == wait(NULL);if(ret > 0){printf("成功回收了子進(jìn)程資源, 子進(jìn)程PID: %d\n", ret);// 判斷進(jìn)程是不是正常退出if(WIFEXITED(status)){printf("子進(jìn)程退出時(shí)候的狀態(tài)碼: %d\n", WEXITSTATUS(status));}if(WIFSIGNALED(status)){printf("子進(jìn)程是被這個(gè)信號(hào)殺死的: %d\n", WTERMSIG(status));}}else{printf("回收失敗, 或者是已經(jīng)沒有子進(jìn)程了...\n");break;}printf("我是父進(jìn)程, pid=%d\n", getpid());}}else if(pid == 0){// 子進(jìn)程, 執(zhí)行這句代碼之后, 子進(jìn)程退出了printf("===我是子進(jìn)程, pid=%d, 父進(jìn)程ID: %d\n", getpid(), getppid());}return 0;
}

下面代碼演示了如何通過 waitpid()非阻塞回收多個(gè)子進(jìn)程資源:

// 非阻塞處理
#include <sys/wait.h>int main()
{pid_t pid;// 創(chuàng)建子進(jìn)程for(int i=0; i<5; ++i){pid = fork();if(pid == 0){break;}}// 父進(jìn)程if(pid > 0){// 需要保證父進(jìn)程一直在運(yùn)行while(1){// 回收子進(jìn)程的資源// 子進(jìn)程由多個(gè), 需要循環(huán)回收子進(jìn)程資源// 子進(jìn)程退出了就回收, // 沒退出就不回收, 返回0int status;pid_t ret = waitpid(-1, &status, WNOHANG);  // 非阻塞if(ret > 0){printf("成功回收了子進(jìn)程資源, 子進(jìn)程PID: %d\n", ret);// 判斷進(jìn)程是不是正常退出if(WIFEXITED(status)){printf("子進(jìn)程退出時(shí)候的狀態(tài)碼: %d\n", WEXITSTATUS(status));}if(WIFSIGNALED(status)){printf("子進(jìn)程是被這個(gè)信號(hào)殺死的: %d\n", WTERMSIG(status));}}else if(ret == 0){printf("子進(jìn)程還沒有退出, 不做任何處理...\n");}else{printf("回收失敗, 或者是已經(jīng)沒有子進(jìn)程了...\n");break;}printf("我是父進(jìn)程, pid=%d\n", getpid());}}else if(pid == 0){// 子進(jìn)程, 執(zhí)行這句代碼之后, 子進(jìn)程退出了printf("===我是子進(jìn)程, pid=%d, 父進(jìn)程ID: %d\n", getpid(), getppid());}return 0;
}

2. 管道

2.1 管道

管道的是進(jìn)程間通信(IPC - InterProcess Communication)的一種方式,管道的本質(zhì)其實(shí)就是內(nèi)核中的一塊內(nèi)存(或者叫內(nèi)核緩沖區(qū))
這塊緩沖區(qū)中的數(shù)據(jù)存儲(chǔ)在一個(gè)環(huán)形隊(duì)列中,因?yàn)楣艿涝趦?nèi)核里邊,因此我們不能直接對(duì)其進(jìn)行任何操作。
在這里插入圖片描述

因?yàn)楣艿罃?shù)據(jù)是通過隊(duì)列來維護(hù)的,我們先來分析一個(gè)管道中數(shù)據(jù)的特點(diǎn):

  • 管道對(duì)應(yīng)的內(nèi)核緩沖區(qū)大小是固定的,默認(rèn)為4k(也就是隊(duì)列最大能存儲(chǔ)4k數(shù)據(jù))

  • 管道分為兩部分:讀端和寫端(隊(duì)列的兩端),數(shù)據(jù)從寫端進(jìn)入管道,從讀端流出管道。

  • 管道中的數(shù)據(jù)只能讀一次,做一次讀操作之后數(shù)據(jù)也就沒有了(讀數(shù)據(jù)相當(dāng)于出隊(duì)列)。

  • 管道是單工的:數(shù)據(jù)只能單向流動(dòng), 數(shù)據(jù)從寫端流向讀端。

  • 對(duì)管道的操作(讀、寫)默認(rèn)是阻塞的

    • 讀管道:管道中沒有數(shù)據(jù),讀操作被阻塞,當(dāng)管道中有數(shù)據(jù)之后阻塞才能解除
    • 寫管道:管道被寫滿了,寫數(shù)據(jù)的操作被阻塞,當(dāng)管道變?yōu)椴粷M的狀態(tài),寫阻塞解除

管道在內(nèi)核中, 不能直接對(duì)其進(jìn)行操作,通過什么方式去讀寫管道呢?
其實(shí)管道操作就是文件IO操作,內(nèi)核中管道的兩端分別對(duì)應(yīng)兩個(gè)文件描述符
通過寫端的文件描述符把數(shù)據(jù)寫入到管道中,通過讀端的文件描述符將數(shù)據(jù)從管道中讀出來。
讀寫管道的函數(shù)就是Linux中的文件IO函數(shù) read/write

// 讀管道
ssize_t read(int fd, void *buf, size_t count);
// 寫管道的函數(shù)
ssize_t write(int fd, const void *buf, size_t count);

分析一下為什么可以使用管道進(jìn)行進(jìn)程間通信,看一下圖片:
在這里插入圖片描述

在上圖中假設(shè)父進(jìn)程一系列操作:可以通過文件描述符表中的文件描述符fd3寫管道,通過fd4讀管道,然后再通過 fork() 創(chuàng)建出子進(jìn)程,那么在父進(jìn)程中被分配的文件描述符 fd3, fd4也就被拷貝到子進(jìn)程中,子進(jìn)程通過 fd3可以將數(shù)據(jù)寫入到內(nèi)核的管道中,通過fd4將數(shù)據(jù)從管道中讀出來。

也就是說管道是獨(dú)立于任何進(jìn)程的,并且充當(dāng)了兩個(gè)進(jìn)程用于數(shù)據(jù)通信的載體,只要兩個(gè)進(jìn)程能夠得到同一個(gè)管道的入口和出口(讀端和寫端的文件描述符),那么他們之間就可以通過管道進(jìn)行數(shù)據(jù)的交互。


2.2 匿名管道

2.2.1 創(chuàng)建匿名管道

匿名管道是管道的一種,既然是匿名也就是說這個(gè)管道沒有名字,但其本質(zhì)不變,就是位于內(nèi)核中的一塊內(nèi)存,匿名管道擁有上面介紹的管道的所有特性
額外我們需知,匿名管道只能實(shí)現(xiàn)有血緣關(guān)系的進(jìn)程間通信,如:父子進(jìn)程,兄弟進(jìn)程,爺孫進(jìn)程,叔侄進(jìn)程。

// 創(chuàng)建匿名函數(shù)的函數(shù)原型
#include <unistd.h>
// 創(chuàng)建一個(gè)匿名的管道, 得到兩個(gè)可用的文件描述符
int pipe(int pipefd[2]);
  • 參數(shù):傳出參數(shù),需要傳遞一個(gè)整形數(shù)組的地址,數(shù)組大小為 2,也就是說最終會(huì)傳出兩個(gè)元素
    • pipefd[0]: 對(duì)應(yīng)管道讀端的文件描述符,通過它可以將數(shù)據(jù)從管道中讀出
    • pipefd[1]: 對(duì)應(yīng)管道寫端的文件描述符,通過它可以將數(shù)據(jù)寫入到管道中
  • 返回值:成功返回 0,失敗返回 -1

2.2.2 進(jìn)程間通信

使用匿名管道只能夠?qū)崿F(xiàn)有血緣關(guān)系的進(jìn)程間通信,要求寫一段程序完成下邊的功能:

需求描述:在父進(jìn)程中創(chuàng)建一個(gè)子進(jìn)程, 父子進(jìn)程分別執(zhí)行不同的操作:- 子進(jìn)程: 執(zhí)行一個(gè)shell命令 "ps aux", 將命令的結(jié)果傳遞給父進(jìn)程- 父進(jìn)程: 將子進(jìn)程命令的結(jié)果輸出到終端

需求分析:

  • 子進(jìn)程中執(zhí)行shell命令相當(dāng)于啟動(dòng)一個(gè)磁盤程序,因此需要使用 execl()/execlp()函數(shù)
    • execlp(“ps”, “ps”, “aux”, NULL)
  • 子進(jìn)程中執(zhí)行完shell命令直接就可以在終端輸出結(jié)果,如果將這些信息傳遞給父進(jìn)程呢?
    • 數(shù)據(jù)傳遞需要使用管道,子進(jìn)程需要將數(shù)據(jù)寫入到管道中
    • 將默認(rèn)輸出到終端的數(shù)據(jù)寫入到管道就需要進(jìn)行輸出的重定向,需要使用 dup2() 做這件事情
      • dup2(fd[1], STDOUT_FILENO);
  • 父進(jìn)程需要讀管道,將從管道中讀出的數(shù)據(jù)打印到終端
  • 父進(jìn)程最后需要釋放子進(jìn)程資源,防止出現(xiàn)僵尸進(jìn)程

在使用管道進(jìn)行進(jìn)程間通信的注意事項(xiàng):必須要保證數(shù)據(jù)在管道中的單向流動(dòng)。
這句話怎么理解呢,通過下面的圖來分析一下:

第一步: 在父進(jìn)程中創(chuàng)建了匿名管道,得到了兩個(gè)分配的文件描述符,fd3操作管道的讀端,fd4操作管道的寫端。

在這里插入圖片描述

第二步:父進(jìn)程創(chuàng)建子進(jìn)程,父進(jìn)程的文件描述符被拷貝,在子進(jìn)程的文件描述符表中也得到了兩個(gè)被分配的可以使用的文件描述符,通過fd3讀管道,通過fd4寫管道。通過下圖可以看到管道中數(shù)據(jù)的流動(dòng)不是單向的,有以下這么幾種情況:

父進(jìn)程通過fd4將數(shù)據(jù)寫入管道,然后父進(jìn)程再通過fd3將數(shù)據(jù)從管道中讀出
父進(jìn)程通過fd4將數(shù)據(jù)寫入管道,然后子進(jìn)程再通過fd3將數(shù)據(jù)從管道中讀出
子進(jìn)程通過fd4將數(shù)據(jù)寫入管道,然后子進(jìn)程再通過fd3將數(shù)據(jù)從管道中讀出
子進(jìn)程通過fd4將數(shù)據(jù)寫入管道,然后父進(jìn)程再通過fd3將數(shù)據(jù)從管道中讀出
前邊說到過,管道行為默認(rèn)是阻塞的,假設(shè)子進(jìn)程通過寫端將數(shù)據(jù)寫入管道,父進(jìn)程的讀端將數(shù)據(jù)讀出,這樣子進(jìn)程的讀端就讀不到數(shù)據(jù),導(dǎo)致子進(jìn)程阻塞在讀管道的操作上,這樣就會(huì)給程序的執(zhí)行造成一些不必要的影響。
如果我們本來也沒有打算讓進(jìn)程讀或者寫管道,那么就可以將進(jìn)程操作的讀端或者寫端關(guān)閉。

在這里插入圖片描述

第三步:為了避免兩個(gè)進(jìn)程都讀管道,但是可能其中某個(gè)進(jìn)程由于讀不到數(shù)據(jù)而阻塞的情況,我們可以關(guān)閉進(jìn)程中用不到的那一端的文件描述符,這樣數(shù)據(jù)就只能單向的從一端流向另外一端了,如下圖,我們關(guān)閉了父進(jìn)程的寫端,關(guān)閉了子進(jìn)程的讀端:

在這里插入圖片描述

根據(jù)上面的分析,最終可以寫出下面的代碼:

// 管道的數(shù)據(jù)是單向流動(dòng)的:
// 操作管道的是兩個(gè)進(jìn)程, 進(jìn)程A讀管道, 需要關(guān)閉管道的寫端, 進(jìn)程B寫管道, 需要關(guān)閉管道的讀端
// 如果不做上述的操作, 會(huì)對(duì)程序的結(jié)果造成一些影響, 對(duì)管道的操作無法結(jié)束
#include <fcntl.h>
#include <sys/wait.h>int main()
{// 1. 創(chuàng)建匿名管道, 得到兩個(gè)文件描述符int fd[2];int ret = pipe(fd);if(ret == -1){perror("pipe");exit(0);}// 2. 創(chuàng)建子進(jìn)程 -> 能夠操作管道的文件描述符被復(fù)制到子進(jìn)程中pid_t pid = fork();if(pid == 0){// 關(guān)閉讀端close(fd[0]);// 3. 在子進(jìn)程中執(zhí)行 execlp("ps", "ps", "aux", NULL);// 在子進(jìn)程中完成輸出的重定向, 原來輸出到終端現(xiàn)在要寫管道// 進(jìn)程打印數(shù)據(jù)默認(rèn)輸出到終端, 終端對(duì)應(yīng)的文件描述符: stdout_fileno// 標(biāo)準(zhǔn)輸出 重定向到 管道的寫端dup2(fd[1], STDOUT_FILENO);execlp("ps", "ps", "aux", NULL);perror("execlp");}// 4. 父進(jìn)程讀管道else if(pid > 0){// 關(guān)閉管道的寫端close(fd[1]);// 5. 父進(jìn)程打印讀到的數(shù)據(jù)信息char buf[4096];// 讀管道// 如果管道中沒有數(shù)據(jù), read會(huì)阻塞// 有數(shù)據(jù)之后, read解除阻塞, 直接讀數(shù)據(jù)// 需要循環(huán)讀數(shù)據(jù), 管道是有容量的, 寫滿之后就不寫了// 數(shù)據(jù)被讀走之后, 繼續(xù)寫管道, 那么就需要再繼續(xù)讀數(shù)據(jù)while(1){memset(buf, 0, sizeof(buf));int len = read(fd[0], buf, sizeof(buf));if(len == 0){// 管道的寫端關(guān)閉了, 如果管道中沒有數(shù)據(jù), 管道讀端不會(huì)阻塞// 沒數(shù)據(jù)直接返回0, 如果有數(shù)據(jù), 將數(shù)據(jù)讀出, 數(shù)據(jù)讀完之后返回0break;}printf("%s, len = %d\n", buf, len);}close(fd[0]);// 回收子進(jìn)程資源wait(NULL);}return 0;
}

2.3 有名管道

2.3.1 創(chuàng)建有名管道

有名管道擁有管道的所有特性,之所以稱為有名是因管道在磁盤上有實(shí)體文件, 文件類型為p有名管道文件大小永為0,因有名管道是將數(shù)據(jù)存儲(chǔ)到內(nèi)存的緩沖區(qū)中,打開這個(gè)磁盤上的管道文件就可以得到操作有名管道的文件描述符,通過文件描述符讀寫管道存儲(chǔ)在內(nèi)核中的數(shù)據(jù)。

有名管道也可稱為 fifo (first in first out),有名管道既可進(jìn)行有血緣關(guān)系的進(jìn)程間通信,也可進(jìn)行沒有血緣關(guān)系的進(jìn)程間通信。
創(chuàng)建有名管道的方式有兩種,一種是通過命令,一種是通過函數(shù)。

  • 通過命令
$ mkfifo 有名管道的名字
  • 通過函數(shù)
#include <sys/types.h>
#include <sys/stat.h>
// int open(const char *pathname, int flags, mode_t mode);
int mkfifo(const char *pathname, mode_t mode);
  • 參數(shù):
    • pathname: 要?jiǎng)?chuàng)建的有名管道的名字
    • mode: 文件的操作權(quán)限, 和open()的第三個(gè)參數(shù)一個(gè)作用,最終權(quán)限: (mode & ~umask)
  • 返回值:創(chuàng)建成功返回 0,失敗返回 -1

2.3.2 進(jìn)程間通信

不管有血緣關(guān)系還是沒血緣關(guān)系,使用有名管道實(shí)現(xiàn)進(jìn)程間通信的方式是相同的
就是在兩個(gè)進(jìn)程中分別以讀、寫的方式打開磁盤上的管道文件
得到用于讀管道、寫管道的文件描述符,就可調(diào)用對(duì)應(yīng)的read()、write()函數(shù)進(jìn)行讀寫操作了。

有名管道操作需要通過 open() 操作得到讀寫管道的文件描述符,如果只是讀端打開了或者只是寫端打開了,進(jìn)程會(huì)阻塞在這里不會(huì)向下執(zhí)行
直到在另一個(gè)進(jìn)程中將管道的對(duì)端打開,當(dāng)前進(jìn)程的阻塞也就解除了。
所以當(dāng)發(fā)現(xiàn)進(jìn)程阻塞在了open()函數(shù)上不要感到驚訝?!?/p>

  • 寫管道的進(jìn)程
1. 創(chuàng)建有名管道文件 mkfifo()
2. 打開有名管道文件, 打開方式是 o_wronlyint wfd = open("xx", O_WRONLY);
3. 調(diào)用write函數(shù)寫文件 ==> 數(shù)據(jù)被寫入管道中write(wfd, data, strlen(data));
4. 寫完之后關(guān)閉文件描述符close(wfd);
#include <fcntl.h>
#include <sys/stat.h>int main()
{// 1. 創(chuàng)建有名管道文件int ret = mkfifo("./testfifo", 0664);if(ret == -1){perror("mkfifo");exit(0);}printf("管道文件創(chuàng)建成功...\n");// 2. 打開管道文件// 因?yàn)橐獙懝艿? 所有打開方式, 應(yīng)該指定為 O_WRONLY// 如果先打開寫端, 讀端還沒有打開, open函數(shù)會(huì)阻塞, 當(dāng)讀端也打開之后, open解除阻塞int wfd = open("./testfifo", O_WRONLY);if(wfd == -1){perror("open");exit(0);}printf("以只寫的方式打開文件成功...\n");// 3. 循環(huán)寫管道int i = 0;while(i<100){char buf[1024];sprintf(buf, "hello, fifo, 我在寫管道...%d\n", i);write(wfd, buf, strlen(buf));i++;sleep(1);}close(wfd);return 0;
}
  • 讀管道的進(jìn)程
1. 這兩個(gè)進(jìn)程需要操作相同的管道文件
2. 打開有名管道文件, 打開方式是 o_rdonlyint rfd = open("xx", O_RDONLY);
3. 調(diào)用read函數(shù)讀文件 ==> 讀管道中的數(shù)據(jù)char buf[4096];read(rfd, buf, sizeof(buf));
4. 讀完之后關(guān)閉文件描述符close(rfd);
#include <fcntl.h>
#include <sys/stat.h>int main()
{// 1. 打開管道文件// 因?yàn)橐猺ead管道, so打開方式, 應(yīng)該指定為 O_RDONLY// 如果只打開了讀端, 寫端還沒有打開, open阻塞, 當(dāng)寫端被打開, 阻塞就解除了int rfd = open("./testfifo", O_RDONLY);if(rfd == -1){perror("open");exit(0);}printf("以只讀的方式打開文件成功...\n");// 2. 循環(huán)讀管道while(1){char buf[1024];memset(buf, 0, sizeof(buf));// 讀是阻塞的, 如果管道中沒有數(shù)據(jù), read自動(dòng)阻塞// 有數(shù)據(jù)解除阻塞, 繼續(xù)讀數(shù)據(jù)int len = read(rfd, buf, sizeof(buf));printf("讀出的數(shù)據(jù): %s\n", buf);if(len == 0){// 寫端關(guān)閉了, read解除阻塞返回0printf("管道的寫端已經(jīng)關(guān)閉, 拜拜...\n");break;}}close(rfd);return 0;
}

2.4 管道的讀寫行為

關(guān)于管道不管是有名的還是匿名,在讀寫時(shí),它們表現(xiàn)出的行為是一致的
下面是對(duì)其讀寫行為的總結(jié):

  • 讀管道,需要根據(jù)寫端的狀態(tài)進(jìn)行分析:
    • 寫端沒有關(guān)閉 (操作管道寫端的文件描述符沒有被關(guān)閉)
      • 如果管道中沒有數(shù)據(jù) ==> 讀阻塞, 如果管道中被寫入了數(shù)據(jù), 阻塞解除
      • 如果管道中有數(shù)據(jù) ==> 不阻塞,管道中的數(shù)據(jù)被讀完了, 再繼續(xù)讀管道還會(huì)阻塞
    • 寫端已經(jīng)關(guān)閉了 (沒有可用的文件描述符可以寫管道了)
      • 管道中沒有數(shù)據(jù) ==> 讀端解除阻塞, read函數(shù)返回0
      • 管道中有數(shù)據(jù) ==> read先將數(shù)據(jù)讀出, 數(shù)據(jù)讀完之后返回0, 不會(huì)阻塞了
  • 寫管道,需要根據(jù)讀端的狀態(tài)進(jìn)行分析:
    • 讀端沒有關(guān)閉
      • 如果管道有存儲(chǔ)的空間, 一直寫數(shù)據(jù)
      • 如果管道寫滿了, 寫操作就阻塞, 當(dāng)讀端將管道數(shù)據(jù)讀走了, 解除阻塞繼續(xù)寫
    • 讀端關(guān)閉了,管道破裂(異常), 進(jìn)程直接退出

管道的兩端默認(rèn)是阻塞的,如何將管道設(shè)置為非阻塞呢?
管道的讀寫兩端的非阻塞操作是相同的,下面的代碼中將匿名的讀端設(shè)置為了非阻塞:

// 通過fcntl 修改就可以, 一般情況下不建議修改
// 管道操作對(duì)應(yīng)兩個(gè)文件描述符, 分別是管道的讀端 和 寫端// 1. 獲取讀端的文件描述符的flag屬性
int flag = fcntl(fd[0], F_GETFL);
// 2. 添加非阻塞屬性到 flag中
flag |= O_NONBLOCK;
// 3. 將新的flag屬性設(shè)置給讀端的文件描述符
fcntl(fd[0], F_SETFL, flag);
// 4. 非阻塞讀管道
char buf[4096];
read(fd[0], buf, sizeof(buf));

3. 內(nèi)存映射(mmap)

3.1 創(chuàng)建內(nèi)存映射區(qū)

想要實(shí)現(xiàn)進(jìn)程間通信,可通過函數(shù)創(chuàng)建一塊內(nèi)存映射區(qū)
和管道不同的是管道對(duì)應(yīng)的內(nèi)存空間在內(nèi)核中,而內(nèi)存映射區(qū)對(duì)應(yīng)的內(nèi)存空間在進(jìn)程的用戶區(qū)(用于加載動(dòng)態(tài)庫的那個(gè)區(qū)域)
也就是說進(jìn)程間通信使用的內(nèi)存映射區(qū)不是一塊,而是在每個(gè)進(jìn)程內(nèi)部都有一塊。

由于每個(gè)進(jìn)程的地址空間是獨(dú)立的,各個(gè)進(jìn)程之間也不能直接訪問對(duì)方的內(nèi)存映射區(qū),要通信的進(jìn)程需將各自的內(nèi)存映射區(qū)和同一個(gè)磁盤文件進(jìn)行映射,這樣進(jìn)程之間就可以通過磁盤文件這個(gè)唯一的橋梁完成數(shù)據(jù)的交互了。
在這里插入圖片描述

如上圖所示:
磁盤文件數(shù)據(jù)可完全加載到進(jìn)程的內(nèi)存映射區(qū)也可部分加載到進(jìn)程的內(nèi)存映射區(qū),當(dāng)進(jìn)程A中的內(nèi)存映射區(qū)數(shù)據(jù)被修改了,數(shù)據(jù)會(huì)被自動(dòng)同步到磁盤文件,同時(shí)和磁盤文件建立映射關(guān)系的其他進(jìn)程內(nèi)存映射區(qū)中的數(shù)據(jù)也會(huì)和磁盤文件進(jìn)行數(shù)據(jù)的實(shí)時(shí)同步,這個(gè)同步機(jī)制保障了各進(jìn)程間的數(shù)據(jù)共享。

使用內(nèi)存映射區(qū)既可以進(jìn)程有血緣關(guān)系的進(jìn)程間通信也可以進(jìn)程沒有血緣關(guān)系的進(jìn)程間通信。
創(chuàng)建內(nèi)存映射區(qū)的函數(shù)原型如下:

#include <sys/mman.h>
// 創(chuàng)建內(nèi)存映射區(qū)
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
  • 參數(shù):
    • addr: 從動(dòng)態(tài)庫加載區(qū)的什么位置開始創(chuàng)建內(nèi)存映射區(qū),一般定為NULL, 委托內(nèi)核分配

    • length: 創(chuàng)建的內(nèi)存映射區(qū)的大小(byte),實(shí)際上這個(gè)大小是按4k的整數(shù)倍去分配的

    • prot: 對(duì)內(nèi)存映射區(qū)的操作權(quán)限

      • PROT_READ: 讀內(nèi)存映射區(qū)
      • PROT_WRITE: 寫內(nèi)存映射區(qū)
      • 如果要對(duì)映射區(qū)有讀寫權(quán)限: PROT_READ | PROT_WRITE
    • flags:

      • MAP_SHARED: 多個(gè)進(jìn)程可以共享數(shù)據(jù),進(jìn)行映射區(qū)數(shù)據(jù)同步
      • MAP_PRIVATE: 映射區(qū)數(shù)據(jù)是私有的,不能同步給其他進(jìn)程
    • fd: 文件描述符, 對(duì)應(yīng)一個(gè)打開的磁盤文件,內(nèi)存映射區(qū)通過這個(gè)文件描述符和磁盤文件建立關(guān)聯(lián)

    • offset: 磁盤文件的偏移量,文件從偏移到的位置開始進(jìn)行數(shù)據(jù)映射
      使用這個(gè)參數(shù)需要注意兩個(gè)問題:

      • 偏移量必須是4k的整數(shù)倍, 寫0代表不偏移
      • 這個(gè)參數(shù)必須大于 0
  • 返回值:
    • 成功: 返回一個(gè)內(nèi)存映射區(qū)的起始地址
    • 失敗: MAP_FAILED(that is, (void *) -1)

mmap() 函數(shù)的參數(shù)較多,在使用該函數(shù)創(chuàng)建用于進(jìn)程間通信的內(nèi)存映射區(qū)的時(shí)候,各參數(shù)的指定有一些注意事項(xiàng)

  1. 第一個(gè)參數(shù) addr 指定為 NULL 即可
  2. 第二個(gè)參數(shù) length 必須要 > 0
  3. 第三個(gè)參數(shù) prot,進(jìn)程間通信需要對(duì)內(nèi)存映射區(qū)有讀寫權(quán)限,因此需要指定為:
    PROT_READ | PROT_WRITE
  4. 第四個(gè)參數(shù) flags,如果要進(jìn)行進(jìn)程間通信, 需要指定 MAP_SHARED
  5. 第五個(gè)參數(shù) fd,打開的文件必須大于0,進(jìn)程間通信需要文件操作權(quán)限和映射區(qū)操作權(quán)限相同
    • 內(nèi)存映射區(qū)創(chuàng)建成功之后, 關(guān)閉這個(gè)文件描述符不會(huì)影響進(jìn)程間通信
  6. 第六個(gè)參數(shù) offset,不偏移指定為0,如果偏移必須是4k的整數(shù)倍

內(nèi)存映射區(qū)使用完之后也需要釋放,釋放函數(shù)原型如下:

int munmap(void *addr, size_t length);
  • 參數(shù):
    • addr: mmap()的返回值, 創(chuàng)建的內(nèi)存映射區(qū)的起始地址
    • length: 和mmap()第二個(gè)參數(shù)相同即可
  • 返回值:函數(shù)調(diào)用成功返回 0,失敗返回 -1

3.2 進(jìn)程間通信

操作內(nèi)存映射區(qū)和操作管道是不一樣的,
內(nèi)存映射區(qū)是直接對(duì)內(nèi)存地址進(jìn)行操作,管道是通過文件描述符讀寫隊(duì)列中的數(shù)據(jù)
管道的讀寫是阻塞的,內(nèi)存映射區(qū)的讀寫是非阻塞的。

3.2.1 有血緣關(guān)系

由于創(chuàng)建子進(jìn)程會(huì)發(fā)生虛擬地址空間的復(fù)制,那么在父進(jìn)程中創(chuàng)建的內(nèi)存映射區(qū)也會(huì)被復(fù)制到子進(jìn)程中,這樣在子進(jìn)程里邊就可以直接使用這塊內(nèi)存映射區(qū)了,所以對(duì)于有血緣關(guān)系的進(jìn)程,進(jìn)行進(jìn)程間通信是非常簡(jiǎn)單的,處理代碼如下:

1.  先創(chuàng)建內(nèi)存映射區(qū), 得到一個(gè)起始地址, 假設(shè)使用ptr指針保存這個(gè)地址
2.  通過fork()創(chuàng)建子進(jìn)程->子進(jìn)程中也就有一個(gè)內(nèi)存映射區(qū), 子進(jìn)程中也有一個(gè)ptr指針指向這個(gè)地址
3.  父進(jìn)程往自己的內(nèi)存映射區(qū)寫數(shù)據(jù), 數(shù)據(jù)同步到了磁盤文件中磁盤文件數(shù)據(jù)又同步到子進(jìn)程的映射區(qū)中子進(jìn)程從自己的映射區(qū)往外讀數(shù)據(jù), 這個(gè)數(shù)據(jù)就是父進(jìn)程寫的
#include <sys/mman.h>
#include <fcntl.h>int main()
{// 1. 打開一個(gè)磁盤文件int fd = open("./english.txt", O_RDWR);// 2. 創(chuàng)建內(nèi)存映射區(qū)void* ptr = mmap(NULL, 4000, PROT_READ|PROT_WRITE,MAP_SHARED, fd, 0);if(ptr == MAP_FAILED){perror("mmap");exit(0);}// 3. 創(chuàng)建子進(jìn)程pid_t pid = fork();if(pid > 0){// 父進(jìn)程, 寫數(shù)據(jù)const char* pt = "我你爹, 你我兒?";memcpy(ptr, pt, strlen(pt)+1);}else if(pid == 0){// 子進(jìn)程, 讀數(shù)據(jù)usleep(1);	// 內(nèi)存映射區(qū)不阻塞, 為了讓子進(jìn)程讀出數(shù)據(jù)printf("從映射區(qū)讀出的數(shù)據(jù): %s\n", (char*)ptr);}// 釋放內(nèi)存映射區(qū)munmap(ptr, 4000);return 0;
}

3.2.2 沒有血緣關(guān)系

對(duì)于沒有血緣關(guān)系的進(jìn)程間通信,需要在每個(gè)進(jìn)程中分別創(chuàng)建內(nèi)存映射區(qū),但是這些進(jìn)程的內(nèi)存映射區(qū)必須要關(guān)聯(lián)相同的磁盤文件,這樣才能實(shí)現(xiàn)進(jìn)程間的數(shù)據(jù)同步。

進(jìn)程A的測(cè)試代碼:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/mman.h>
#include <fcntl.h>int main()
{// 1. 打開一個(gè)磁盤文件int fd = open("./english.txt", O_RDWR);// 2. 創(chuàng)建內(nèi)存映射區(qū)void* ptr = mmap(NULL, 4000, PROT_READ|PROT_WRITE,MAP_SHARED, fd, 0);if(ptr == MAP_FAILED){perror("mmap");exit(0);}const char* pt = "我你爹, 你我兒?";memcpy(ptr, pt, strlen(pt)+1);// 釋放內(nèi)存映射區(qū)munmap(ptr, 4000);return 0;
}

進(jìn)程B的測(cè)試代碼:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/mman.h>
#include <fcntl.h>int main()
{// 1. 打開一個(gè)磁盤文件int fd = open("./english.txt", O_RDWR);// 2. 創(chuàng)建內(nèi)存映射區(qū)void* ptr = mmap(NULL, 4000, PROT_READ|PROT_WRITE,MAP_SHARED, fd, 0);if(ptr == MAP_FAILED){perror("mmap");exit(0);}// 讀內(nèi)存映射區(qū)printf("從映射區(qū)讀出的數(shù)據(jù): %s\n", (char*)ptr);// 釋放內(nèi)存映射區(qū)munmap(ptr, 4000);return 0;
}

3.3 拷貝文件

用內(nèi)存映射區(qū)除了可實(shí)現(xiàn)進(jìn)程間通信,也可進(jìn)行文件的拷貝,用這種方式拷貝文件可減少工作量,我們只需負(fù)責(zé)創(chuàng)建內(nèi)存映射區(qū)和打開磁盤文件,關(guān)于文件中的數(shù)據(jù)讀寫就無需關(guān)心了。

使用內(nèi)存映射區(qū)拷貝文件思路:

  1. 打開被拷貝文件,得到文件描述符 fd1,并計(jì)算出這個(gè)文件的大小 size
  2. 創(chuàng)建內(nèi)存映射區(qū)A并且和被拷貝文件關(guān)聯(lián),也就是和fd1關(guān)聯(lián)起來,得到映射區(qū)地址 ptrA
  3. 創(chuàng)建新文件,得到文件描述符fd2,用于存儲(chǔ)被拷貝的數(shù)據(jù),并且將這個(gè)文件大小拓展為 size
  4. 創(chuàng)建內(nèi)存映射區(qū)B并且和新創(chuàng)建的文件關(guān)聯(lián),也就是和fd2關(guān)聯(lián)起來,得到映射區(qū)地址 ptrB
  5. 進(jìn)程地址空間之間的數(shù)據(jù)拷貝,memcpy(ptrB, ptrA,size),數(shù)據(jù)自動(dòng)同步到新建文件中
  6. 關(guān)閉內(nèi)存映射區(qū)

文件拷貝示例代碼如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>int main()
{// 1. 打開一個(gè)操盤文件english.txt得到文件描述符int fd = open("./english.txt", O_RDWR);// 計(jì)算文件大小int size = lseek(fd, 0, SEEK_END);// 2. 創(chuàng)建內(nèi)存映射區(qū)和english.txt進(jìn)行關(guān)聯(lián), 得到映射區(qū)起始地址void* ptrA = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);if(ptrA == MAP_FAILED){perror("mmap");exit(0);}// 3. 創(chuàng)建一個(gè)新文件, 存儲(chǔ)拷貝的數(shù)據(jù)int fd1 = open("./copy.txt", O_RDWR|O_CREAT, 0664);// 拓展這個(gè)新文件ftruncate(fd1, size);// 4. 創(chuàng)建一個(gè)映射區(qū)和新文件進(jìn)行關(guān)聯(lián), 得到映射區(qū)的起始地址secondvoid* ptrB = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd1, 0);if(ptrB == MAP_FAILED){perror("mmap----");exit(0);}// 5. 使用memcpy拷貝映射區(qū)數(shù)據(jù)// 這兩個(gè)指針指向兩塊內(nèi)存, 都是內(nèi)存映射區(qū)// 指針指向有效的內(nèi)存, 拷貝的是內(nèi)存中的數(shù)據(jù)memcpy(ptrB, ptrA, size);// 6. 釋放內(nèi)存映射區(qū)munmap(ptrA, size);munmap(ptrB, size);close(fd);close(fd1);return 0;
}

4. 共享內(nèi)存

共享內(nèi)存不同于內(nèi)存映射區(qū),它不屬于任何進(jìn)程,并且不受進(jìn)程生命周期的影響。
通過調(diào)用Linux提供的系統(tǒng)函數(shù)就可得到這塊共享內(nèi)存。
使用前需讓進(jìn)程和共享內(nèi)存進(jìn)行關(guān)聯(lián),得到共享內(nèi)存的起始地址之后就可直接進(jìn)行讀寫操作了,進(jìn)程也可以和這塊共享內(nèi)存解除關(guān)聯(lián), 解除關(guān)聯(lián)之后就不能操作這塊共享內(nèi)存了。
在所有進(jìn)程間通信的方式中共享內(nèi)存的效率是最高的。

共享內(nèi)存操作默認(rèn)不阻塞,如果多個(gè)進(jìn)程同時(shí)讀寫共享內(nèi)存,可能出現(xiàn)數(shù)據(jù)混亂,共享內(nèi)存需要借助其他機(jī)制來保證進(jìn)程間的數(shù)據(jù)同步,比如:信號(hào)量.
共享內(nèi)存內(nèi)部沒有提供這種機(jī)制。

4.1 創(chuàng)建/打開共享內(nèi)存

4.1.1 shmget

如共享內(nèi)存不存在就需先創(chuàng)建出來,如已存在就需先打開這塊共享內(nèi)存。
不管是創(chuàng)建還是打開共享內(nèi)存使用的函數(shù)是同一個(gè)

// 函數(shù)原型
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
  • 參數(shù):
    • key: 類型 key_t 是個(gè)整形數(shù), 通過這個(gè)key可以創(chuàng)建或者打開一塊共享內(nèi)存,該參數(shù)的值一定要大于0
    • size: 創(chuàng)建共享內(nèi)存的時(shí)候, 指定共享內(nèi)存的大小,如果是打開一塊存在的共享內(nèi)存, size是沒有意義的
    • shmflg:創(chuàng)建共享內(nèi)存的時(shí)候指定的屬性
      • IPC_CREAT: 創(chuàng)建新的共享內(nèi)存,如果創(chuàng)建共享內(nèi)存, 需要指定對(duì)共享內(nèi)存的操作權(quán)限,比如:IPC_CREAT | 0664
      • IPC_EXCL: 檢測(cè)共享內(nèi)存是否已經(jīng)存在了,必須和 IPC_CREAT一起使用
  • 返回值:共享內(nèi)存創(chuàng)建或者打開成功返回標(biāo)識(shí)共享內(nèi)存的唯一的ID,失敗返回-1

函數(shù)使用舉例:

場(chǎng)景1:創(chuàng)建一塊大小為4k的共享內(nèi)存

shmget(100, 4096, IPC_CREAT|0664);

場(chǎng)景2:創(chuàng)建一塊大小為4k的共享內(nèi)存, 并且檢測(cè)是否存在

// 	如果共享內(nèi)存已經(jīng)存在, 共享內(nèi)存創(chuàng)建失敗, 返回-1, 可以perror() 打印錯(cuò)誤信息
shmget(100, 4096, IPC_CREAT|0664|IPC_EXCL);

場(chǎng)景3:打開一塊已經(jīng)存在的共享內(nèi)存

// 函數(shù)參數(shù)雖然指定了大小和IPC_CREAT, 但是都不起作用
// 因?yàn)楣蚕韮?nèi)存已經(jīng)存在, 只能打開, 參數(shù)4096也沒有意義
shmget(100, 4096, IPC_CREAT|0664);
shmget(100, 0, 0);

場(chǎng)景4:打開一塊共享內(nèi)存, 如果不存在就創(chuàng)建

shmget(100, 4096, IPC_CREAT|0664);

4.1.2 ftok

shmget() 函數(shù)的第一個(gè)參數(shù)是一個(gè)大于0的正整數(shù),如果不想自己指定可以通過 ftok()函數(shù)直接生成這個(gè)key值。

// ftok函數(shù)原型
#include <sys/types.h>
#include <sys/ipc.h>// 將兩個(gè)參數(shù)作為種子, 生成一個(gè) key_t 類型的數(shù)值
key_t ftok(const char *pathname, int proj_id);
  • 參數(shù):

    • pathname: 當(dāng)前操作系統(tǒng)中一個(gè)存在的路徑

    • proj_id: 這個(gè)參數(shù)只用到了int中的一個(gè)字節(jié)
      傳參的時(shí)候要將其作為 char 進(jìn)行操作,取值范圍: 1-255

  • 返回值:函數(shù)調(diào)用成功返回一個(gè)可用于創(chuàng)建、打開共享內(nèi)存的key值,調(diào)用失敗返回-1

使用舉例:

// 根據(jù)路徑生成一個(gè)key_t
key_t key = ftok("/home/robin", 'a');
// 創(chuàng)建或打開共享內(nèi)存
shmget(key, 4096, IPC_CREATE|0664);

4.2 關(guān)聯(lián)和解除關(guān)聯(lián)

4.2.1 shmat

創(chuàng)建/打開共享內(nèi)存之后還必須和共享內(nèi)存進(jìn)行關(guān)聯(lián),這樣才能得到共享內(nèi)存的起始地址,通過得到的內(nèi)存地址進(jìn)行數(shù)據(jù)的讀寫操作
關(guān)聯(lián)函數(shù)的原型如下:

void *shmat(int shmid, const void *shmaddr, int shmflg);
  • 參數(shù):
    • shmid: 要操作的共享內(nèi)存的ID, 是 shmget() 函數(shù)的返回值
    • shmaddr: 共享內(nèi)存的起始地址, 用戶不知道, 需要讓內(nèi)核指定, 寫NULL
    • shmflg: 和共享內(nèi)存關(guān)聯(lián)的對(duì)共享內(nèi)存的操作權(quán)限
      • SHM_RDONLY: 讀權(quán)限, 只能讀共享內(nèi)存中的數(shù)據(jù)
      • 0: 讀寫權(quán)限,可以讀寫共享內(nèi)存數(shù)據(jù)
  • 返回值:關(guān)聯(lián)成功,返回值共享內(nèi)存的起始地址,關(guān)聯(lián)失敗返回 (void *) -1

4.2.2 shmdt

當(dāng)進(jìn)程不需要再操作共享內(nèi)存,可以讓進(jìn)程和共享內(nèi)存解除關(guān)聯(lián)
另外如果沒有執(zhí)行該操作,進(jìn)程退出之后,結(jié)束的進(jìn)程和共享內(nèi)存的關(guān)聯(lián)也就自動(dòng)解除了。

int shmdt(const void *shmaddr);
  • 參數(shù):shmat() 函數(shù)的返回值, 共享內(nèi)存的起始地址
  • 返回值:關(guān)聯(lián)解除成功返回0,失敗返回-1

4.3 刪除共享內(nèi)存

4.3.1 shmctl

shmctl() 函數(shù)是一個(gè)多功能函數(shù),可設(shè)置、獲取共享內(nèi)存狀態(tài)也可將共享內(nèi)存標(biāo)記為刪除狀態(tài)。
當(dāng)共享內(nèi)存被標(biāo)記為刪除狀態(tài)之后,并不會(huì)馬上被刪除,直到所有的進(jìn)程全部和共享內(nèi)存解除關(guān)聯(lián),共享內(nèi)存才會(huì)被刪除。
因?yàn)橥ㄟ^shmctl()函數(shù)只是標(biāo)記刪除共享內(nèi)存,所以在程序中多次調(diào)用該操作也沒關(guān)系。

// 共享內(nèi)存控制函數(shù)
int shmctl(int shmid, int cmd, struct shmid_ds *buf);// 參數(shù) struct shmid_ds 結(jié)構(gòu)體原型          
struct shmid_ds {struct ipc_perm shm_perm;    /* Ownership and permissions */size_t          shm_segsz;   /* Size of segment (bytes) */time_t          shm_atime;   /* Last attach time */time_t          shm_dtime;   /* Last detach time */time_t          shm_ctime;   /* Last change time */pid_t           shm_cpid;    /* PID of creator */pid_t           shm_lpid;    /* PID of last shmat(2)/shmdt(2) */// 引用計(jì)數(shù), 多少個(gè)進(jìn)程和共享內(nèi)存進(jìn)行了關(guān)聯(lián)shmatt_t        shm_nattch;  /* 記錄了有多少個(gè)進(jìn)程和當(dāng)前共享內(nèi)存進(jìn)行了管聯(lián) */...
};
  • 參數(shù):
    • shmid: 要操作的共享內(nèi)存的ID, 是 shmget() 函數(shù)的返回值
    • cmd: 要做的操作
      • IPC_STAT: 得到當(dāng)前共享內(nèi)存的狀態(tài)
      • IPC_SET: 設(shè)置共享內(nèi)存的狀態(tài)
      • IPC_RMID: 標(biāo)記共享內(nèi)存要被刪除了
    • buf:
      • cmd==IPC_STAT, 作為傳出參數(shù), 會(huì)得到共享內(nèi)存的相關(guān)屬性信息
      • cmd==IPC_SET, 作為傳入?yún)? 將用戶的自定義屬性設(shè)置到共享內(nèi)存中
      • cmd==IPC_RMID, buf就沒意義了, 這時(shí)候buf指定為NULL即可
  • 返回值:函數(shù)調(diào)用成功返回值大于等于0,調(diào)用失敗返回-1

4.3.2 相關(guān)shell命令

使用ipcs 添加參數(shù)-m可以查看系統(tǒng)中共享內(nèi)存的詳細(xì)信息

$ ipcs -m------------ 共享內(nèi)存段 --------------
key        shmid      擁有者  權(quán)限     字節(jié)     nattch     狀態(tài)      
0x00000000 425984     oracle     600        524288     2          目標(biāo)       
0x00000000 327681     oracle     600        524288     2          目標(biāo)       
0x00000000 458754     oracle     600        524288     2          目標(biāo) 	

使用 ipcrm 命令可以標(biāo)記刪除某塊共享內(nèi)存

# key == shmget的第一個(gè)參數(shù)
$ ipcrm -M shmkey  # id == shmget的返回值
$ ipcrm -m shmid	

4.3.3 共享內(nèi)存狀態(tài)

// 參數(shù) struct shmid_ds 結(jié)構(gòu)體原型          
struct shmid_ds {struct ipc_perm shm_perm;    /* Ownership and permissions */size_t          shm_segsz;   /* Size of segment (bytes) */time_t          shm_atime;   /* Last attach time */time_t          shm_dtime;   /* Last detach time */time_t          shm_ctime;   /* Last change time */pid_t           shm_cpid;    /* PID of creator */pid_t           shm_lpid;    /* PID of last shmat(2)/shmdt(2) */// 引用計(jì)數(shù), 多少個(gè)進(jìn)程和共享內(nèi)存進(jìn)行了關(guān)聯(lián)shmatt_t        shm_nattch;  /* 記錄了有多少個(gè)進(jìn)程和當(dāng)前共享內(nèi)存進(jìn)行了管聯(lián) */...
};

通過shmctl()我們可得知,共享內(nèi)存的信息是存儲(chǔ)到一個(gè)叫做struct shmid_ds的結(jié)構(gòu)體中,其中有一個(gè)非常重要的成員叫做shm_nattch,在這個(gè)成員變量里邊記錄著當(dāng)前共享內(nèi)存關(guān)聯(lián)的進(jìn)程的個(gè)數(shù),一般將其稱為引用計(jì)數(shù)。
當(dāng)共享內(nèi)存被標(biāo)記為刪除狀態(tài),并且這個(gè)引用計(jì)數(shù)變?yōu)?之后共享內(nèi)存才會(huì)被真正的被刪除掉。

當(dāng)共享內(nèi)存被標(biāo)記為刪除狀態(tài)之后,共享內(nèi)存的狀態(tài)也會(huì)發(fā)生變化,共享內(nèi)存內(nèi)部維護(hù)的key從一個(gè)正整數(shù)變?yōu)?,其屬性從公共的變?yōu)樗接?。這里的私有指只有已經(jīng)關(guān)聯(lián)成功的進(jìn)程才允許繼續(xù)訪問共享內(nèi)存,不再允許新的進(jìn)程和這塊共享內(nèi)存進(jìn)行關(guān)聯(lián)了。
下圖演示了共享內(nèi)存的狀態(tài)變化:

在這里插入圖片描述


4.4 進(jìn)程間通信

使用共享內(nèi)存實(shí)現(xiàn)進(jìn)程間通信的操作流程如下:

1. 調(diào)用linux的系統(tǒng)API創(chuàng)建一塊共享內(nèi)存- 這塊內(nèi)存不屬于任何進(jìn)程, 默認(rèn)進(jìn)程不能對(duì)其進(jìn)行操作2. 準(zhǔn)備好進(jìn)程A, 和進(jìn)程B, 這兩個(gè)進(jìn)程需要和創(chuàng)建的共享內(nèi)存進(jìn)行關(guān)聯(lián)- 關(guān)聯(lián)操作: 調(diào)用linux的 api- 關(guān)聯(lián)成功之后, 得到了這塊共享內(nèi)存的起始地址3. 在進(jìn)程A或者進(jìn)程B中對(duì)共享內(nèi)存進(jìn)行讀寫操作- 讀內(nèi)存: printf();- 寫內(nèi)存: memcpy();4. 通信完成, 可以讓進(jìn)程A和B和共享內(nèi)存解除關(guān)聯(lián)- 解除成功, 進(jìn)程A和B不能再操作共享內(nèi)存了- 共享內(nèi)存不受進(jìn)程生命周期的影響的5. 共享內(nèi)存不在使用之后, 將其刪除- 調(diào)用linux的api函數(shù), 刪除之后這塊內(nèi)存被內(nèi)核回收了

寫共享內(nèi)存的進(jìn)程代碼:

#include <stdio.h>
#include <sys/shm.h>
#include <string.h>int main()
{// 1. 創(chuàng)建共享內(nèi)存, 大小為4kint shmid = shmget(1000, 4096, IPC_CREAT|0664);if(shmid == -1){perror("shmget error");return -1;}// 2. 當(dāng)前進(jìn)程和共享內(nèi)存關(guān)聯(lián)void* ptr = shmat(shmid, NULL, 0);if(ptr == (void *) -1){perror("shmat error");return -1;}// 3. 寫共享內(nèi)存const char* p = "hello 共享內(nèi)存";memcpy(ptr, p, strlen(p)+1);// 阻塞程序printf("按任意鍵繼續(xù), 刪除共享內(nèi)存\n");getchar();shmdt(ptr);// 刪除共享內(nèi)存shmctl(shmid, IPC_RMID, NULL);printf("共享內(nèi)存已刪除...\n");return 0;
}

讀共享內(nèi)存的進(jìn)程代碼:

#include <stdio.h>
#include <sys/shm.h>
#include <string.h>int main()
{// 1. 創(chuàng)建共享內(nèi)存, 大小為4kint shmid = shmget(1000, 0, 0);if(shmid == -1){perror("shmget error");return -1;}// 2. 當(dāng)前進(jìn)程和共享內(nèi)存關(guān)聯(lián)void* ptr = shmat(shmid, NULL, 0);if(ptr == (void *) -1){perror("shmat error");return -1;}// 3. 讀共享內(nèi)存printf("共享內(nèi)存數(shù)據(jù): %s\n", (char*)ptr);// 阻塞程序printf("按任意鍵繼續(xù), 刪除共享內(nèi)存\n");getchar();shmdt(ptr);// 刪除共享內(nèi)存shmctl(shmid, IPC_RMID, NULL);printf("共享內(nèi)存已經(jīng)被刪除...\n");return 0;
}

4.5 shm和mmap的區(qū)別

共享內(nèi)存內(nèi)存映射區(qū)都可以實(shí)現(xiàn)進(jìn)程間通信,下面來分析一下二者的區(qū)別:

  • 實(shí)現(xiàn)進(jìn)程間通信的方式

    • shm: 多個(gè)進(jìn)程只需要一塊共享內(nèi)存就夠了,共享內(nèi)存不屬于進(jìn)程,需要和進(jìn)程關(guān)聯(lián)才能使用
    • 內(nèi)存映射區(qū): 位于每個(gè)進(jìn)程的虛擬地址空間中, 并且需要關(guān)聯(lián)同一個(gè)磁盤文件才能實(shí)現(xiàn)進(jìn)程間數(shù)據(jù)通信
  • 效率:

    • shm: 直接對(duì)內(nèi)存操作,效率高
    • 內(nèi)存映射區(qū): 需要內(nèi)存和文件之間的數(shù)據(jù)同步,效率低
  • 生命周期

    • shm:進(jìn)程退出對(duì)共享內(nèi)存沒有影響,調(diào)用相關(guān)函數(shù)/命令/ 關(guān)機(jī)才能刪除共享內(nèi)存
    • 內(nèi)存映射區(qū):進(jìn)程退出, 內(nèi)存映射區(qū)也就沒有了
  • 數(shù)據(jù)的完整性 -> 突發(fā)狀態(tài)下數(shù)據(jù)能不能被保存下來(比如: 突然斷電)

    • shm:數(shù)據(jù)存儲(chǔ)在物理內(nèi)存中, 斷電之后系統(tǒng)關(guān)閉, 內(nèi)存數(shù)據(jù)也就丟失了
    • 內(nèi)存映射區(qū):可以完整的保存數(shù)據(jù), 內(nèi)存映射區(qū)數(shù)據(jù)會(huì)同步到磁盤文件

5. 信號(hào)

5.1 信號(hào)概述

Linux中的信號(hào)是一種消息處理機(jī)制, 它本質(zhì)上是一個(gè)整數(shù),不同的信號(hào)對(duì)應(yīng)不同的值
由于信號(hào)的結(jié)構(gòu)簡(jiǎn)單所以天生不能攜帶很大的信息量,但信號(hào)在系統(tǒng)中優(yōu)先級(jí)非常高。

在Linux中的很多常規(guī)操作中都會(huì)有相關(guān)的信號(hào)產(chǎn)生,先從我們最熟悉的場(chǎng)景說起:

  • 通過鍵盤操作產(chǎn)生了信號(hào):用戶按下Ctrl-C,這個(gè)鍵盤輸入產(chǎn)生一個(gè)硬件中斷,使用這個(gè)快捷鍵會(huì)產(chǎn)生信號(hào), 這個(gè)信號(hào)會(huì)殺死對(duì)應(yīng)的某個(gè)進(jìn)程
  • 通過shell命令產(chǎn)生了信號(hào):通過kill命令終止某一個(gè)進(jìn)程,kill -9 進(jìn)程PID
  • 通過函數(shù)調(diào)用產(chǎn)生了信號(hào):如果CPU當(dāng)前正在執(zhí)行這個(gè)進(jìn)程的代碼調(diào)用,比如函數(shù) sleep(),進(jìn)程收到相關(guān)的信號(hào),被迫掛起
  • 通過對(duì)硬件進(jìn)行非法訪問產(chǎn)生了信號(hào):正在運(yùn)行的程序訪問了非法內(nèi)存,發(fā)生段錯(cuò)誤,進(jìn)程退出。

信號(hào)也可以實(shí)現(xiàn)進(jìn)程間通信,但是信號(hào)能傳遞的數(shù)據(jù)量很少,不滿足大部分需求,另外信號(hào)的優(yōu)先級(jí)很高,并且它對(duì)應(yīng)的處理動(dòng)作是回調(diào)完成的,它會(huì)打亂程序原有的處理流程,影響到最終的處理結(jié)果。
因此非常不建議使用信號(hào)進(jìn)行進(jìn)程間通信。


5.1.1 信號(hào)編號(hào)

通過 kill -l 命令可以察看系統(tǒng)定義的信號(hào)列表:

# 執(zhí)行shell命令查看信號(hào)
$ 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

下表中詳細(xì)闡述了信號(hào)產(chǎn)生的時(shí)機(jī)和對(duì)應(yīng)的默認(rèn)處理動(dòng)作:

編號(hào)信號(hào)對(duì)應(yīng)事件默認(rèn)動(dòng)作
1SIGHUP用戶退出shell時(shí),由該shell啟動(dòng)的所有進(jìn)程將收到這個(gè)信號(hào)終止進(jìn)程
2SIGINT當(dāng)用戶按下了<Ctrl+C>組合鍵時(shí),用戶終端向正在運(yùn)行中的由該終端啟動(dòng)的程序發(fā)出此信號(hào)終止進(jìn)程
3SIGQUIT用戶按下<ctrl+>組合鍵時(shí)產(chǎn)生該信號(hào),用戶終端向正在運(yùn)行中的由該終端啟動(dòng)的程序發(fā)出些信號(hào)終止進(jìn)程
4SIGILLCPU檢測(cè)到某進(jìn)程執(zhí)行了非法指令終止進(jìn)程并產(chǎn)生core文件
5SIGTRAP該信號(hào)由斷點(diǎn)指令或其他 trap指令產(chǎn)生終止進(jìn)程并產(chǎn)生core文件
6SIGABRT調(diào)用abort函數(shù)時(shí)產(chǎn)生該信號(hào)終止進(jìn)程并產(chǎn)生core文件
7SIGBUS非法訪問內(nèi)存地址,包括內(nèi)存對(duì)齊出錯(cuò)終止進(jìn)程并產(chǎn)生core文件
8SIGFPE在發(fā)生致命的運(yùn)算錯(cuò)誤時(shí)發(fā)出。不僅包括浮點(diǎn)運(yùn)算錯(cuò)誤,還包括溢出及除數(shù)為0等所有的算法錯(cuò)誤終止進(jìn)程并產(chǎn)生core文件
9SIGKILL無條件終止進(jìn)程。本信號(hào)不能被忽略,處理和阻塞終止進(jìn)程,可以殺死任何進(jìn)程
10SIGUSE1用戶定義的信號(hào)。即程序員可以在程序中定義并使用該信號(hào)終止進(jìn)程
11SIGSEGV指示進(jìn)程進(jìn)行了無效內(nèi)存訪問(段錯(cuò)誤)終止進(jìn)程并產(chǎn)生core文件
12SIGUSR2另外一個(gè)用戶自定義信號(hào),程序員可以在程序中定義并使用該信號(hào)終止進(jìn)程
13SIGPIPEBroken pipe向一個(gè)沒有讀端的管道寫數(shù)據(jù)終止進(jìn)程
14SIGALRM定時(shí)器超時(shí),超時(shí)的時(shí)間由系統(tǒng)調(diào)用alarm設(shè)置終止進(jìn)程
15SIGTERM程序結(jié)束信號(hào),與SIGKILL不同的是,該信號(hào)可以被阻塞和終止。通常用來示程序正常退出。執(zhí)行shell命令Kill時(shí),缺省產(chǎn)生這個(gè)信號(hào)終止進(jìn)程
16SIGSTKFLTLinux早期版本出現(xiàn)的信號(hào),現(xiàn)仍保留向后兼容終止進(jìn)程
17SIGCHLD子進(jìn)程結(jié)束時(shí),父進(jìn)程會(huì)收到這個(gè)信號(hào)忽略這個(gè)信號(hào)
18SIGCONT如果進(jìn)程已停止,則使其繼續(xù)運(yùn)行繼續(xù)/忽略
19SIGSTOP停止進(jìn)程的執(zhí)行。信號(hào)不能被忽略,處理和阻塞為終止進(jìn)程
20SIGTSTP停止終端交互進(jìn)程的運(yùn)行。按下<ctrl+z>組合鍵時(shí)發(fā)出這個(gè)信號(hào)暫停進(jìn)程
21SIGTTIN后臺(tái)進(jìn)程讀終端控制臺(tái)暫停進(jìn)程
22SIGTTOU該信號(hào)類似于SIGTTIN,在后臺(tái)進(jìn)程要向終端輸出數(shù)據(jù)時(shí)發(fā)生暫停進(jìn)程
23SIGURG套接字上有緊急數(shù)據(jù)時(shí),向當(dāng)前正在運(yùn)行的進(jìn)程發(fā)出些信號(hào),報(bào)告有緊急數(shù)據(jù)到達(dá)。如網(wǎng)絡(luò)
24SIGXCPU進(jìn)程執(zhí)行時(shí)間超過了分配給該進(jìn)程的CPU時(shí)間 ,系統(tǒng)產(chǎn)生該信號(hào)并發(fā)送給該進(jìn)程終止進(jìn)程
25SIGXFSZ超過文件的最大長度設(shè)置終止進(jìn)程
26SIGVTALRM虛擬時(shí)鐘超時(shí)時(shí)產(chǎn)生該信號(hào)。類似于SIGALRM,但是該信號(hào)只計(jì)算該進(jìn)程占用CPU的使用時(shí)間終止進(jìn)程
27SGIPROF類似于SIGVTALRM,它不公包括該進(jìn)程占用CPU時(shí)間還包括執(zhí)行系統(tǒng)調(diào)用時(shí)間終止進(jìn)程
28SIGWINCH窗口變化大小時(shí)發(fā)出忽略該信號(hào)
29SIGIO此信號(hào)向進(jìn)程指示發(fā)出了一個(gè)異步IO事件忽略該信號(hào)
30SIGPWR關(guān)機(jī)終止進(jìn)程
31SIGSYS無效的系統(tǒng)調(diào)用終止進(jìn)程并產(chǎn)生core文件
34~64SIGRTMIN ~ SIGRTMAXLINUX的實(shí)時(shí)信號(hào),它們沒有固定的含義(可以由用戶自定義)終止進(jìn)程

5.1.2 查看信號(hào)信息

通過Linux提供的 man 文檔可以查詢所有信號(hào)的詳細(xì)信息:

# 查看man文檔的信號(hào)描述
$ man 7 signal

在信號(hào)描述中介紹了對(duì)產(chǎn)生的信號(hào)的五種默認(rèn)處理動(dòng)作,分別是:

  1. Term:信號(hào)將進(jìn)程終止
  2. Ign:信號(hào)產(chǎn)生之后默認(rèn)被忽略了
  3. Core:信號(hào)將進(jìn)程終止, 并且生成一個(gè)core文件(一般用于gdb調(diào)試)
  4. Stop:信號(hào)會(huì)暫停進(jìn)程的運(yùn)行
  5. Cont:信號(hào)會(huì)讓暫停的進(jìn)程繼續(xù)運(yùn)行

關(guān)于對(duì)信號(hào)的介紹有一句非常重要的描述:
The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.
9號(hào)信號(hào)和19號(hào)信號(hào)不能被 捕捉, 阻塞, 和 忽略
9號(hào)信號(hào): 無條件殺死進(jìn)程
19號(hào)信號(hào): 無條件暫停進(jìn)程

有些信號(hào)在不同的平臺(tái)對(duì)應(yīng)的值是不一樣的,對(duì)應(yīng)我們使用PC機(jī)來說,需要看中間一列的值:

在這里插入圖片描述


5.1.3 信號(hào)的狀態(tài)

Linux中的信號(hào)有三種狀態(tài),分別為:產(chǎn)生,未決,遞達(dá)。

  1. 產(chǎn)生:鍵盤輸入, 函數(shù)調(diào)用, 執(zhí)行shell命令, 對(duì)硬件進(jìn)行非法訪問都會(huì)產(chǎn)生信號(hào)
  2. 未決:信號(hào)產(chǎn)生了, 但是這個(gè)信號(hào)還沒有被處理掉, 這個(gè)期間信號(hào)的狀態(tài)稱之為未決狀態(tài)
  3. 遞達(dá):信號(hào)被處理了(被某個(gè)進(jìn)程處理掉)

在這里插入圖片描述


5.2 信號(hào)相關(guān)函數(shù)

Linux中能夠產(chǎn)生信號(hào)的函數(shù)有很多,下面介紹幾個(gè)常用函數(shù):

5.2.1 kill/raise/abort

這三個(gè)函數(shù)的功能比較類似,可以發(fā)送相關(guān)的信號(hào)給到對(duì)應(yīng)的進(jìn)程。

  • kill 發(fā)送指定的信號(hào)到指定的進(jìn)程
// 函數(shù)原型
#include <signal.h>
// 給某一個(gè)進(jìn)程發(fā)送一個(gè)信號(hào)
int kill(pid_t pid, int sig);
  • 參數(shù):
    • pid: 進(jìn)程ID(man 文檔里邊寫的比較詳細(xì))
    • sig: 要發(fā)送的信號(hào)

函數(shù)使用舉例:

// 自己殺死自己
kill(getpid(), 9);
// 子進(jìn)程殺死自己的父進(jìn)程
kill(getppid(), 10);
  • raise:給當(dāng)前進(jìn)程發(fā)送指定的信號(hào)
// 函數(shù)原型
// 給自己發(fā)送某一個(gè)信號(hào)
#include <signal.h>
int raise(int sig);	// 參數(shù)就是要給當(dāng)前進(jìn)程發(fā)送的信號(hào)
  • abort:給當(dāng)前進(jìn)程發(fā)送一個(gè)固定信號(hào) (SIGABRT)
// 函數(shù)原型
// 這是一個(gè)中斷函數(shù), 調(diào)用這個(gè)函數(shù), 發(fā)送一個(gè)固定信號(hào) (SIGABRT), 殺死當(dāng)前進(jìn)程
#include <stdlib.h>
void abort(void);

5.2.2 定時(shí)器

5.2.2.1 alarm

alarm() 函數(shù)只能進(jìn)行單次定時(shí),定時(shí)完成發(fā)射出一個(gè)信號(hào)。

#include <unistd.h>
unsigned int alarm(unsigned int seconds);
  • 參數(shù): 倒計(jì)時(shí)seconds秒, 倒計(jì)時(shí)完成發(fā)送一個(gè)信號(hào) SIGALRM , 當(dāng)前進(jìn)程會(huì)收到這個(gè)信號(hào),這個(gè)信號(hào)默認(rèn)的處理動(dòng)作是中斷當(dāng)前進(jìn)程
  • 返回值: 大于0表示倒計(jì)時(shí)還剩多少秒,返回值為0表示倒計(jì)時(shí)完成, 信號(hào)被發(fā)出

使用這個(gè)定時(shí)器函數(shù), 檢測(cè)一下當(dāng)前計(jì)算機(jī)1s鐘之內(nèi)能數(shù)多少個(gè)數(shù)

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>int main()
{// 1. 設(shè)置一個(gè)定時(shí)器, 定時(shí)1salarm(1);	// 1s之后會(huì)發(fā)出一個(gè)信號(hào), 這個(gè)信號(hào)將中斷當(dāng)前進(jìn)程int i = 0;while(1){printf("%d\n", i++);}return 0;
}

執(zhí)行上述程序的時(shí)候, 計(jì)算一下時(shí)間

# 直接通過終端輸出
$ time ./a.out
real    0m1.013s		# 實(shí)際數(shù)數(shù)用的總時(shí)間
user    0m0.060s		# 用戶區(qū)代碼使用的時(shí)間
sys     0m0.324s		# 內(nèi)核區(qū)使用的時(shí)間real = user + sys + 消耗的時(shí)間(頻率的從用戶區(qū)到內(nèi)核區(qū)進(jìn)程切換)# 不直接寫終端, 將數(shù)據(jù)重定向到磁盤文件中
$ time ./a.out > a.txt
Alarm clockreal    0m1.002s    # 用戶實(shí)際數(shù)數(shù)的時(shí)間變長了
user    0m0.740s
sys     0m0.236s

文件IO操作需要進(jìn)行用戶區(qū)到內(nèi)核區(qū)的切換,處理方式不同,二者之間切換的頻率也不同。
也就是說對(duì)文件IO操作進(jìn)行優(yōu)化是可以提供程序的執(zhí)行效率的。


5.2.2.2 setitimer

setitimer () 函數(shù)可以進(jìn)行周期性定時(shí),每觸發(fā)一次定時(shí)器就會(huì)發(fā)射出一個(gè)信號(hào)。

// 這個(gè)函數(shù)可以實(shí)現(xiàn)周期性定時(shí), 每個(gè)一段固定的時(shí)間, 發(fā)出一個(gè)特定的定時(shí)器信號(hào)
#include <sys/time.h>struct itimerval {struct timeval it_interval; /* 時(shí)間間隔 */struct timeval it_value;    /* 第一次觸發(fā)定時(shí)器的時(shí)長 */
};
// 舉例: luffy有一個(gè)鬧鐘, 并且使用這個(gè)鬧鐘定時(shí):
// 早晨7點(diǎn)中起床, 第一次鬧鐘響起時(shí)可能起不來, 之后每隔5分鐘再響一次
//  - it_value: 當(dāng)前設(shè)置鬧鐘的時(shí)間點(diǎn) 到 明天早晨7點(diǎn) 對(duì)應(yīng)的總秒數(shù)
//  - it_interval: 鬧鐘第一次響過之后, 每隔5分鐘響一次// 這個(gè)結(jié)構(gòu)體表示的是一個(gè)時(shí)間段: tv_sec + tv_usec
struct timeval {time_t      tv_sec;         /* 秒 */suseconds_t tv_usec;        /* 微妙 */
};int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
  • 參數(shù):
    • which: 定時(shí)器使用什么樣的計(jì)時(shí)法則, 不同的計(jì)時(shí)法則發(fā)出的信號(hào)不同
      • ITIMER_REAL: 自然計(jì)時(shí)法, 最常用, 發(fā)出的信號(hào)為SIGALRM, 一般使用這個(gè)宏值,自然計(jì)時(shí)法時(shí)間 = 用戶區(qū) + 內(nèi)核 + 消耗的時(shí)間(從進(jìn)程的用戶區(qū)到內(nèi)核區(qū)切換使用的總時(shí)間)
      • ITIMER_VIRTUAL: 只計(jì)算程序在用戶區(qū)運(yùn)行使用的時(shí)間,發(fā)射的信號(hào)為 SIGVTALRM
      • ITIMER_PROF: 只計(jì)算內(nèi)核運(yùn)行使用的時(shí)間, 發(fā)出的信號(hào)為SIGPROF
    • new_value: 給定時(shí)器設(shè)置的定時(shí)信息, 傳入?yún)?shù)
    • old_value: 上一次給定時(shí)器設(shè)置的定時(shí)信息, 傳出參數(shù),如果不需要這個(gè)信息, 指定為NULL

5.3 信號(hào)集

5.3.1 阻塞/未決信號(hào)集

在PCB中有兩個(gè)非常重要的信號(hào)集。一個(gè)稱之為“阻塞信號(hào)集”,另一個(gè)稱之為“未決信號(hào)集”。
這兩個(gè)信號(hào)集 體現(xiàn)在內(nèi)核中就是兩張表。
但是操作系統(tǒng)不允許我們直接對(duì)這兩個(gè)信號(hào)集進(jìn)行任何操作,而是需要自定義另外一個(gè)集合,借助信號(hào)集操作函數(shù)來對(duì)PCB中的這兩個(gè)信號(hào)集進(jìn)行修改。

  • 信號(hào)的 “未決” 是一種狀態(tài),指的是從信號(hào)的產(chǎn)生到信號(hào)被處理前的這一段時(shí)間。

  • 信號(hào)的 “阻塞” 是一個(gè)開關(guān)動(dòng)作,指的是阻止信號(hào)被處理,但不是阻止信號(hào)產(chǎn)生。

信號(hào)的阻塞就是讓系統(tǒng)暫時(shí)保留信號(hào)留待以后發(fā)送。
由于另外有辦法讓系統(tǒng)忽略信號(hào),所以一般情況下信號(hào)的阻塞只是暫時(shí)的,只是為了 防止信號(hào)打斷某些敏感的操作。
在這里插入圖片描述

阻塞信號(hào)集和未決信號(hào)集在內(nèi)核中的結(jié)構(gòu)是相同的
它們都是一個(gè)整形數(shù)組(被封裝過的), 一共 128 字節(jié) (int [32] == 1024 bit),1024個(gè)標(biāo)志位,其中前31個(gè)標(biāo)志位,每一個(gè)都對(duì)應(yīng)一個(gè)Linux中的標(biāo)準(zhǔn)信號(hào),通過標(biāo)志位的值來標(biāo)記當(dāng)前信號(hào)在信號(hào)集中的狀態(tài)。

# 上圖對(duì)信號(hào)集在內(nèi)核中存儲(chǔ)的狀態(tài)的描述
# 前31個(gè)信號(hào): 1-31 , 對(duì)應(yīng) 1024個(gè)標(biāo)志位的前31個(gè)標(biāo)志位信號(hào)		標(biāo)志位(從低地址位 到 高地址位)1      ->  	02             13             24             331            30	
  • 在阻塞信號(hào)集中,描述這個(gè)信號(hào)有沒有被阻塞
    • 默認(rèn)情況下沒有信號(hào)是被阻塞的, 因此信號(hào)對(duì)應(yīng)的標(biāo)志位的值為 0
    • 如果某個(gè)信號(hào)被設(shè)置為了阻塞狀態(tài), 這個(gè)信號(hào)對(duì)應(yīng)的標(biāo)志位 被設(shè)置為 1
  • 在未決信號(hào)集中, 描述信號(hào)是否處于未決狀態(tài)
    • 如果這個(gè)信號(hào)被阻塞了, 不能處理, 這個(gè)信號(hào)對(duì)應(yīng)的標(biāo)志位被設(shè)置為1
    • 如果這個(gè)信號(hào)的阻塞被解除了, 未決信號(hào)集中的這個(gè)信號(hào)馬上就被處理了, 這個(gè)信號(hào)對(duì)應(yīng)的標(biāo)志位值變?yōu)?
    • 如果這個(gè)信號(hào)沒有阻塞, 信號(hào)產(chǎn)生之后直接被處理, 因此不會(huì)在未決信號(hào)集中做任何記錄

5.3.2 信號(hào)集函數(shù)

因?yàn)橛脩羰遣荒苤苯硬僮鲀?nèi)核中的阻塞信號(hào)集和未決信號(hào)集的,必須要調(diào)用系統(tǒng)函數(shù)
阻塞信號(hào)集可通過系統(tǒng)函數(shù)進(jìn)行讀寫操作,未決信號(hào)集只能對(duì)其進(jìn)行讀操作。

先來看一下讀/寫阻塞信號(hào)集的函數(shù):

#include <signal.h>
// 使用這個(gè)函數(shù)修改內(nèi)核中的阻塞信號(hào)集
// sigset_t 被封裝之后得到的數(shù)據(jù)類型, 原型:int[32], 里邊一共有1024給標(biāo)志位, 每一個(gè)信號(hào)對(duì)應(yīng)一個(gè)標(biāo)志位
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
  • 參數(shù):
    • how:
      • SIG_BLOCK: 將參數(shù) set 集合中的數(shù)據(jù)追加到阻塞信號(hào)集中
      • SIG_UNBLOCK: 將參數(shù) set 集合中的信號(hào)在阻塞信號(hào)集中解除阻塞
      • SIG_SETMASK: 使用參 set 結(jié)合中的數(shù)據(jù)覆蓋內(nèi)核的阻塞信號(hào)集數(shù)據(jù)
    • oldset: 通過這個(gè)參數(shù)將設(shè)置之前的阻塞信號(hào)集數(shù)據(jù)傳出,如果不需要可以指定為NULL
  • 返回值:函數(shù)調(diào)用成功返回0,調(diào)用失敗返回-1

sigprocmask() 函數(shù)有一個(gè) sigset_t 類型的參數(shù),對(duì)這種類型的數(shù)據(jù)進(jìn)行初始化
需要調(diào)用一些相關(guān)的操作函數(shù):

#include <signal.h>
// 如果在程序中讀寫 sigset_t 類型的變量
// 阻塞信號(hào)集和未決信號(hào)集都存儲(chǔ)在 sigset_t 類型的變量中, 這個(gè)變量對(duì)應(yīng)一塊內(nèi)存
// 阻塞信號(hào)集和未決信號(hào)集, 對(duì)應(yīng)的內(nèi)存中有1024bit = 128字節(jié)// 將set集合中所有的標(biāo)志位設(shè)置為0
int sigemptyset(sigset_t *set);
// 將set集合中所有的標(biāo)志位設(shè)置為1
int sigfillset(sigset_t *set);
// 將set集合中某一個(gè)信號(hào)(signum)對(duì)應(yīng)的標(biāo)志位設(shè)置為1
int sigaddset(sigset_t *set, int signum);
// 將set集合中某一個(gè)信號(hào)(signum)對(duì)應(yīng)的標(biāo)志位設(shè)置為0
int sigdelset(sigset_t *set, int signum);
// 判斷某個(gè)信號(hào)在集合中對(duì)應(yīng)的標(biāo)志位到底是0還是1, 如果是0返回0, 如果是1返回1
int sigismember(const sigset_t *set, int signum);

在這里插入圖片描述

未決信號(hào)集不需要我們修改, 如果設(shè)置了某個(gè)信號(hào)阻塞, 當(dāng)這個(gè)信號(hào)產(chǎn)生之后, 內(nèi)核會(huì)將這個(gè)信號(hào)的未決狀態(tài)記錄到未決信號(hào)集中,當(dāng)阻塞的信號(hào)被解除阻塞, 未決信號(hào)集中的信號(hào)隨之被處理, 內(nèi)核再次修改未決信號(hào)集將該信號(hào)的狀態(tài)修改為遞達(dá)狀態(tài)(標(biāo)志位置0)。
因此,寫未決信號(hào)集的動(dòng)作都是內(nèi)核做的,這是一個(gè)讀未決信號(hào)集的操作函數(shù):

#include <signal.h>
// 這個(gè)函數(shù)的參數(shù)是傳出參數(shù), 傳出的內(nèi)核未決信號(hào)集的拷貝
// 讀一下這個(gè)集合就指定哪個(gè)信號(hào)是未決狀態(tài)
int sigpending(sigset_t *set);

下面舉一個(gè)簡(jiǎn)單的例子,演示一下信號(hào)集操作函數(shù)的使用:

需求: 
在阻塞信號(hào)集中設(shè)置某些信號(hào)阻塞, 通過一些操作產(chǎn)生這些信號(hào),
然后讀未決信號(hào)集, 最后再解除這些信號(hào)的阻塞
假設(shè)阻塞這些信號(hào): - 2號(hào)信號(hào): SIGINT: ctrl+c- 3號(hào)信號(hào): SIGQUIT: ctrl+\- 9號(hào)信號(hào): SIGKILL: 通過shell命令給進(jìn)程發(fā)送這個(gè)信號(hào) kill -9 PID
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>int main()
{// 1. 初始化信號(hào)集sigset_t myset;sigemptyset(&myset);// 設(shè)置阻塞的信號(hào)sigaddset(&myset, SIGINT);  // 2sigaddset(&myset, SIGQUIT); // 3sigaddset(&myset, SIGKILL); // 9 測(cè)試不能被阻塞// 2. 將初始化的信號(hào)集中的數(shù)據(jù)設(shè)置給內(nèi)核sigset_t old;sigprocmask(SIG_BLOCK, &myset, &old);// 3. 讓進(jìn)程一直運(yùn)行, 在當(dāng)前進(jìn)程中產(chǎn)生對(duì)應(yīng)的信號(hào)int i = 0;while(1){// 4. 讀內(nèi)核的未決信號(hào)集sigset_t curset;sigpending(&curset);// 遍歷這個(gè)信號(hào)集for(int i=1; i<32; ++i){int ret = sigismember(&curset, i);printf("%d", ret);}printf("\n");sleep(1);i++;if(i==10){// 解除阻塞, 重新設(shè)置阻塞信號(hào)集//sigprocmask(SIG_UNBLOCK, &myset, NULL);sigprocmask(SIG_SETMASK, &old, NULL);}}return 0;
}

結(jié)論:程序中對(duì) 9 號(hào)信號(hào)的阻塞是無效的,因?yàn)樗鼰o法被阻塞。

一張圖總結(jié)這些信號(hào)集操作函數(shù)之間的關(guān)系:
在這里插入圖片描述


5.4 信號(hào)捕捉

Linux中的每個(gè)信號(hào)產(chǎn)生后都會(huì)有對(duì)應(yīng)的默認(rèn)處理行為,如果想要忽略這個(gè)信號(hào)或修改某些信號(hào)的默認(rèn)行為就需要在程序中捕捉該信號(hào)。
程序中進(jìn)行信號(hào)捕捉可看做一個(gè)注冊(cè)的動(dòng)作,提前告訴應(yīng)用程序信號(hào)產(chǎn)生后做什么處理,當(dāng)進(jìn)程中對(duì)應(yīng)的信號(hào)產(chǎn)生了,這個(gè)處理動(dòng)作也就被調(diào)用了。

5.4.1 signal

使用 signal() 函數(shù)可以捕捉進(jìn)程中產(chǎn)生的信號(hào),并且修改捕捉到的函數(shù)的行為,這個(gè)信號(hào)的自定義處理動(dòng)作是一個(gè)回調(diào)函數(shù),內(nèi)核通過 signal() 得到這個(gè)回調(diào)函數(shù)的地址,在信號(hào)產(chǎn)生之后該函數(shù)會(huì)被內(nèi)核調(diào)用。

#include <signal.h>
// 在程序中什么時(shí)候產(chǎn)生信號(hào), 程序猿是不知道的, 因此不能在信號(hào)產(chǎn)生之后再去處理
// 在信號(hào)產(chǎn)生之前, 提供一個(gè)注冊(cè)函數(shù), 用來捕捉信號(hào)
//	  - 假設(shè)在將來這個(gè)信號(hào)產(chǎn)生了, 就委托內(nèi)核進(jìn)行捕捉, 這個(gè)信號(hào)的默認(rèn)動(dòng)作就不能被執(zhí)行
//	  - 執(zhí)行什么樣的處理動(dòng)作 ==> 在signal函數(shù)中指定的處理動(dòng)作
//	  - 如果這個(gè)信號(hào)不產(chǎn)生, 回調(diào)函數(shù)永遠(yuǎn)不會(huì)被調(diào)用
sighandler_t signal(int signum, sighandler_t handler);   
  • 參數(shù):
    • signum: 需要捕捉的信號(hào)
    • handler: 信號(hào)捕捉到之后的處理動(dòng)作, 這是一個(gè)函數(shù)指針, 函數(shù)原型
typedef void (*sighandler_t)(int);

這個(gè)回調(diào)函數(shù)是需要我們寫的, 若我們不調(diào)用, 由內(nèi)核調(diào)用,
內(nèi)核調(diào)用回調(diào)函數(shù)的時(shí)候, 會(huì)給它傳遞一個(gè)實(shí)參,這個(gè)實(shí)參的值就是捕捉的那個(gè)信號(hào)值。

下面的測(cè)試程序中使用 signal() 函數(shù)來捕捉定時(shí)器產(chǎn)生的信號(hào) SIGALRM:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/time.h>
#include <signal.h>// 定時(shí)器信號(hào)的處理動(dòng)作
void doing(int arg)
{printf("當(dāng)前捕捉到的信號(hào)是: %d\n", arg);// 打印當(dāng)前的時(shí)間
}int main()
{// 注冊(cè)要捕捉哪一個(gè)信號(hào), 執(zhí)行什么樣的處理動(dòng)作signal(SIGALRM, doing);// 1. 調(diào)用定時(shí)器函數(shù)設(shè)置定時(shí)器函數(shù)struct itimerval newact;// 3s之后發(fā)出第一個(gè)定時(shí)器信號(hào), 之后每隔1s發(fā)出一個(gè)定時(shí)器信號(hào)newact.it_value.tv_sec = 3;newact.it_value.tv_usec = 0;newact.it_interval.tv_sec = 1;newact.it_interval.tv_usec = 0;// 這個(gè)函數(shù)也不是阻塞函數(shù), 函數(shù)調(diào)用成功, 倒計(jì)時(shí)開始// 倒計(jì)時(shí)過程中程序是繼續(xù)運(yùn)行的setitimer(ITIMER_REAL, &newact, NULL);// 編寫一個(gè)業(yè)務(wù)處理, 阻止當(dāng)前進(jìn)程自己結(jié)束, 讓當(dāng)前進(jìn)程被發(fā)出的信號(hào)殺死while(1){sleep(1000000);}return 0;
}

5.4.2 sigaction

sigaction() 函數(shù)和 signal() 函數(shù)的功能是一樣的,用于捕捉進(jìn)程中產(chǎn)生的信號(hào),并將用戶自定義的信號(hào)行為函數(shù)(回調(diào)函數(shù))注冊(cè)給內(nèi)核,內(nèi)核在信號(hào)產(chǎn)生之后調(diào)用這個(gè)處理動(dòng)作。
sigaction() 可以看做是 signal() 函數(shù)是加強(qiáng)版,函數(shù)參數(shù)更多更復(fù)雜,函數(shù)功能也更強(qiáng)一些。

// 函數(shù)原型
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
  • 參數(shù):
    • signum: 要捕捉的信號(hào)
    • act: 捕捉到信號(hào)之后的處理動(dòng)作
    • oldact: 上一次調(diào)用該函數(shù)進(jìn)行信號(hào)捕捉設(shè)置的信號(hào)處理動(dòng)作, 該參數(shù)一般指定為NULL
  • 返回值:函數(shù)調(diào)用成功返回0,失敗返回-1

該函數(shù)的參數(shù)是一個(gè)結(jié)構(gòu)體類型,結(jié)構(gòu)體原型如下:

struct sigaction {void     (*sa_handler)(int);    // 指向一個(gè)函數(shù)(回調(diào)函數(shù))void     (*sa_sigaction)(int, siginfo_t *, void *);sigset_t   sa_mask;             // 初始化為空即可, 處理函數(shù)執(zhí)行期間不屏蔽任何信號(hào)int        sa_flags;	        // 0void     (*sa_restorer)(void);  //不用
};

結(jié)構(gòu)體成員介紹:

  • sa_handler: 函數(shù)指針,指向的函數(shù)就是捕捉到的信號(hào)的處理動(dòng)作

  • sa_sigaction: 函數(shù)指針,指向的函數(shù)就是捕捉到的信號(hào)的處理動(dòng)作

  • sa_mask: 在信號(hào)處理函數(shù)執(zhí)行期間, 臨時(shí)屏蔽某些信號(hào), 將要屏蔽的信號(hào)設(shè)置到集合中即可

    • 當(dāng)前處理函數(shù)執(zhí)行完畢, 臨時(shí)屏蔽自動(dòng)解除
    • 假設(shè)在這個(gè)集合中不屏蔽任何信號(hào), 默認(rèn)也會(huì)屏蔽一個(gè)(捕捉的信號(hào)是誰, 就臨時(shí)屏蔽誰)
  • sa_flags:使用哪個(gè)函數(shù)指針指向的函數(shù)處理捕捉到的信號(hào)

    • 0:使用 sa_handler (一般情況下使用這個(gè))
    • SA_SIGINFO:使用 sa_sigaction (使用信號(hào)傳遞數(shù)據(jù)==進(jìn)程間通信)
  • sa_restorer: 被廢棄的成員

示例代碼,通過sigaction()捕捉阻塞信號(hào)集中解除阻塞的信號(hào),如果捕捉多個(gè)信號(hào),可以給不同的信號(hào)添加不同的處理動(dòng)作,代碼中的處理動(dòng)作只有一個(gè):

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>// 信號(hào)的處理動(dòng)作
void callback(int num)
{printf("當(dāng)前捕捉的信號(hào): %d\n", num);
}int main()
{// 1. 初始化信號(hào)集sigset_t myset;sigemptyset(&myset);// 設(shè)置阻塞的信號(hào)sigaddset(&myset, SIGINT);  // 2sigaddset(&myset, SIGQUIT); // 3sigaddset(&myset, SIGKILL); // 9 測(cè)試不能被阻塞// 當(dāng)阻塞的信號(hào)被解除阻塞, 該信號(hào)就可以被捕捉到了// 如果信號(hào)被捕捉到之后, 馬上就被處理掉了 --> 遞達(dá)狀態(tài)struct sigaction act;act.sa_handler = callback;act.sa_flags = 0;sigemptyset(&act.sa_mask);sigaction(SIGINT, &act, NULL);// 和sigint的處理動(dòng)作相同sigaction(SIGQUIT, &act, NULL);sigaction(SIGKILL, &act, NULL);// 2. 將初始化的信號(hào)集中的數(shù)據(jù)設(shè)置給內(nèi)核sigset_t old;sigprocmask(SIG_BLOCK, &myset, &old);// 3. 讓進(jìn)程一直運(yùn)行, 在當(dāng)前進(jìn)程中產(chǎn)生對(duì)應(yīng)的信號(hào)int i = 0;while(1){// 4. 讀內(nèi)核的未決信號(hào)集sigset_t curset;sigpending(&curset);// 遍歷這個(gè)信號(hào)集for(int i=1; i<32; ++i){int ret = sigismember(&curset, i);printf("%d", ret);}printf("\n");sleep(1);i++;if(i==10){// 解除阻塞, 重新設(shè)置阻塞信號(hào)集//sigprocmask(SIG_UNBLOCK, &myset, NULL);sigprocmask(SIG_SETMASK, &old, NULL);}}return 0;
}

結(jié)論:程序中對(duì) 9 號(hào)信號(hào)的捕捉是無效的,因?yàn)樗鼰o法被捕捉。


5.5 SIGCHLD 信號(hào)

當(dāng)子進(jìn)程退出、暫停、從暫?;貜?fù)運(yùn)行的時(shí)候,在子進(jìn)程中會(huì)產(chǎn)生一個(gè)SIGCHLD信號(hào),并將其發(fā)送給父進(jìn)程,但是父進(jìn)程收到這個(gè)信號(hào)后默認(rèn)忽略。
我們可以在父進(jìn)程中對(duì)這個(gè)信號(hào)加以利用,基于這個(gè)信號(hào)來回收子進(jìn)程的資源,因此需要在父進(jìn)程中捕捉子進(jìn)程發(fā)送過來的這個(gè)信號(hào)。

下面是基于信號(hào)回收子進(jìn)程資源的示例代碼:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#include <signal.h>// 回收子進(jìn)程處理函數(shù)
void recycle(int num)
{printf("捕捉到的信號(hào)是: %d\n", num);// 子進(jìn)程的資源回收, 非阻塞// SIGCHLD信號(hào)17號(hào)信號(hào), 1-31號(hào)信號(hào)不支持排隊(duì)// 如果這些信號(hào)同時(shí)產(chǎn)生多個(gè), 最終處理的時(shí)候只處理一次// 假設(shè)多個(gè)子進(jìn)程同時(shí)退出, 父進(jìn)程同時(shí)收到了多個(gè)sigchld信號(hào)// 父進(jìn)程只會(huì)處理一次這個(gè)信號(hào), 因此當(dāng)前函數(shù)被調(diào)用了一次, waitpid被調(diào)用一次// 相當(dāng)于只回收了一個(gè)子進(jìn)程, 但是是同時(shí)死了多個(gè)子進(jìn)程, 因此就出現(xiàn)了僵尸進(jìn)程// 解決方案: 循環(huán)回收即可while(1){// 如果是阻塞回收, 就回不到另外一個(gè)處理邏輯上去了pid_t pid = waitpid(-1, NULL, WNOHANG);if(pid > 0){printf("child died, pid = %d\n", pid);}else if(pid == 0){// 沒有死亡的子進(jìn)程, 直接退出當(dāng)前循環(huán)break;}else if(pid == -1){printf("所有子進(jìn)程都回收完畢了, 拜拜...\n");break;}}
}int main()
{// 設(shè)置sigchld信號(hào)阻塞sigset_t myset;sigemptyset(&myset);sigaddset(&myset, SIGCHLD);sigprocmask(SIG_BLOCK, &myset, NULL);// 循環(huán)創(chuàng)建多個(gè)子進(jìn)程 - 20pid_t pid;for(int i=0; i<20; ++i){pid = fork();if(pid == 0){break;}}if(pid == 0){printf("我是子進(jìn)程, pid = %d\n", getpid());}else if(pid > 0){printf("我是父進(jìn)程, pid = %d\n", getpid());// 注冊(cè)信號(hào)捕捉, 捕捉sigchldstruct sigaction act;act.sa_flags  =0;act.sa_handler = recycle;sigemptyset(&act.sa_mask);// 注冊(cè)信號(hào)捕捉, 委托內(nèi)核處理將來產(chǎn)生的信號(hào)// 當(dāng)信號(hào)產(chǎn)生之后, 當(dāng)前進(jìn)程優(yōu)先處理信號(hào), 之前的處理動(dòng)作會(huì)暫停// 信號(hào)處理完畢之后, 回到原來的暫停的位置繼續(xù)運(yùn)行sigaction(SIGCHLD, &act, NULL);// 解除sigcld信號(hào)的阻塞// 信號(hào)被阻塞之后,就捕捉不到了, 解除阻塞之后才能捕捉到這個(gè)信號(hào)sigprocmask(SIG_UNBLOCK, &myset, NULL);// 父進(jìn)程執(zhí)行其他業(yè)務(wù)邏輯就可以了// 默認(rèn)父進(jìn)程執(zhí)行這個(gè)while循環(huán), 但是信號(hào)產(chǎn)生了, 這個(gè)執(zhí)行邏輯或強(qiáng)迫暫停// 	父進(jìn)程去處理信號(hào)的處理函數(shù)while(1){sleep(100);}}return 0;
}

6. 守護(hù)進(jìn)程

守護(hù)進(jìn)程(Daemon Process),也就是通常說的 Daemon 進(jìn)程(精靈進(jìn)程),是 Linux 中的后臺(tái)服務(wù)進(jìn)程。它是一個(gè)生存期較長的進(jìn)程,通常獨(dú)立于控制終端并且周期性地執(zhí)行某種任務(wù)或等待處理某些發(fā)生的事件。一般采用以d結(jié)尾的名字。

6.1 進(jìn)程組

多個(gè)進(jìn)程的集合就是進(jìn)程組, 這個(gè)組中必須有一個(gè)組長, 組長就是進(jìn)程組中的第一個(gè)進(jìn)程,組長以外的都是普通的成員,每個(gè)進(jìn)程組都有一個(gè)唯一的組ID,進(jìn)程組的ID和組長的PID是一樣的。

進(jìn)程組中的成員是可以轉(zhuǎn)移的,如果當(dāng)前進(jìn)程組中的成員被轉(zhuǎn)移到了其他的組,或者進(jìn)制中的所有進(jìn)程都退出了,那么這個(gè)進(jìn)程組也就不存在了。
如果進(jìn)程組中組長死了, 但是當(dāng)前進(jìn)程組中有其他進(jìn)程,這個(gè)進(jìn)程組還是繼續(xù)存在的。
下面介紹幾個(gè)常用的進(jìn)程組函數(shù):

得到當(dāng)前進(jìn)程所在的進(jìn)程組的組ID

pid_t getpgrp(void);

獲取指定的進(jìn)程所在的進(jìn)程組的組ID,參數(shù) pid 就是指定的進(jìn)程

pid_t getpgid(pid_t pid);

將某個(gè)進(jìn)程移動(dòng)到其他進(jìn)程組中或者創(chuàng)建新的進(jìn)程組

int setpgid(pid_t pid, pid_t pgid);
  • 參數(shù):
    • pid: 某個(gè)進(jìn)程的進(jìn)程ID
    • pgid: 某個(gè)進(jìn)程組的組ID
      • 如果pgid對(duì)應(yīng)的進(jìn)程組存在,pid對(duì)應(yīng)的進(jìn)程會(huì)移動(dòng)到這個(gè)組中, pid != pgid
      • 如果pgid對(duì)應(yīng)的進(jìn)程組不存在,會(huì)創(chuàng)建一個(gè)新的進(jìn)程組, 因此要求 pid == pgid, 當(dāng)前進(jìn)程就是組長了
  • 返回值:函數(shù)調(diào)用成功返回0,失敗返回-1

6.2 會(huì)話

會(huì)話(session)是由一個(gè)或多個(gè)進(jìn)程組組成的,一個(gè)會(huì)話可以對(duì)應(yīng)一個(gè)控制終端, 也可以沒有。
一個(gè)普通的進(jìn)程可以調(diào)用 setsid() 函數(shù)使自己成為新 session 的領(lǐng)頭進(jìn)程(會(huì)長),并且這個(gè) session 領(lǐng)頭進(jìn)程還會(huì)被放入到一個(gè)新的進(jìn)程組中。

// 函數(shù)原型
#include <unistd.h>// 獲取某個(gè)進(jìn)程所屬的會(huì)話ID
pid_t getsid(pid_t pid);// 將某個(gè)進(jìn)程變成會(huì)話 =>> 得到一個(gè)守護(hù)進(jìn)程
// 使用哪個(gè)進(jìn)程調(diào)用這個(gè)函數(shù), 這個(gè)進(jìn)程就會(huì)變成一個(gè)會(huì)話
pid_t setsid(void);

使用這個(gè)函數(shù)的注意事項(xiàng):

  1. 調(diào)用這個(gè)函數(shù)的進(jìn)程不能是組長進(jìn)程, 如果是,該函數(shù)調(diào)用失敗,如何保證這個(gè)函數(shù)能調(diào)用成功呢?
    先fork()創(chuàng)建子進(jìn)程, 終止父進(jìn)程,
  2. 讓子進(jìn)程調(diào)用這個(gè)函數(shù) 如果調(diào)用這個(gè)函數(shù)的進(jìn)程不是進(jìn)程組長, 會(huì)話創(chuàng)建成功
    這個(gè)進(jìn)程會(huì)變成當(dāng)前會(huì)話中的第一個(gè)進(jìn)程,同時(shí)也會(huì)變成新的進(jìn)程組的組長
    該函數(shù)調(diào)用成功之后, 當(dāng)前進(jìn)程就脫離了控制終端,因此不會(huì)阻塞終端

6.3 創(chuàng)建守護(hù)進(jìn)程

如果要?jiǎng)?chuàng)建一個(gè)守護(hù)進(jìn)程,標(biāo)準(zhǔn)步驟如下,部分操作可以根據(jù)實(shí)際需求進(jìn)行取舍:

  1. 創(chuàng)建子進(jìn)程, 讓父進(jìn)程退出

    • 因?yàn)楦高M(jìn)程有可能是組長進(jìn)程,不符合條件,也沒有什么利用價(jià)值,退出即可
    • 子進(jìn)程沒有任何職務(wù), 目的是讓子進(jìn)程最終變成一個(gè)會(huì)話, 最終就會(huì)得到守護(hù)進(jìn)程
  2. 通過子進(jìn)程創(chuàng)建新的會(huì)話,調(diào)用函數(shù) setsid(),脫離控制終端, 變成守護(hù)進(jìn)程

  3. 改變當(dāng)前進(jìn)程的工作目錄 (可選項(xiàng))

    • 某些文件系統(tǒng)可以被卸載, 比如: U盤, 移動(dòng)硬盤
      進(jìn)程如果在這些目錄中運(yùn)行,運(yùn)行期間這些設(shè)備被卸載了,運(yùn)行的進(jìn)程也就不能正常工作了。
    • 修改當(dāng)前進(jìn)程的工作目錄需要調(diào)用函數(shù) chdir()
int chdir(const char *path);
  1. 重新設(shè)置文件的掩碼 (可選項(xiàng),)
    • 掩碼: umask, 在創(chuàng)建新文件的時(shí)候需要和這個(gè)掩碼進(jìn)行運(yùn)算, 去掉文件的某些權(quán)限
    • 設(shè)置掩碼需要使用函數(shù) umask()
mode_t umask(mode_t mask);
  1. 關(guān)閉/重定向文件描述符 (建議做)

    • 啟動(dòng)一個(gè)進(jìn)程, 文件描述符表中默認(rèn)有三個(gè)被打開了, 對(duì)應(yīng)的都是當(dāng)前的終端文件

    • 因?yàn)檫M(jìn)程通過調(diào)用 setsid() 已經(jīng)脫離了當(dāng)前終端, 因此關(guān)聯(lián)的文件描述符也就沒用了, 可以關(guān)閉.如下第一代碼部分

    • 重定向文件描述符(和關(guān)閉二選一): 改變文件描述符關(guān)聯(lián)的默認(rèn)文件, 讓他們指向一個(gè)特殊的文件/dev/null,只要把數(shù)據(jù)扔到這個(gè)特殊的設(shè)備文件中, 數(shù)據(jù)被被銷毀了

close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
int fd = open("/dev/null", O_RDWR);
// 重定向之后, 這三個(gè)文件描述符就和當(dāng)前終端沒有任何關(guān)系了
dup2(fd, STDIN_FILENO);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
  1. 根據(jù)實(shí)際需求在守護(hù)進(jìn)程中執(zhí)行某些特定的操作

6.4 守護(hù)進(jìn)程的應(yīng)用

寫一個(gè)守護(hù)進(jìn)程, 每隔2s獲取一次系統(tǒng)時(shí)間, 并將得到的時(shí)間寫入到磁盤文件中。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/time.h>
#include <time.h>// 信號(hào)的處理動(dòng)作
void writeFile(int num)
{// 得到系統(tǒng)時(shí)間time_t seconds = time(NULL);// 時(shí)間轉(zhuǎn)換, 總秒數(shù) -> 可以識(shí)別的時(shí)間字符串struct tm* loc = localtime(&seconds);// sprintf();char* curtime = asctime(loc); // 自帶換行// 打開一個(gè)文件, 如果文件不存在, 就創(chuàng)建, 文件需要有追加屬性// ./對(duì)應(yīng)的是哪個(gè)目錄? /home/robin// 0664 & ~022int fd = open("./time+++++++.log", O_WRONLY|O_CREAT|O_APPEND, 0664);write(fd, curtime, strlen(curtime));close(fd);
}int main()
{// 1. 創(chuàng)建子進(jìn)程, 殺死父進(jìn)程pid_t pid = fork();if(pid > 0){// 父進(jìn)程exit(0); // kill(getpid(), 9); raise(9); abort();}// 2. 子進(jìn)程, 將其變成會(huì)話, 脫離當(dāng)前終端setsid();// 3. 修改進(jìn)程的工作目錄, 修改到一個(gè)不能被修改和刪除的目錄中 /home/robinchdir("/home/robin");// 4. 設(shè)置掩碼, 在進(jìn)程中創(chuàng)建文件的時(shí)候這個(gè)掩碼就起作用了umask(022);// 5. 重定向和終端關(guān)聯(lián)的文件描述符 -> /dev/nullint fd = open("/dev/null", O_RDWR);dup2(fd, STDIN_FILENO);dup2(fd, STDOUT_FILENO);dup2(fd, STDERR_FILENO);// 5. 委托內(nèi)核捕捉并處理將來發(fā)生的信號(hào)-SIGALRM(14)struct sigaction act;act.sa_flags = 0;act.sa_handler = writeFile;sigemptyset(&act.sa_mask);sigaction(SIGALRM, &act, NULL);// 6. 設(shè)置定時(shí)器struct itimerval val;val.it_value.tv_sec = 2;val.it_value.tv_usec = 0;val.it_interval.tv_sec = 2;val.it_interval.tv_usec = 0;setitimer(ITIMER_REAL, &val, NULL);while(1){sleep(100);}return 0;
}

http://www.risenshineclean.com/news/52217.html

相關(guān)文章:

  • 學(xué)做衣服上什么網(wǎng)站軟文廣告發(fā)稿
  • 手機(jī)網(wǎng)站制作平臺(tái)有哪些微商軟文范例大全100
  • 北京城鄉(xiāng)和住房建設(shè)部網(wǎng)站百度推廣首次開戶需要多少錢
  • 網(wǎng)頁制作與網(wǎng)站建設(shè)廣州排名優(yōu)化哪家專業(yè)
  • 手機(jī)做網(wǎng)站公關(guān)公司
  • 資深網(wǎng)站百度學(xué)術(shù)論文官網(wǎng)入口
  • 公司 網(wǎng)站建設(shè) 簡(jiǎn)介付費(fèi)推廣
  • 制作圖片庫蘭州seo技術(shù)優(yōu)化排名公司
  • 唐山網(wǎng)站怎么做seo搜索引擎外部優(yōu)化有哪些渠道
  • 佛山做網(wǎng)站yunzhanfs企業(yè)網(wǎng)絡(luò)推廣平臺(tái)
  • 網(wǎng)站建設(shè)全包需要多少錢廣州seo代理計(jì)費(fèi)
  • 建設(shè)高端網(wǎng)站公司網(wǎng)絡(luò)銷售推廣是做什么的具體
  • wordpress國內(nèi)能用嗎武漢標(biāo)兵seo
  • 金川做網(wǎng)站公司吸引客流的25個(gè)技巧
  • 營山網(wǎng)站建設(shè)seo扣費(fèi)系統(tǒng)源碼
  • 廣州市網(wǎng)絡(luò)seo推廣seo秘籍優(yōu)化課程
  • 物理機(jī)安裝虛擬機(jī)做網(wǎng)站想建立自己的網(wǎng)站
  • ui作品集 網(wǎng)站怎么做搜素引擎優(yōu)化
  • wordpress 前臺(tái)刪除評(píng)論安徽網(wǎng)絡(luò)推廣和優(yōu)化
  • 做網(wǎng)站的圖片是怎么做的騰訊企點(diǎn)是干嘛的
  • 單頁網(wǎng)站設(shè)計(jì)最近一個(gè)月的熱點(diǎn)事件
  • 廣州網(wǎng)站建設(shè)360元陜西seo優(yōu)化
  • 公司做網(wǎng)站費(fèi)用會(huì)計(jì)分錄谷歌是如何運(yùn)營的
  • 本地做網(wǎng)站貴seo搜索引擎優(yōu)化推廣
  • 網(wǎng)站開發(fā)的前端語言是哪些品牌營銷戰(zhàn)略
  • 找網(wǎng)站建設(shè)公司百度seo插件
  • 鄭州做網(wǎng)站推廣價(jià)格廣東網(wǎng)站se0優(yōu)化公司
  • 昆明做網(wǎng)站建設(shè)的公司全國疫情最新消息今天新增
  • 花都網(wǎng)站開發(fā)公司百度新聞發(fā)布
  • h5制作開發(fā)地點(diǎn)企業(yè)關(guān)鍵詞優(yōu)化價(jià)格