1. 寫在前面 之前的文章總結了使用管道進行進程間通信的方法,除了pipe和fifo,Linux內核還為我們提供了其他更高級的IPC方式,包括共用記憶體,消息隊列,信號量等,本篇文章會通過一個具有完整邏輯功能的示例說明如何使用這些IPC方法。畢竟單純地查手冊,寫代碼...周而複始,這個過程還是比較枯燥 ...
1. 寫在前面
之前的文章總結了使用管道進行進程間通信的方法,除了pipe和fifo,Linux內核還為我們提供了其他更高級的IPC方式,包括共用記憶體,消息隊列,信號量等,本篇文章會通過一個具有完整邏輯功能的示例說明如何使用這些IPC方法。畢竟單純地查手冊,寫代碼...周而複始,這個過程還是比較枯燥的,而且並沒有哪個IPC方法能解決所有的進程間通信問題,每種方法都不是孤立存在的,通過一個小例子把它們串聯起來,是一種更好的學習方式。下文中的代碼實現可以參考我的代碼倉庫。
2. POSIX IPC概述
進程間通信,主要解決兩個問題,即數據傳遞和同步。POSIX IPC提供了下麵三種方法:
- 消息隊列
- 共用記憶體
- 信號量
操作系統中運行的進程,彼此之間是隔離的,要想實現通信,就必須有一個媒介,是通信雙方都可以訪問到的。從這個角度看,操作系統內核正是每個進程都可以訪問到的那個媒介,就像一個"全局變數"。消息隊列不過是內核維護的一個隊列,保存了用戶進程發送來的消息,其他進程可以從隊列中取走消息,每個消息可以設置優先順序,進程發送和接收消息的行為可以是阻塞或者非阻塞的,這一點類似管道;共用記憶體就是利用了虛擬地址空間以及物理地址空間,讓不同進程的虛擬地址映射到同一個物理頁面上,這樣就實現了共用,對於映射的地址空間可以設定可讀,可寫以及可執行的標誌;信號量就像一個內核中的整型變數,這個變數的數值記錄了某種資源的數量,進程可以對它進行加減操作,合理使用的話就能完成想要的進程之間的同步邏輯。可以看到,這三種IPC方法在內核中都對應了一種數據結構,為了能夠讓用戶進程訪問到這些數據結構,POSIX IPC延續了“一切皆文件”的設計思路,我們可以用類似“/somename”這種形式的文件名去創建或者打開這些IPC對象,然後對它們進行各種操作,和文件的訪問許可權類似,進程操作IPC對象時也會進行許可權檢查。可能上面對三種POSIX IPC的描述存在不嚴謹的地方,但對於使用者來說,我們只要在腦子裡建立一個合適的,能夠描述它們工作方式的模型就可以了,而不是不斷重覆手冊中對每個api的敘述。下麵的表格列出了常用的POSIX IPC api:
消息隊列 | 共用記憶體 | 信號量 | |
---|---|---|---|
打開 | mq_open | shm_open | sem_open |
關閉 | mq_close | shm_close | sem_close |
操作 | mq_send/mq_receive | 記憶體讀寫 | sem_wait/sem_post |
刪除 | mq_unlink | shm_unlink | sem_unlink |
3. POSIX IPC使用
3.1 項目功能說明
下麵將使用三種POSIX IPC實現一個簡單的項目,用來記錄IPC的使用方法。項目包含一個server進程和若幹個client進程,他們各自的功能如下:
- server進程
- 首先運行,等待client的連接到來;
- 收到client的連接,fork出一個新的進程去處理client的請求;
- client進程
- 可以同時運行多個;
- 啟動時和server建立連接,連接建立完成之後,接受用於輸入,向server發起請求;
- 可以完成主動斷開連接,終止server進程,以及其他操作;
首先啟動server進程,然後啟動多個client進程向server發送請求,項目實現之後的效果如下:
3.2 項目實現原理
-
client如何和server建立連接?
client進程和server進程都可以訪問一段共用記憶體,當server進程啟動時,會對這段共用記憶體進行初始化,初始化完成之後,server對信號量A執行post操作,表明共用記憶體準備完畢,之後server進程就通過信號量B等待新連接的建立;當有新的client進程想建立連接時,會先通過對信號量A執行wait操作,等待共用記憶體可用,如果可用,client會把請求參數寫到共用記憶體之中,寫入完成後會對信號量B執行post操作,通知server進程有新的連接已經建立。
-
client建立連接之後如何發送請求?
client通過兩個消息隊列實現發送請求和接收響應。在client建立連接時,會在共用記憶體中寫入用於和server通信的兩個消息隊列的名字,server在處理連接時會打開消息隊列,然後和client進行通信。對於每個新建立的連接,server會fork出一個新的進程去處理該連接對應的client發送來的請求。
-
client如何通過發送請求關閉server?
client通過向請求消息隊列中寫入kill_server請求,可以實現關閉server。當server進程fork出的進程從消息隊列中讀到kill_server請求,該進程會通過管道寫入數據,通知server的主進程結束運行。
-
server和client之間的時序關係:
通過前面3點的描述可以看出,這個簡單的項目幾乎用到了全部常用的IPC方法,下麵這個時序圖更直觀地說明瞭其工作原理:
server和client之間的同步操作主要集中在步驟6,7,8,9。當server準備好共用記憶體之後,通過第6步的信號量A通知client可以建立連接了,之後client向共用記憶體寫入數據,再操作第9步的信號量B通知server連接數據已經寫入,最後server會創建子進程去處理client的請求。實際上server的主進程是一個迴圈,處理請求都是在server的子進程中完成的,以上內容說明瞭server主進程在迴圈中完成的工作。
-
資源清理
當我們使用POSIX IPC時,內核會建立相應的數據結構,並且通過文件系統介面展示給用戶,但IPC資源不能無限創建,當我們的程式運行結束之後應該清理自己用到的IPC資源。運行程式時創建的POSIX IPC對象可以在/dev/shm以及/dev/mqueue下查看,程式結束之後,server和client會釋放掉自己創建的IPC資源。所以,要查看server和client創建的共用記憶體,信號量以及消息隊列,需要在程式運行期間查看上述的兩個目錄。
3.3 主要代碼功能
-
消息格式:
server和client之間通過消息隊列傳遞請求和響應數據,消息隊列中消息格式定義如下:
struct msgbuf { int type; union { struct { int a; int b; } request_add; struct { int c; } response_add; struct { int disconect; } request_disconnect; struct { int kill_server; } request_kill_server; } data; };
-
server主進程:
int main(int argc, char **argv) { int err = server_init(); if (err) { log_warning("server_init failed\n"); return -1; } server_start(); server_shutdown(); return 0; }
其中,在server_init中,server會創建需要使用的共用記憶體,信號量以及管道。
int server_init() { memset(&ipc_server, 0, sizeof(ipc_server)); // shared memory init ipc_server.conn_buf_fd = shm_open(CONNECTION_SHM, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR); ... if (ftruncate(ipc_server.conn_buf_fd, CONNECTION_SHM_SIZE) < 0) { log_warning("server failed ftruncate\n"); return -1; } ipc_server.conn_buf = (struct connection *)mmap( NULL, CONNECTION_SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, ipc_server.conn_buf_fd, 0); ... memset(ipc_server.conn_buf, 0, CONNECTION_SHM_SIZE); ipc_server.conn_buf_ready = sem_open(CONNECTION_BUF_SEM, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR, 0); ... ipc_server.conn_new_ready = sem_open(CONNECTION_NEW_SEM, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR, 0); ... // pipe init int pipefd[2]; if (pipe2(ipc_server.pipefd, O_NONBLOCK)) { log_warning("server failed pipe2\n"); return -1; } log_info("server init done\n"); return 0; }
在server_start中會迴圈處理來自client的連接。
void server_start() { int err = sem_post(ipc_server.conn_buf_ready); ... struct connection conn; int stop = 0; while (!stop) { // handle new connection sem_wait(ipc_server.conn_new_ready); if (read(ipc_server.pipefd[0], &stop, sizeof(int)) <= 0) stop = 0; if (ipc_server.conn_buf->valid) { log_info("new connection established\n"); memcpy(&conn, ipc_server.conn_buf, sizeof(conn)); handle_connection(&conn); memset(ipc_server.conn_buf, 0, sizeof(struct connection)); sem_post(ipc_server.conn_buf_ready); } } }
當server主進程退出之後,server_shutdown會清理IPC資源。
-
client進程:
client啟動之後,首先會嘗試和server建立連接,建立連接之後會迴圈處理用戶輸入,通過消息隊列向server的服務進程發送請求。
int main(int argc, char **argv) { if (build_connection()) { log_info("client failed build_connection\n"); return -1; } handle_command(); cleanup(); log_info("client %d exit\n", getpid()); return 0; }
client建立連接的過程如下:建立連接時client需要等待共用記憶體可用,並且在寫入連接數據之後通知server,這些同步操作都是通過信號量實現的。
int build_connection() { ... connection.mqreq_fd = mq_open(connection.mqreq, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR, &attr); connection.mqrsp_fd = mq_open(connection.mqrsp, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR, &attr); ... // open and map shared memory int fd = shm_open(CONNECTION_SHM, O_RDWR, 0); void *conn_buf = mmap(NULL, CONNECTION_SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); // write connection to conn_buf, notify server new conncection is comming sem_t *conn_buf_ready = sem_open(CONNECTION_BUF_SEM, O_RDWR); sem_t *conn_new_ready = sem_open(CONNECTION_NEW_SEM, O_RDWR); ... sem_wait(conn_buf_ready); connection.valid = 1; memcpy(conn_buf, &connection, sizeof(connection)); sem_post(conn_new_ready); ... return 0; }
寫在最後
通過一個包含server和client的代碼示例,說明瞭POSIX IPC中共用記憶體,消息隊列以及信號量的使用方法。具體實現可以參考我的代碼倉庫。
本文來自博客園,作者:kfggww,轉載請註明原文鏈接:https://www.cnblogs.com/kfggww/p/17087335.html