進程間通訊的7種方式_進程間通信的幾種方法-CSDN博客
- 管道 pipe(命名管道和匿名管道);
- 信號 signal;
- 共享內存;
- 消息隊列;
- 信號量 semaphore;
- 套接字 socket;
1. 管道
內核提供,單工,自同步機制。?
1.1 匿名管道
磁盤上無法看到,只能有親緣關系的進程才能用匿名管道。一般用于父子進程間通信。
pipe(2) 系統調用可以創建一個匿名管道 pipefd,文件描述符 pipefd[0] 為讀管道,pipefd[1] 為寫管道。??
#include <unistd.h>int pipe(int pipefd[2]);#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <fcntl.h> /* Obtain O_* constant definitions */
#include <unistd.h>int pipe2(int pipefd[2], int flags);
例子,父進程通過管道發送 hello 給子進程:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>int main()
{pid_t pid;int pipefd[2];if (pipe(pipefd) < 0) {perror("pipe");exit(1);}pid = fork();if (pid > 0) {// parentclose(pipefd[0]);write(pipefd[1], "hello", 5);close(pipefd[1]);wait(NULL);exit(0);}else if (pid == 0) {// childclose(pipefd[1]);char buf[50];int len = read(pipefd[0], buf, 50);printf("%d\n", len);write(1, buf, len);close(pipefd[0]);exit(0);}else {perror("fork");exit(1);}exit(0);
}
1.2 命名管道
磁盤上能看到,文件類型為 p 的文件。
mkfifo(3) 函數可以創建一個 fifo 特殊文件(命名管道)。
#include <sys/types.h>
#include <sys/stat.h>int mkfifo(const char *pathname, mode_t mode);
2 XSI -> SysV
2.1?消息隊列,message queue?
主動端,先發包的一方;被動端,先收包的一方;消息隊列可以用于沒有親緣關系的進程間通信。
例子,proto.h,包含了傳遞的內容:
#ifndef PROTO_H__
#define PROTO_H__#define KEYPATH "/etc/services"
#define KEYPROJ 'g'#define NAMESIZE 32struct msg_st
{long mtype;char name[NAMESIZE];int math;int chinese;
};#endif
receiver.c,接收者:
#include <stdio.h>
#include <stdlib.h>
#include "proto.h"
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>int main()
{key_t key;int msgid;struct msg_st rbuf;key = ftok(KEYPATH, KEYPROJ);if (key < 0) {perror("ftok");exit(1);}msgid = msgget(key, IPC_CREAT | 0600);if (msgid < 0) {perror("msgget");exit(1);}while (1){if (msgrcv(msgid, &rbuf, sizeof(rbuf) - sizeof(long), 0, 0) < 0){perror("msgrcv");exit(1);}printf("NAME = %s\n", rbuf.name);printf("MATH = %d\n", rbuf.math);printf("CHINESE = %d\n", rbuf.chinese);}msgctl(msgid, IPC_RMID, NULL);exit(0);
}
運行接收者可以看到創建的 msg queue:?
sender.c,發送者:?
#include <stdio.h>
#include <stdlib.h>
#include "proto.h"
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>int main()
{key_t key;struct msg_st sbuf;int msgid;key = ftok(KEYPATH, KEYPROJ);if (key < 0) {perror("ftock");exit(1);}msgid = msgget(key, 0);if (msgid < 0){perror("msgget");exit(1);}sbuf.mtype = 1;strcpy(sbuf.name, "lzp");sbuf.math = 99;sbuf.chinese = 100;if (msgsnd(msgid, &sbuf, sizeof(sbuf) - sizeof(long), 0) < 0) {perror("msgsend");exit(1);}puts("ok!");exit(0);
}
先運行 receiver,然后運行 sender 發送信息:?
2.2 信號量,semaphore arrays
semget、semop、semctl;可以用于沒有親緣關系的進程間通信。
2.3 共享內存,shared memory segment
shmget、shmop、shmctl;之前可以通過 mmap 實現共享內存,不過只能在有親緣關系的進程間通信。匿名 ipc 也只能在有親緣關系的進程間通信;
3. 網絡套接字 socket
3.1 跨主機的傳輸
- 字節序:大端存儲(低地址處放高字節)和小端存儲(低地址處放低字節,x86);主機字節序(host)和網絡字節序(network);主機序轉網絡序,網絡序轉主機序;htons,htonl,ntohs,ntohl。
- 對齊:結構體中 char 類型可能會占 4 個字節;如果不對齊,那么 int 類型的存儲地址可能就不是在 4 的倍數的地址上了,可能需要兩次訪存才能取出一個 int 類型。深入理解字節對齊-CSDN博客
- 類型長度問題:不同主機間的架構,機器字長可能不一樣,結構體類型大小也可能不一樣。解決方案(int32_t,int16_t ...);
socket(2) 系統調用如下:?
#include <sys/types.h>
#include <sys/socket.h>int socket(int domain, int type, int protocol);
- domain 指定協議族;
- AF_INET ? ? ?IPv4 Internet protocols? ? ip(7)
- AF_INET6 ? ? IPv6 Internet protocols? ?ipv6(7)
- type 指定通信語義;
- SOCK_STREAM(流式傳輸):Provides sequenced, reliable, two-way, connection-based byte streams.
- SOCK_DGRAM(報式傳輸)? :Supports datagrams (connectionless, unreliable messages of a fixed maximum length).
- protocol 指定協議族中的協議;
bind(2) 系統調用可以給 socket 綁定地址。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
recvfrom(2) 可以從 socket 上接收一條 message:?
#include <sys/types.h>
#include <sys/socket.h>ssize_t recv(int sockfd, void *buf, size_t len, int flags);ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
- recv(2) 是用于流式套接字的,是提前建立好連接的,一對一的,點對點的,?不需要記錄對方的 socket 地址;
- recvfrom(2) 可以用于報式和流式套接字,每次需要傳入需要通信的 socket 地址;
sendto(2) 函數可以發送 msg 到對應的 socket 地址,可以用于流式和報式傳輸;而?send(2) 函數不需要指定 socket 地址,只能用于事先建立好連接的流式傳輸。
#include <sys/types.h>
#include <sys/socket.h>ssize_t send(int sockfd, const void *buf, size_t len, int flags);ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
3.2 報式套接字(udp)
被動端(先運行):
- 取得 socket(socket);
- 給 socket 取得地址,相當于綁定本地的地址(bind);
- 收/發消息(recvfrom);
- 關閉 socket(close);
主動端:
- 取得 socket;
- 給 socket 取得地址(可省略);
- 收/發消息;
- 關閉 socket;
__attribute__((packed)):packed屬性:使用該屬性可以使得變量或者結構體成員使用最小的對齊方式,即對變量是一字節對齊,對域(field)是位對齊。即不進行對齊。
使用如下指令查看被動段的地址和端口(u代表udp):
netstat -anu
proto.h 約定傳輸格式和內容:?
#ifndef PROTO_H__
#define PROTO_H__#define RCVPORT 1989#define NAMESIZE 11// communication struct
struct msg_st
{char name[NAMESIZE];int math;int ch;
} __attribute__((packed));#endif
receiver.c 接收方, 注意多字節需要使用?ntohl() 來轉換:
#include <stdio.h>
#include <stdlib.h>
#include "proto.h"
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>#define IPSTRSIZE 40int main()
{int sd; // socket fdstruct sockaddr_in laddr;struct sockaddr_in raddr;struct msg_st rbuf;socklen_t raddr_len;char ipstr[IPSTRSIZE];// IPV4 DGRAM UDPsd = socket(AF_INET, SOCK_DGRAM, 0/* IPPROTO_UDP */);if (sd < 0) {perror("socket");exit(1);}laddr.sin_family = AF_INET;laddr.sin_port = htons(RCVPORT);inet_pton(AF_INET, "0.0.0.0", &laddr.sin_addr);if (bind(sd, (struct sockaddr *)&laddr, sizeof(laddr)) < 0) {perror("bindqqq");exit(1);}/* !!! */raddr_len = sizeof(raddr);while (1){recvfrom(sd, &rbuf, sizeof(rbuf), 0, (struct sockaddr *)&raddr, &raddr_len);inet_ntop(AF_INET, &raddr.sin_addr, ipstr, IPSTRSIZE);printf("message from %s:%d---\n", ipstr, ntohs(raddr.sin_port));printf("name = %s\n", rbuf.name);printf("name = %d\n", ntohl(rbuf.math));printf("name = %d\n", ntohl(rbuf.ch));}close(sd);exit(0);
}
sender.c 發送方,發送報文給對應的地址:?
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include "proto.h"
#include <unistd.h>int main(int argc, char *argv[])
{int sd;struct msg_st sbuf;struct sockaddr_in raddr;if (argc < 2){fprintf(stderr, "usage..\n");exit(1);}sd = socket(AF_INET, SOCK_DGRAM, 0);if (sd < 0){perror("socket");exit(1);}//bind();strcpy(sbuf.name, "Alan");sbuf.math = htonl(99);sbuf.ch = htonl(93);raddr.sin_family = AF_INET;raddr.sin_port = htons(RCVPORT);inet_pton(AF_INET, argv[1], &raddr.sin_addr);if (sendto(sd, &sbuf, sizeof(sbuf), 0, (struct sockaddr *)&raddr, sizeof(raddr)) < 0){perror("sendto");exit(1);}puts("ok\n");close(sd);exit(0);
}
運行結果:?
報式套接字還能實現多播、廣播(全網廣播和子網廣播)、組播:
可以通過 getsockopt() 和 setsockopt() 來打開廣播選項。然后將發送的目標地址改成 255.255.255.255 就可以發送廣播了。
多點通信(廣播、多播、組播)只能用報式套接字實現,因為流式套接字是一對一的,點對點的。
udp 會出現丟包的問題,TTL生存周期(路由跳轉個數)?,丟包是由阻塞造成的,當等待隊列快滿的時候會發生丟包(網絡太擁塞),可以使用流量控制解決(限制發送端的速率)。
3.3 流式套接字(tcp)
協議,約定雙方對話的格式。
三次握手容易被ddos攻擊,可以去掉半連接池,然后使用cookie(對方的ip+端口加上我放的ip+端口加上一個內核產生的令牌)。
Client 端和 Server 端:
client:
- 獲取 socket;
- 給 socket 取得地址;
- 發送連接;
- 收/發消息;
- 關閉連接;
server:
- 獲取 socket;
- 給 socket 取得地址;
- 將 socket 置為監聽模式;
- 接受連接;
- 收/發消息;
- 關閉;
服務端(server.c):
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <time.h>
#include "proto.h"#define IPSTRSIZE 40
#define BUFSIZE 1024static void server_job(int sd)
{char buf[BUFSIZE];int len = sprintf(buf, FMT_STAMP, (long long)time(NULL));if (send(sd, buf, len, 0) < 0) {perror("send()");exit(1);}
}int main()
{struct sockaddr_in laddr, raddr;socklen_t raddr_len;char ipstr[IPSTRSIZE];int sd = socket(AF_INET, SOCK_STREAM, 0/* default IPPROTO_TCP */);if (sd < 0) {perror("socket()");exit(1);}laddr.sin_family = AF_INET;laddr.sin_port = htons(atoi(SERVERPORT));inet_pton(AF_INET, "0.0.0.0", &laddr.sin_addr);if (bind(sd, (void *)&laddr, sizeof(laddr)) < 0) {perror("bind()");exit(1);}// listen for connections on a socketif (listen(sd, 200) < 0){perror("listen()");exit(1);}raddr_len = sizeof(raddr);while (1) {// accept a connection on a socketint newsd;if ((newsd = accept(sd, (void *)&raddr, &raddr_len)) < 0) {perror("accept()");exit(1);}inet_ntop(AF_INET, &raddr.sin_addr, ipstr, IPSTRSIZE);printf("Client: %s : %d\n", ipstr, ntohs(raddr.sin_port));server_job(newsd);// close newsdclose(newsd);}close(sd);exit(0);
}
運行后使用 netstat -ant 可以查看:
當連接被釋放后,服務端會進入一段時間的 timewait 狀態。
客戶端(client.c):
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#include "proto.h"
int main(int argc, char* argv[])
{struct sockaddr_in raddr;long long stamp;if (argc < 2) {fprintf(stderr, "Usage...\n");exit(1);}int sd = socket(AF_INET, SOCK_STREAM, 0);if (sd < 0) {perror("socket()");exit(1);}// initiate a connection on a socketraddr.sin_family = AF_INET;raddr.sin_port = htons(atoi(SERVERPORT));inet_pton(AF_INET, argv[1], &raddr.sin_addr);if (connect(sd, (void *)&raddr, sizeof(raddr)) < 0) {perror("connect()");exit(1);}FILE* fp = fdopen(sd, "r+");if (fp == NULL) {perror("fdopen()");exit(1);}if (fscanf(fp, FMT_STAMP, &stamp) < 1) {fprintf(stderr, "bad format\n");}else {fprintf(stdout, "stamp = %lld\n", stamp);}fclose(fp);exit(0);
}
上面這種方法有一個缺點,就是假如 server_job 的任務執行時間太長的話,需要等待 server_job 執行完后才能繼續 accept 下一個連接請求,這樣效率十分低,所以可以采用多線程的方式來解決,每個進程或線程處理一個連接請求。