成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

圖解 | Linux進程通信之管道實現

系統 Linux
處于安全的考慮,不同進程之間的內存空間是相互隔離的,也就是說 進程A 是不能訪問 進程B 的內存空間,反之亦然。如果不同進程間能夠相互訪問和修改對方的內存,那么當前進程的內存就有可能被其他進程非法修改,從而導致安全隱患。

本文轉載自微信公眾號「Linux內核那些事」,作者songsong001 。轉載本文請聯系Linux內核那些事公眾號。

處于安全的考慮,不同進程之間的內存空間是相互隔離的,也就是說 進程A 是不能訪問 進程B 的內存空間,反之亦然。如果不同進程間能夠相互訪問和修改對方的內存,那么當前進程的內存就有可能被其他進程非法修改,從而導致安全隱患。

不同的進程就像是大海上孤立的島嶼,它們之間不能直接相互通信,如下圖所示:

但某些場景下,不同進程間需要相互通信,比如:進程A 負責處理用戶的請求,而 進程B 負責保存處理后的數據。那么當 進程A 處理完請求后,就需要把處理后的數據提交給 進程B 進行存儲。此時,進程A 就需要與 進程B 進行通信。如下圖所示:

由于不同進程間是相互隔離的,所以必須借助內核來作為橋梁來進行相互通信,內核相當于島嶼之間的輪船,如下圖所示:

內核提供多種進程間通信的方式,如:共享內存,信號,消息隊列 和 管道(pipe) 等。本文主要介紹 管道 的原理與實現。

一、管道的使用

管道 一般用于父子進程之間相互通信,一般的用法如下:

  • 父進程使用 pipe 系統調用創建一個管道。
  • 然后父進程使用 fork 系統調用創建一個子進程。
  • 由于子進程會繼承父進程打開的文件句柄,所以父子進程可以通過新創建的管道進行通信。

其原理如下圖所示:

由于管道分為讀端和寫端,所以需要兩個文件描述符來管理管道:fd[0] 為讀端,fd[1] 為寫端。

下面代碼介紹了怎么使用 pipe 系統調用來創建一個管道:

  1. #include <stdio.h> 
  2. #include <unistd.h> 
  3. #include <sys/types.h> 
  4. #include <stdlib.h> 
  5. #include <string.h> 
  6.  
  7. int main() 
  8.     int ret = -1; 
  9.     int fd[2];  // 用于管理管道的文件描述符 
  10.     pid_t pid; 
  11.     char buf[512] = {0}; 
  12.     char *msg = "hello world"
  13.  
  14.     // 創建一個管理 
  15.     ret = pipe(fd); 
  16.     if (-1 == ret) { 
  17.         printf("failed to create pipe\n"); 
  18.         return -1; 
  19.     } 
  20.    
  21.     pid = fork();     // 創建子進程 
  22.  
  23.     if (0 == pid) {   // 子進程 
  24.         close(fd[0]); // 關閉管道的讀端 
  25.         ret = write(fd[1], msg, strlen(msg)); // 向管道寫端寫入數據 
  26.         exit(0); 
  27.     } else {          // 父進程 
  28.         close(fd[1]); // 關閉管道的寫端 
  29.         ret = read(fd[0], buf, sizeof(buf)); // 從管道的讀端讀取數據 
  30.         printf("parent read %d bytes data: %s\n", ret, buf); 
  31.     } 
  32.  
  33.     return 0; 

編譯代碼:

  1. [root@localhost pipe]# gcc -g pipe.c -o pipe 

運行代碼,輸出結果如下:

  1. [root@localhost pipe]# ./pipe 
  2. parent read 11 bytes data: hello world 

二、管道的實現

每個進程的用戶空間都是獨立的,但內核空間卻是共用的。所以,進程間通信必須由內核提供服務。前面介紹了 管道(pipe) 的使用,接下來將會介紹管道在內核中的實現方式。

本文使用 Linux-2.6.23 內核作為分析對象。

1. 環形緩沖區(Ring Buffer)

在內核中,管道 使用了環形緩沖區來存儲數據。環形緩沖區的原理是:把一個緩沖區當成是首尾相連的環,其中通過讀指針和寫指針來記錄讀操作和寫操作位置。如下圖所示:

在 Linux 內核中,使用了 16 個內存頁作為環形緩沖區,所以這個環形緩沖區的大小為 64KB(16 * 4KB)。

當向管道寫數據時,從寫指針指向的位置開始寫入,并且將寫指針向前移動。而從管道讀取數據時,從讀指針開始讀入,并且將讀指針向前移動。當對沒有數據可讀的管道進行讀操作,將會阻塞當前進程。而對沒有空閑空間的管道進行寫操作,也會阻塞當前進程。

注意:可以將管道文件描述符設置為非阻塞,這樣對管道進行讀寫操作時,就不會阻塞當前進程。

2. 管道對象

在 Linux 內核中,管道使用 pipe_inode_info 對象來進行管理。我們先來看看 pipe_inode_info 對象的定義,如下所示:

  1. struct pipe_inode_info { 
  2.     wait_queue_head_t wait; 
  3.     unsigned int nrbufs, 
  4.     unsigned int curbuf; 
  5.     ... 
  6.     unsigned int readers; 
  7.     unsigned int writers; 
  8.     unsigned int waiting_writers; 
  9.     ... 
  10.     struct inode *inode; 
  11.     struct pipe_buffer bufs[16]; 
  12. }; 

下面介紹一下 pipe_inode_info 對象各個字段的作用:

  • wait:等待隊列,用于存儲正在等待管道可讀或者可寫的進程。
  • bufs:環形緩沖區,由 16 個 pipe_buffer 對象組成,每個 pipe_buffer 對象擁有一個內存頁 ,后面會介紹。
  • nrbufs:表示未讀數據已經占用了環形緩沖區的多少個內存頁。
  • curbuf:表示當前正在讀取環形緩沖區的哪個內存頁中的數據。
  • readers:表示正在讀取管道的進程數。
  • writers:表示正在寫入管道的進程數。
  • waiting_writers:表示等待管道可寫的進程數。
  • inode:與管道關聯的 inode 對象。

由于環形緩沖區是由 16 個 pipe_buffer 對象組成,所以下面我們來看看 pipe_buffer 對象的定義:

  1. struct pipe_buffer { 
  2.     struct page *page; 
  3.     unsigned int offset; 
  4.     unsigned int len; 
  5.     ... 
  6. }; 

下面介紹一下 pipe_buffer 對象各個字段的作用:

  • page:指向 pipe_buffer 對象占用的內存頁。
  • offset:如果進程正在讀取當前內存頁的數據,那么 offset 指向正在讀取當前內存頁的偏移量。
  • len:表示當前內存頁擁有未讀數據的長度。
  • 下圖展示了 pipe_inode_info 對象與 pipe_buffer 對象的關系:

管道的環形緩沖區實現方式與經典的環形緩沖區實現方式有點區別,經典的環形緩沖區一般先申請一塊地址連續的內存塊,然后通過讀指針與寫指針來對讀操作與寫操作進行定位。

但為了減少對內存的使用,內核不會在創建管道時就申請 64K 的內存塊,而是在進程向管道寫入數據時,按需來申請內存。

那么當進程從管道讀取數據時,內核怎么處理呢?下面我們來看看管道讀操作的實現方式。

3. 讀操作

從 經典的環形緩沖區 中讀取數據時,首先通過讀指針來定位到讀取數據的起始地址,然后判斷環形緩沖區中是否有數據可讀,如果有就從環形緩沖區中讀取數據到用戶空間的緩沖區中。如下圖所示:

而 管道的環形緩沖區 與 經典的環形緩沖區 實現稍有不同,管道的環形緩沖區 其讀指針是由 pipe_inode_info 對象的 curbuf 字段與 pipe_buffer 對象的 offset 字段組合而成:

  • pipe_inode_info 對象的 curbuf 字段表示讀操作要從 bufs 數組的哪個 pipe_buffer 中讀取數據。
  • pipe_buffer 對象的 offset 字段表示讀操作要從內存頁的哪個位置開始讀取數據。

讀取數據的過程如下圖所示:

從緩沖區中讀取到 n 個字節的數據后,會相應移動讀指針 n 個字節的位置(也就是增加 pipe_buffer 對象的 offset 字段),并且減少 n 個字節的可讀數據長度(也就是減少 pipe_buffer 對象的 len 字段)。

當 pipe_buffer 對象的 len 字段變為 0 時,表示當前 pipe_buffer 沒有可讀數據,那么將會對 pipe_inode_info 對象的 curbuf 字段移動一個位置,并且其 nrbufs 字段進行減一操作。

我們來看看管道讀操作的代碼實現,讀操作由 pipe_read 函數完成。為了突出重點,我們只列出關鍵代碼,如下所示:

  1. static ssize_t 
  2. pipe_read(struct kiocb *iocb, const struct iovec *_iov, unsigned long nr_segs, 
  3.           loff_t pos) 
  4.     ... 
  5.     struct pipe_inode_info *pipe; 
  6.  
  7.     // 1. 獲取管道對象 
  8.     pipe = inode->i_pipe; 
  9.  
  10.     for (;;) { 
  11.         // 2. 獲取管道未讀數據占有多少個內存頁 
  12.         int bufs = pipe->nrbufs; 
  13.  
  14.         if (bufs) { 
  15.             // 3. 獲取讀操作應該從環形緩沖區的哪個內存頁處讀取數據 
  16.             int curbuf = pipe->curbuf;   
  17.             struct pipe_buffer *buf = pipe->bufs + curbuf; 
  18.             ... 
  19.  
  20.             /* 4. 通過 pipe_buffer 的 offset 字段獲取真正的讀指針, 
  21.              *    并且從管道中讀取數據到用戶緩沖區. 
  22.              */ 
  23.             error = pipe_iov_copy_to_user(iov, addr + buf->offset, chars, atomic); 
  24.             ... 
  25.  
  26.             ret += chars; 
  27.             buf->offset += chars; // 增加 pipe_buffer 對象的 offset 字段的值 
  28.             buf->len -= chars;    // 減少 pipe_buffer 對象的 len 字段的值 
  29.  
  30.             /* 5. 如果當前內存頁的數據已經被讀取完畢 */ 
  31.             if (!buf->len) { 
  32.                 ... 
  33.                 curbuf = (curbuf + 1) & (PIPE_BUFFERS - 1); 
  34.                 pipe->curbuf = curbuf; // 移動 pipe_inode_info 對象的 curbuf 指針 
  35.                 pipe->nrbufs = --bufs; // 減少 pipe_inode_info 對象的 nrbufs 字段 
  36.                 do_wakeup = 1; 
  37.             } 
  38.  
  39.             total_len -= chars; 
  40.  
  41.             // 6. 如果讀取到用戶期望的數據長度, 退出循環 
  42.             if (!total_len) 
  43.                 break; 
  44.         } 
  45.         ... 
  46.     } 
  47.  
  48.     ... 
  49.     return ret; 

上面代碼總結來說分為以下步驟:

  • 通過文件 inode 對象來獲取到管道的 pipe_inode_info 對象。
  • 通過 pipe_inode_info 對象的 nrbufs 字段獲取管道未讀數據占有多少個內存頁。
  • 通過 pipe_inode_info 對象的 curbuf 字段獲取讀操作應該從環形緩沖區的哪個內存頁處讀取數據。
  • 通過 pipe_buffer 對象的 offset 字段獲取真正的讀指針, 并且從管道中讀取數據到用戶緩沖區。
  • 如果當前內存頁的數據已經被讀取完畢,那么移動 pipe_inode_info 對象的 curbuf 指針,并且減少其 nrbufs 字段的值。
  • 如果讀取到用戶期望的數據長度,退出循環。

4. 寫操作

分析完管道讀操作的實現后,接下來,我們分析一下管道寫操作的實現。

經典的環形緩沖區 寫入數據時,首先通過寫指針進行定位要寫入的內存地址,然后判斷環形緩沖區的空間是否足夠,足夠就把數據寫入到環形緩沖區中。如下圖所示:

但 管道的環形緩沖區 并沒有保存 寫指針,而是通過 讀指針 計算出來。那么怎么通過讀指針計算出寫指針呢?

其實很簡單,就是:

寫指針 = 讀指針 + 未讀數據長度

下面我們來看看,向管道寫入 200 字節數據的過程示意圖,如下所示:

如上圖所示,向管道寫入數據時:

  • 首先通過 pipe_inode_info 的 curbuf 字段和 nrbufs 字段來定位到,應該向哪個 pipe_buffer 寫入數據。
  • 然后再通過 pipe_buffer 對象的 offset 字段和 len 字段來定位到,應該寫入到內存頁的哪個位置。

下面我們通過源碼來分析,寫操作是怎么實現的,代碼如下(為了特出重點,代碼有所刪減):

  1. static ssize_t 
  2. pipe_write(struct kiocb *iocb, const struct iovec *_iov, unsigned long nr_segs, 
  3.            loff_t ppos) 
  4.     ... 
  5.     struct pipe_inode_info *pipe; 
  6.     ... 
  7.     pipe = inode->i_pipe; 
  8.     ... 
  9.     chars = total_len & (PAGE_SIZE - 1); /* size of the last buffer */ 
  10.  
  11.     // 1. 如果最后寫入的 pipe_buffer 還有空閑的空間 
  12.     if (pipe->nrbufs && chars != 0) { 
  13.         // 獲取寫入數據的位置 
  14.         int lastbuf = (pipe->curbuf + pipe->nrbufs - 1) & (PIPE_BUFFERS-1); 
  15.         struct pipe_buffer *buf = pipe->bufs + lastbuf; 
  16.         const struct pipe_buf_operations *ops = buf->ops; 
  17.         int offset = buf->offset + buf->len; 
  18.  
  19.         if (ops->can_merge && offset + chars <= PAGE_SIZE) { 
  20.             ... 
  21.             error = pipe_iov_copy_from_user(offset + addr, iov, chars, atomic); 
  22.             ... 
  23.             buf->len += chars; 
  24.             total_len -= chars; 
  25.             ret = chars; 
  26.  
  27.             // 如果要寫入的數據已經全部寫入成功, 退出循環 
  28.             if (!total_len) 
  29.                 goto out
  30.         } 
  31.     } 
  32.  
  33.     // 2. 如果最后寫入的 pipe_buffer 空閑空間不足, 那么申請一個新的內存頁來存儲數據 
  34.     for (;;) { 
  35.         int bufs; 
  36.         ... 
  37.         bufs = pipe->nrbufs; 
  38.  
  39.         if (bufs < PIPE_BUFFERS) { 
  40.             int newbuf = (pipe->curbuf + bufs) & (PIPE_BUFFERS-1); 
  41.             struct pipe_buffer *buf = pipe->bufs + newbuf; 
  42.             ... 
  43.  
  44.             // 申請一個新的內存頁 
  45.             if (!page) { 
  46.                 page = alloc_page(GFP_HIGHUSER); 
  47.                 ... 
  48.             } 
  49.             ... 
  50.             error = pipe_iov_copy_from_user(src, iov, chars, atomic); 
  51.             ... 
  52.             ret += chars; 
  53.  
  54.             buf->page = page; 
  55.             buf->ops = &anon_pipe_buf_ops; 
  56.             buf->offset = 0; 
  57.             buf->len = chars; 
  58.  
  59.             pipe->nrbufs = ++bufs; 
  60.             pipe->tmp_page = NULL
  61.  
  62.             // 如果要寫入的數據已經全部寫入成功, 退出循環 
  63.             total_len -= chars; 
  64.             if (!total_len) 
  65.                 break; 
  66.         } 
  67.         ... 
  68.     } 
  69.  
  70. out
  71.     ... 
  72.     return ret; 

上面代碼有點長,但是邏輯卻很簡單,主要進行如下操作:

如果上次寫操作寫入的 pipe_buffer 還有空閑的空間,那么就將數據寫入到此 pipe_buffer 中,并且增加其 len 字段的值。

如果上次寫操作寫入的 pipe_buffer 沒有足夠的空閑空間,那么就新申請一個內存頁,并且把數據保存到新的內存頁中,并且增加 pipe_inode_info 的 nrbufs 字段的值。

如果寫入的數據已經全部寫入成功,那么就退出寫操作。

三、思考一下

管道讀寫操作的實現已經分析完畢,現在我們來思考一下以下問題。

1. 為什么父子進程可以通過管道來通信?

這是因為父子進程通過 pipe 系統調用打開的管道,在內核空間中指向同一個管道對象(pipe_inode_info)。所以父子進程共享著同一個管道對象,那么就可以通過這個共享的管道對象進行通信。

2. 為什么內核要使用 16 個內存頁進行數據存儲?

這是為了減少內存使用。

因為使用 pipe 系統調用打開管道時,并沒有立刻申請內存頁,而是當有進程向管道寫入數據時,才會按需申請內存頁。當內存頁的數據被讀取完后,內核會將此內存頁回收,來減少管道對內存的使用。

 

責任編輯:武曉燕 來源: Linux內核那些事
相關推薦

2021-07-06 21:30:06

Linux進程通信

2019-05-13 10:00:41

Linux進程間通信命令

2010-01-05 10:00:48

Linux進程間通信

2024-01-03 10:17:51

Linux通信

2010-01-21 11:22:35

Linux多線程同步

2020-11-04 07:17:42

Nodejs通信進程

2023-03-05 16:12:41

Linux進程線程

2015-03-09 10:33:14

即時通信管道過濾

2018-01-12 14:35:00

Linux進程共享內存

2017-06-19 13:36:12

Linux進程消息隊列

2009-12-24 14:47:42

Linux系統進程

2013-03-28 13:14:45

AIDL進程間通信Android使用AI

2021-08-11 14:31:52

鴻蒙HarmonyOS應用

2021-02-14 21:05:05

通信消息系統

2011-06-22 17:27:19

QT 進程通信

2023-03-02 23:50:36

Linux進程管理

2011-01-11 13:47:27

Linux管理進程

2009-02-23 15:55:29

ASP.NET.NET性能提升

2021-09-05 18:29:58

Linux內存回收

2011-06-13 09:15:18

AIXlinuxunix
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 男插女下体视频 | 亚洲国产高清高潮精品美女 | 亚洲一区二区三区四区av | 精品不卡 | 一区二区在线免费播放 | 国产美女精品视频 | 精品国产一区二区国模嫣然 | 在线看av的网址 | a级片在线 | 91视频久久 | 国产精品亚洲片在线播放 | 一区二区在线 | 奇米av| 一区二区三区在线免费观看 | 日日噜噜夜夜爽爽狠狠 | 精品国产一区二区三区性色 | 成人精品久久日伦片大全免费 | 欧美日韩在线一区二区 | 亚洲精品日日夜夜 | 欧美一级片 | 国产亚洲精品成人av久久ww | 色屁屁在线观看 | 国产精品久久久久久久久久久久久久 | 天堂成人av | 99九九视频 | 色婷婷国产精品综合在线观看 | 精品中文字幕一区 | 欧美在线a| 九九综合 | 老头搡老女人毛片视频在线看 | 又黑又粗又长的欧美一区 | www.日韩 | 久久宗合色 | 欧美性jizz18性欧美 | 欧美激情精品久久久久久 | 精品九九久久 | 日本精品视频一区二区三区四区 | 色婷婷av一区二区三区软件 | 欧美久久久久久 | 999在线精品 | 久久99精品国产 |