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

Linux下的進程間通信:套接字和信號

系統 Linux
篇是 Linux 下進程間通信(IPC)系列的第三篇同時也是最后一篇文章。第一篇文章聚焦在通過共享存儲(文件和共享內存段)來進行 IPC,第二篇文章則通過管道(無名的或者命名的)及消息隊列來達到相同的目的。

[[267074]]

學習在 Linux 中進程是如何與其他進程進行同步的。

本篇是 Linux 下進程間通信(IPC)系列的第三篇同時也是最后一篇文章。第一篇文章聚焦在通過共享存儲(文件和共享內存段)來進行 IPC,第二篇文章則通過管道(無名的或者命名的)及消息隊列來達到相同的目的。這篇文章將目光從高處(套接字)然后到低處(信號)來關注 IPC。代碼示例將用力地充實下面的解釋細節。

套接字

正如管道有兩種類型(命名和無名)一樣,套接字也有兩種類型。IPC 套接字(即 Unix 套接字)給予進程在相同設備(主機)上基于通道的通信能力;而網絡套接字給予進程運行在不同主機的能力,因此也帶來了網絡通信的能力。網絡套接字需要底層協議的支持,例如 TCP(傳輸控制協議)或 UDP(用戶數據報協議)。

與之相反,IPC 套接字依賴于本地系統內核的支持來進行通信;特別的,IPC 通信使用一個本地的文件作為套接字地址。盡管這兩種套接字的實現有所不同,但在本質上,IPC 套接字和網絡套接字的 API 是一致的。接下來的例子將包含網絡套接字的內容,但示例服務器和客戶端程序可以在相同的機器上運行,因為服務器使用了 localhost(127.0.0.1)這個網絡地址,該地址表示的是本地機器上的本地機器地址。

套接字以流的形式(下面將會討論到)被配置為雙向的,并且其控制遵循 C/S(客戶端/服務器端)模式:客戶端通過嘗試連接一個服務器來初始化對話,而服務器端將嘗試接受該連接。假如萬事順利,來自客戶端的請求和來自服務器端的響應將通過管道進行傳輸,直到其中任意一方關閉該通道,從而斷開這個連接。

一個迭代服務器(只適用于開發)將一直和連接它的客戶端打交道:從最開始服務第一個客戶端,然后到這個連接關閉,然后服務第二個客戶端,循環往復。這種方式的一個缺點是處理一個特定的客戶端可能會掛起,使得其他的客戶端一直在后面等待。生產級別的服務器將是并發的,通常使用了多進程或者多線程的混合。例如,我臺式機上的 Nginx 網絡服務器有一個 4 個工人worker的進程池,它們可以并發地處理客戶端的請求。在下面的代碼示例中,我們將使用迭代服務器,使得我們將要處理的問題保持在一個很小的規模,只關注基本的 API,而不去關心并發的問題。

最后,隨著各種 POSIX 改進的出現,套接字 API 隨著時間的推移而發生了顯著的變化。當前針對服務器端和客戶端的示例代碼特意寫的比較簡單,但是它著重強調了基于流的套接字中連接的雙方。下面是關于流控制的一個總結,其中服務器端在一個終端中開啟,而客戶端在另一個不同的終端中開啟:

  • 服務器端等待客戶端的連接,對于給定的一個成功連接,它就讀取來自客戶端的數據。
  • 為了強調是雙方的會話,服務器端會對接收自客戶端的數據做回應。這些數據都是 ASCII 字符代碼,它們組成了一些書的標題。
  • 客戶端將書的標題寫給服務器端的進程,并從服務器端的回應中讀取到相同的標題。然后客戶端和服務器端都在屏幕上打印出標題。下面是服務器端的輸出,客戶端的輸出也和它完全一樣:
  1. Listening on port 9876 for clients...
  2. War and Peace
  3. Pride and Prejudice
  4. The Sound and the Fury

示例 1. 使用套接字的客戶端程序

  1. #include <string.h>
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. #include <unistd.h>
  5. #include <sys/types.h>
  6. #include <sys/socket.h>
  7. #include <netinet/tcp.h>
  8. #include <arpa/inet.h>
  9. #include "sock.h"
  10.  
  11. void report(const char* msg, int terminate) {
  12. perror(msg);
  13. if (terminate) exit(-1); /* failure */
  14. }
  15.  
  16. int main() {
  17. int fd = socket(AF_INET, /* network versus AF_LOCAL */
  18. SOCK_STREAM, /* reliable, bidirectional: TCP */
  19. 0); /* system picks underlying protocol */
  20. if (fd < 0) report("socket", 1); /* terminate */
  21. /* bind the server's local address in memory */
  22. struct sockaddr_in saddr;
  23. memset(&saddr, 0, sizeof(saddr)); /* clear the bytes */
  24. saddr.sin_family = AF_INET; /* versus AF_LOCAL */
  25. saddr.sin_addr.s_addr = htonl(INADDR_ANY); /* host-to-network endian */
  26. saddr.sin_port = htons(PortNumber); /* for listening */
  27. if (bind(fd, (struct sockaddr *) &saddr, sizeof(saddr)) < 0)
  28. report("bind", 1); /* terminate */
  29. /* listen to the socket */
  30. if (listen(fd, MaxConnects) < 0) /* listen for clients, up to MaxConnects */
  31. report("listen", 1); /* terminate */
  32.  
  33. fprintf(stderr, "Listening on port %i for clients...\n", PortNumber);
  34. /* a server traditionally listens indefinitely */
  35. while (1) {
  36. struct sockaddr_in caddr; /* client address */
  37. int len = sizeof(caddr); /* address length could change */
  38. int client_fd = accept(fd, (struct sockaddr*) &caddr, &len); /* accept blocks */
  39. if (client_fd < 0) {
  40. report("accept", 0); /* don't terminated, though there's a problem */
  41. continue;
  42. }
  43.  
  44. /* read from client */
  45. int i;
  46. for (i = 0; i < ConversationLen; i++) {
  47. char buffer[BuffSize + 1];
  48. memset(buffer, '\0', sizeof(buffer));
  49. int count = read(client_fd, buffer, sizeof(buffer));
  50. if (count > 0) {
  51. puts(buffer);
  52. write(client_fd, buffer, sizeof(buffer)); /* echo as confirmation */
  53. }
  54. }
  55. close(client_fd); /* break connection */
  56. } /* while(1) */
  57. return 0;
  58. }

上面的服務器端程序執行典型的 4 個步驟來準備回應客戶端的請求,然后接受其他的獨立請求。這里每一個步驟都以服務器端程序調用的系統函數來命名。

  1. socket(…):為套接字連接獲取一個文件描述符
  2. bind(…):將套接字和服務器主機上的一個地址進行綁定
  3. listen(…):監聽客戶端請求
  4. accept(…):接受一個特定的客戶端請求

上面的 socket 調用的完整形式為:

  1. int sockfd = socket(AF_INET,      /* versus AF_LOCAL */
  2.                     SOCK_STREAM,  /* reliable, bidirectional */
  3.                     0);           /* system picks protocol (TCP) */

第一個參數特別指定了使用的是一個網絡套接字,而不是 IPC 套接字。對于第二個參數有多種選項,但 SOCK_STREAMSOCK_DGRAM(數據報)是最為常用的。基于流的套接字支持可信通道,在這種通道中如果發生了信息的丟失或者更改,都將會被報告。這種通道是雙向的,并且從一端到另外一端的有效載荷在大小上可以是任意的。相反的,基于數據報的套接字大多是不可信的,沒有方向性,并且需要固定大小的載荷。socket 的第三個參數特別指定了協議。對于這里展示的基于流的套接字,只有一種協議選擇:TCP,在這里表示的 0。因為對 socket 的一次成功調用將返回相似的文件描述符,套接字可以被讀寫,對應的語法和讀寫一個本地文件是類似的。

bind 的調用是最為復雜的,因為它反映出了在套接字 API 方面上的各種改進。我們感興趣的點是這個調用將一個套接字和服務器端所在機器中的一個內存地址進行綁定。但對 listen 的調用就非常直接了:

  1. if (listen(fd, MaxConnects) < 0)

第一個參數是套接字的文件描述符,第二個參數則指定了在服務器端處理一個拒絕連接錯誤之前,有多少個客戶端連接被允許連接。(在頭文件 sock.hMaxConnects 的值被設置為 8。)

accept 調用默認將是一個阻塞等待:服務器端將不做任何事情直到一個客戶端嘗試連接它,然后進行處理。accept 函數返回的值如果是 -1 則暗示有錯誤發生。假如這個調用是成功的,則它將返回另一個文件描述符,這個文件描述符被用來指代另一個可讀可寫的套接字,它與 accept 調用中的第一個參數對應的接收套接字有所不同。服務器端使用這個可讀可寫的套接字來從客戶端讀取請求然后寫回它的回應。接收套接字只被用于接受客戶端的連接。

在設計上,服務器端可以一直運行下去。當然服務器端可以通過在命令行中使用 Ctrl+C 來終止它。

示例 2. 使用套接字的客戶端

  1. #include <string.h>
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. #include <unistd.h>
  5. #include <sys/types.h>
  6. #include <sys/socket.h>
  7. #include <arpa/inet.h>
  8. #include <netinet/in.h>
  9. #include <netinet/tcp.h>
  10. #include <netdb.h>
  11. #include "sock.h"
  12.  
  13. const char* books[] = {"War and Peace",
  14. "Pride and Prejudice",
  15. "The Sound and the Fury"};
  16.  
  17. void report(const char* msg, int terminate) {
  18. perror(msg);
  19. if (terminate) exit(-1); /* failure */
  20. }
  21.  
  22. int main() {
  23. /* fd for the socket */
  24. int sockfd = socket(AF_INET, /* versus AF_LOCAL */
  25. SOCK_STREAM, /* reliable, bidirectional */
  26. 0); /* system picks protocol (TCP) */
  27. if (sockfd < 0) report("socket", 1); /* terminate */
  28.  
  29. /* get the address of the host */
  30. struct hostent* hptr = gethostbyname(Host); /* localhost: 127.0.0.1 */
  31. if (!hptr) report("gethostbyname", 1); /* is hptr NULL? */
  32. if (hptr->h_addrtype != AF_INET) /* versus AF_LOCAL */
  33. report("bad address family", 1);
  34. /* connect to the server: configure server's address 1st */
  35. struct sockaddr_in saddr;
  36. memset(&saddr, 0, sizeof(saddr));
  37. saddr.sin_family = AF_INET;
  38. saddr.sin_addr.s_addr =
  39. ((struct in_addr*) hptr->h_addr_list[0])->s_addr;
  40. saddr.sin_port = htons(PortNumber); /* port number in big-endian */
  41. if (connect(sockfd, (struct sockaddr*) &saddr, sizeof(saddr)) < 0)
  42. report("connect", 1);
  43. /* Write some stuff and read the echoes. */
  44. puts("Connect to server, about to write some stuff...");
  45. int i;
  46. for (i = 0; i < ConversationLen; i++) {
  47. if (write(sockfd, books[i], strlen(books[i])) > 0) {
  48. /* get confirmation echoed from server and print */
  49. char buffer[BuffSize + 1];
  50. memset(buffer, '\0', sizeof(buffer));
  51. if (read(sockfd, buffer, sizeof(buffer)) > 0)
  52. puts(buffer);
  53. }
  54. }
  55. puts("Client done, about to exit...");
  56. close(sockfd); /* close the connection */
  57. return 0;
  58. }

客戶端程序的設置代碼和服務器端類似。兩者主要的區別既不是在于監聽也不在于接收,而是連接:

  1. if (connect(sockfd, (struct sockaddr*) &saddr, sizeof(saddr)) < 0)

connect 的調用可能因為多種原因而導致失敗,例如客戶端擁有錯誤的服務器端地址或者已經有太多的客戶端連接上了服務器端。假如 connect 操作成功,客戶端將在一個 for 循環中,寫入它的請求然后讀取返回的響應。在會話后,服務器端和客戶端都將調用 close 去關閉這個可讀可寫套接字,盡管任何一邊的關閉操作就足以關閉它們之間的連接。此后客戶端可以退出了,但正如前面提到的那樣,服務器端可以一直保持開放以處理其他事務。

從上面的套接字示例中,我們看到了請求信息被回顯給客戶端,這使得客戶端和服務器端之間擁有進行豐富對話的可能性。也許這就是套接字的主要魅力。在現代系統中,客戶端應用(例如一個數據庫客戶端)和服務器端通過套接字進行通信非常常見。正如先前提及的那樣,本地 IPC 套接字和網絡套接字只在某些實現細節上面有所不同,一般來說,IPC 套接字有著更低的消耗和更好的性能。它們的通信 API 基本是一樣的。

信號

信號會中斷一個正在執行的程序,在這種意義下,就是用信號與這個程序進行通信。大多數的信號要么可以被忽略(阻塞)或者被處理(通過特別設計的代碼)。SIGSTOP (暫停)和 SIGKILL(立即停止)是最應該提及的兩種信號。這種符號常量有整數類型的值,例如 SIGKILL 對應的值為 9

信號可以在與用戶交互的情況下發生。例如,一個用戶從命令行中敲了 Ctrl+C 來終止一個從命令行中啟動的程序;Ctrl+C 將產生一個 SIGTERM 信號。SIGTERM 意即終止,它可以被阻塞或者被處理,而不像 SIGKILL 信號那樣。一個進程也可以通過信號和另一個進程通信,這樣使得信號也可以作為一種 IPC 機制。

考慮一下一個多進程應用,例如 Nginx 網絡服務器是如何被另一個進程優雅地關閉的。kill 函數:

  1. int kill(pid_t pid, int signum); /* declaration */

可以被一個進程用來終止另一個進程或者一組進程。假如 kill 函數的第一個參數是大于 0 的,那么這個參數將會被認為是目標進程的 pid(進程 ID),假如這個參數是 0,則這個參數將會被視作信號發送者所屬的那組進程。

kill 的第二個參數要么是一個標準的信號數字(例如 SIGTERMSIGKILL),要么是 0 ,這將會對信號做一次詢問,確認第一個參數中的 pid 是否是有效的。這樣優雅地關閉一個多進程應用就可以通過向組成該應用的一組進程發送一個終止信號來完成,具體來說就是調用一個 kill 函數,使得這個調用的第二個參數是 SIGTERM 。(Nginx 主進程可以通過調用 kill 函數來終止其他工人進程,然后再停止自己。)就像許多庫函數一樣,kill 函數通過一個簡單的可變語法擁有更多的能力和靈活性。

示例 3. 一個多進程系統的優雅停止

  1. #include <stdio.h>
  2. #include <signal.h>
  3. #include <stdlib.h>
  4. #include <unistd.h>
  5. #include <sys/wait.h>
  6.  
  7. void graceful(int signum) {
  8.   printf("\tChild confirming received signal: %i\n", signum);
  9.   puts("\tChild about to terminate gracefully...");
  10.   sleep(1);
  11.   puts("\tChild terminating now...");
  12.   _exit(0); /* fast-track notification of parent */
  13. }
  14.  
  15. void set_handler() {
  16.   struct sigaction current;
  17.   sigemptyset(&current.sa_mask);         /* clear the signal set */
  18.   current.sa_flags = 0;                  /* enables setting sa_handler, not sa_action */
  19.   current.sa_handler = graceful;         /* specify a handler */
  20.   sigaction(SIGTERM, &current, NULL);    /* register the handler */
  21. }
  22.  
  23. void child_code() {
  24.   set_handler();
  25.  
  26.   while (1) {   /` loop until interrupted `/
  27.     sleep(1);
  28.     puts("\tChild just woke up, but going back to sleep.");
  29.   }
  30. }
  31.  
  32. void parent_code(pid_t cpid) {
  33.   puts("Parent sleeping for a time...");
  34.   sleep(5);
  35.  
  36.   /* Try to terminate child. */
  37.   if (-1 == kill(cpid, SIGTERM)) {
  38.     perror("kill");
  39.     exit(-1);
  40.   }
  41.   wait(NULL); /` wait for child to terminate `/
  42.   puts("My child terminated, about to exit myself...");
  43. }
  44.  
  45. int main() {
  46.   pid_t pid = fork();
  47.   if (pid < 0) {
  48.     perror("fork");
  49.     return -1; /* error */
  50.   }
  51.   if (0 == pid)
  52.     child_code();
  53.   else
  54.     parent_code(pid);
  55.   return 0;  /* normal */
  56. }

上面的停止程序模擬了一個多進程系統的優雅退出,在這個例子中,這個系統由一個父進程和一個子進程組成。這次模擬的工作流程如下:

  • 父進程嘗試去 fork 一個子進程。假如這個 fork 操作成功了,每個進程就執行它自己的代碼:子進程就執行函數 child_code,而父進程就執行函數 parent_code
  • 子進程將會進入一個潛在的無限循環,在這個循環中子進程將睡眠一秒,然后打印一個信息,接著再次進入睡眠狀態,以此循環往復。來自父進程的一個 SIGTERM 信號將引起子進程去執行一個信號處理回調函數 graceful。這樣這個信號就使得子進程可以跳出循環,然后進行子進程和父進程之間的優雅終止。在終止之前,進程將打印一個信息。
  • fork 一個子進程后,父進程將睡眠 5 秒,使得子進程可以執行一會兒;當然在這個模擬中,子進程大多數時間都在睡眠。然后父進程調用 SIGTERM 作為第二個參數的 kill 函數,等待子進程的終止,然后自己再終止。

下面是一次運行的輸出:

  1. % ./shutdown
  2. Parent sleeping for a time...
  3.         Child just woke up, but going back to sleep.
  4.         Child just woke up, but going back to sleep.
  5.         Child just woke up, but going back to sleep.
  6.         Child just woke up, but going back to sleep.
  7.         Child confirming received signal: 15  ## SIGTERM is 15
  8.         Child about to terminate gracefully...
  9.         Child terminating now...
  10. My child terminated, about to exit myself...

對于信號的處理,上面的示例使用了 sigaction 庫函數(POSIX 推薦的用法)而不是傳統的 signal 函數,signal 函數有移植性問題。下面是我們主要關心的代碼片段:

  • 假如對 fork 的調用成功了,父進程將執行 parent_code 函數,而子進程將執行 child_code 函數。在給子進程發送信號之前,父進程將會等待 5 秒:

    1. puts("Parent sleeping for a time...");
    2. sleep(5);
    3. if (-1 == kill(cpid, SIGTERM)) {
    4. ...sleepkillcpidSIGTERM...

    假如 kill 調用成功了,父進程將在子進程終止時做等待,使得子進程不會變成一個僵尸進程。在等待完成后,父進程再退出。

  • child_code 函數首先調用 set_handler 然后進入它的可能永久睡眠的循環。下面是我們將要查看的 set_handler 函數:

    1. void set_handler() {
    2.   struct sigaction current;            /* current setup */
    3.   sigemptyset(&current.sa_mask);       /* clear the signal set */
    4.   current.sa_flags = 0;                /* for setting sa_handler, not sa_action */
    5.   current.sa_handler = graceful;       /* specify a handler */
    6.   sigaction(SIGTERM, &current, NULL);  /* register the handler */
    7. }

    上面代碼的前三行在做相關的準備。第四個語句將為 graceful 設定為句柄,它將在調用 _exit 來停止之前打印一些信息。第 5 行和最后一行的語句將通過調用 sigaction 來向系統注冊上面的句柄。sigaction 的第一個參數是 SIGTERM ,用作終止;第二個參數是當前的 sigaction 設定,而最后的參數(在這個例子中是 NULL )可被用來保存前面的 sigaction 設定,以備后面的可能使用。

使用信號來作為 IPC 的確是一個很輕量的方法,但確實值得嘗試。通過信號來做 IPC 顯然可以被歸入 IPC 工具箱中。

這個系列的總結

在這個系列中,我們通過三篇有關 IPC 的文章,用示例代碼介紹了如下機制:

  • 共享文件
  • 共享內存(通過信號量)
  • 管道(命名和無名)
  • 消息隊列
  • 套接字
  • 信號

甚至在今天,在以線程為中心的語言,例如 Java、C# 和 Go 等變得越來越流行的情況下,IPC 仍然很受歡迎,因為相比于使用多線程,通過多進程來實現并發有著一個明顯的優勢:默認情況下,每個進程都有它自己的地址空間,除非使用了基于共享內存的 IPC 機制(為了達到安全的并發,競爭條件在多線程和多進程的時候必須被加上鎖),在多進程中可以排除掉基于內存的競爭條件。對于任何一個寫過即使是基本的通過共享變量來通信的多線程程序的人來說,他都會知道想要寫一個清晰、高效、線程安全的代碼是多么具有挑戰性。使用單線程的多進程的確是很有吸引力的,這是一個切實可行的方式,使用它可以利用好今天多處理器的機器,而不需要面臨基于內存的競爭條件的風險。

當然,沒有一個簡單的答案能夠回答上述 IPC 機制中的哪一個更好。在編程中每一種 IPC 機制都會涉及到一個取舍問題:是追求簡潔,還是追求功能強大。以信號來舉例,它是一個相對簡單的 IPC 機制,但并不支持多個進程之間的豐富對話。假如確實需要這樣的對話,另外的選擇可能會更合適一些。帶有鎖的共享文件則相對直接,但是當要處理大量共享的數據流時,共享文件并不能很高效地工作。管道,甚至是套接字,有著更復雜的 API,可能是更好的選擇。讓具體的問題去指導我們的選擇吧。

盡管所有的示例代碼(可以在我的網站上獲取到)都是使用 C 寫的,其他的編程語言也經常提供這些 IPC 機制的輕量包裝。這些代碼示例都足夠短小簡單,希望這樣能夠鼓勵你去進行實驗。 

責任編輯:龐桂玉 來源: Linux中國
相關推薦

2010-01-05 10:00:48

Linux進程間通信

2019-05-08 11:10:05

Linux進程語言

2019-05-13 10:00:41

Linux進程間通信命令

2024-01-03 10:17:51

Linux通信

2016-11-28 14:11:24

ANDROID BIN通信架構

2017-08-06 00:05:18

進程通信開發

2018-05-30 13:58:02

Linux進程通信

2018-01-12 14:35:00

Linux進程共享內存

2017-06-19 13:36:12

Linux進程消息隊列

2010-07-06 15:33:10

UDP套接字

2020-11-04 07:17:42

Nodejs通信進程

2011-06-22 17:09:50

QT 進程 通信

2025-04-27 03:22:00

2013-03-28 13:14:45

AIDL進程間通信Android使用AI

2019-11-27 10:36:11

進程通信IPC

2022-07-04 08:29:13

electron通信

2019-11-08 14:47:49

TCPIP網絡

2025-05-07 08:03:10

2011-06-24 14:01:34

Qt QCOP 協議

2025-05-13 07:10:31

點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 欧美日韩看片 | 黄色欧美大片 | 少妇久久久 | 国产精品一区二区视频 | 国产视频在线一区二区 | 国内精品久久久久久 | 久久精品a级毛片 | 992人人草| 一级特黄视频 | 国产成人综合亚洲欧美94在线 | 国产精品自拍av | 久久综合色综合 | 日本羞羞影院 | 免费久久99精品国产婷婷六月 | 国产精品呻吟久久av凹凸 | 在线播放国产一区二区三区 | 国产高清在线精品 | 欧美日韩国产一区二区三区 | 国产一区二区三区日韩 | 成人h电影在线观看 | 超碰97人人人人人蜜桃 | 国产精品美女久久久久久久久久久 | 午夜一区二区三区 | 国产精品免费观看视频 | 亚洲aⅴ | 国产第二页 | 在线成人av | 免费超碰 | 久久精品免费 | 精品欧美一区二区三区久久久 | 在线观看视频你懂得 | 精品一区在线免费观看 | 成年人在线观看 | 精品综合久久久 | 婷婷五月色综合香五月 | 精品欧美一区二区三区免费观看 | 高清国产一区二区 | 在线视频一区二区三区 | 在线观看国产精品视频 | 久在线 | 911精品美国片911久久久 |