寫在前面:本博客為本人原創,嚴禁任何形式的轉載!本博客只允許放在博客園(.cnblogs.com),如果您在其他網站看到這篇博文,請通過下面這個唯一的合法鏈接轉到原文!
本博客全網唯一合法URL:http://www.cnblogs.com/acm-icpcer/p/8933628.html
(本篇博客參考了:https://blog.csdn.net/a987073381/article/details/52006729)
在linux下的多個進程間的通信機制叫做IPC(Inter-Process Communication),它是多個進程之間相互溝通的一種方法。在linux下有多種進程間通信的方法:半雙工管道、命名管道、消息隊列、信號、信號量、共享內存、內存映射文件,套接字等等。使用這些機制可以為linux下的網絡服務器開發提供靈活而又堅固的框架。在這篇博客中我實現了其中的幾種機制,詳細如下:
1、無名管道:
管道實際是用於進程間通信的一段共享內存,創建管道的進程稱為管道服務器,連接到一個管道的進程為管道客戶機。一個進程在向管道寫入數據后,另一進程就可以從管道的另一端將其讀取出來。
管道的特點:
(1)管道是半雙工的,數據只能向一個方向流動;需要雙方通信時,需要建立起兩個管道;
(2)只能用於父子進程或者兄弟進程之間(具有親緣關系的進程)。比如fork或exec創建的新進程,在使用exec創建新進程時,需要將管道的文件描述符作為參數傳遞給exec創建的新進程。當父進程與使用fork創建的子進程直接通信時,發送數據的進程關閉讀端,接受數據的進程關閉寫端。
(3)單獨構成一種獨立的文件系統:管道對於管道兩端的進程而言,就是一個文件,但它不是普通的文件,它不屬於某種文件系統,而是自立門戶,單獨構成一種文件系統,並且只存在與內存中。
(4)數據的讀出和寫入:一個進程向管道中寫的內容被管道另一端的進程讀出。寫入的內容每次都添加在管道緩沖區的末尾,並且每次都是從緩沖區的頭部讀出數據。
管道的實現機制:
管道是由內核管理的一個緩沖區,相當於我們放入內存中的一個紙條。管道的一端連接一個進程的輸出。這個進程會向管道中放入信息。管道的另一端連接一個進程的輸入,這個進程取出被放入管道的信息。一個緩沖區不需要很大,它被設計成為環形的數據結構,以便管道可以被循環利用。當管道中沒有信息的話,從管道中讀取的進程會等待,直到另一端的進程放入信息。當管道被放滿信息的時候,嘗試放入信息的進程會等待,直到另一端的進程取出信息。當兩個進程都終結的時候,管道也自動消失。
管道只能在本地計算機中使用,而不可用於網絡間的通信。
#include <unistd.h> #include <sys/types.h> #include <errno.h> #include <stdlib.h> #include <stdio.h> #include <string.h> int main() { int pipe_fd[2]; pid_t pid; char r_buf[10]; char w_buf[4]; int r_num; memset(r_buf,0,sizeof(r_buf)); memset(w_buf,0,sizeof(w_buf)); if(pipe(pipe_fd)<0) { printf("pipe create error\n"); return -1; } if((pid=fork())==0) { printf("\n"); close(pipe_fd[1]); sleep(3);//確保父進程關閉寫端 r_num=read(pipe_fd[0],r_buf,10); printf( "read num is %d the data read from the pipe is %d\n",r_num,atoi(r_buf)); close(pipe_fd[0]); exit(1); } else if(pid>0) { close(pipe_fd[0]);//close read strcpy(w_buf,"111"); if(write(pipe_fd[1],w_buf,4)!=-1) printf("parent write over\n"); printf("parent close fd[1] over\n"); close(pipe_fd[1]);//write sleep(10); } return 0; }
2、有名管道:
命名管道是一種特殊類型的文件,它在系統中以文件形式存在。這樣克服了無名管道的弊端,他可以允許沒有親緣關系的進程間通信。
無名管道和命名管道的區別:
對於命名管道FIFO來說,IO操作和普通管道IO操作基本一樣,但是兩者有一個主要的區別,在命名管道中,管道可以是事先已經創建好的,比如我們在命令行下執行mkfifo myfifo就是創建一個命名通道,我們必須用open函數來顯示地建立連接到管道的通道,而在管道中,管道已經在主進程里創建好了,然后在fork時直接復制相關數據或者是用exec創建的新進程時把管道的文件描述符當參數傳遞進去。
一般來說FIFO和PIPE一樣總是處於阻塞狀態。也就是說如果命名管道FIFO打開時設置了讀權限,則讀進程將一直阻塞,一直到其他進程打開該FIFO並向管道寫入數據。這個阻塞動作反過來也是成立的。如果不希望命名管道操作的時候發生阻塞,可以在open的時候使用O_NONBLOCK標志,以關閉默認的阻塞操作。
//writing #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <sys/stat.h> #include <errno.h> #include <fcntl.h> #define N 80 int main() { int out_file; int nbyte; char buf[N]; if((mkfifo("myfifo",0666))<0) //創建有名管道 { if(errno==EEXIST) { printf("The fifo is exist.\n"); } else{ perror("creat myfifo failed!\n"); exit(-1); } }else{ printf("created by this process.\n"); } out_file = open("myfifo",O_WRONLY); if (out_file < 0) { printf("Error opening fifo."); exit(1); } printf("please input something:\n"); while((nbyte = read(0,buf,N))){ write(out_file,buf,nbyte); printf("please input something:\n"); } close(out_file); return 0; }
//reading #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <errno.h> #include <fcntl.h> #define N 80 int main(void) { int in_file; int count = 1; char buf[N]; if((mkfifo("myfifo",0666))<0)//創建有名管道 { if(errno==EEXIST)//管道已經存在 { printf("The fifo is exist.\n"); } else{ printf("creat myfifo failed!\n"); exit(-1); } } else { printf("created by this process.\n"); } in_file = open("myfifo",O_RDONLY); if (in_file < 0) { printf("Error in opening.\n"); exit(1); } while ((count = read(in_file,buf,N)) > 0) { printf("received from fifo: %s\n", buf); memset(buf,0,N); } close(in_file); return 0; }
3、C/S:
//client.c #include <stdio.h> #include <stdlib.h> #include <sys/msg.h> // 用於創建一個唯一的key #define MSG_FILE "/etc/passwd" // 消息結構 struct msg_form { long mtype; char mtext[256]; }; int main() { int msqid; key_t key; struct msg_form msg; // 獲取key值 if ((key = ftok(MSG_FILE, 'z')) < 0) { perror("ftok error"); exit(1); } // 打印key值 printf("Message Queue - Client key is: %d.\n", key); // 打開消息隊列 if ((msqid = msgget(key, IPC_CREAT|0777)) == -1) { perror("msgget error"); exit(1); } // 打印消息隊列ID及進程ID printf("My msqid is: %d.\n", msqid); printf("My pid is: %d.\n", getpid()); // 添加消息,類型為888 msg.mtype = 888; sprintf(msg.mtext, "hello, I'm client %d", getpid()); msgsnd(msqid, &msg, sizeof(msg.mtext), 0); // 讀取類型為777的消息 msgrcv(msqid, &msg, 256, 999, 0); printf("Client: receive msg.mtext is: %s.\n", msg.mtext); printf("Client: receive msg.mtype is: %d.\n", msg.mtype); return 0; }
//server.c #include <stdio.h> #include <stdlib.h> #include <sys/msg.h> // 用於創建一個唯一的key #define MSG_FILE "/etc/passwd" // 消息結構 struct msg_form { long mtype; char mtext[256]; }; int main() { int msqid; key_t key; struct msg_form msg; // 獲取key值 if((key = ftok(MSG_FILE,'z')) < 0) { perror("ftok error"); exit(1); } // 打印key值 printf("Message Queue - Server key is: %d.\n", key); // 創建消息隊列 if ((msqid = msgget(key, IPC_CREAT|0777)) == -1) { perror("msgget error"); exit(1); } // 打印消息隊列ID及進程ID printf("My msqid is: %d.\n", msqid); printf("My pid is: %d.\n", getpid()); // 循環讀取消息 for(;;) { msgrcv(msqid, &msg, 256, 888, 0);// 返回類型為888的第一個消息 printf("Server: receive msg.mtext is: %s.\n", msg.mtext); printf("Server: receive msg.mtype is: %d.\n", msg.mtype); msg.mtype = 999; // 客戶端接收的消息類型 sprintf(msg.mtext, "hello, I'm server %d", getpid()); msgsnd(msqid, &msg, sizeof(msg.mtext), 0); } return 0; }
4、讀者/寫者:
//write #include<stdio.h> #include<stdlib.h> // exit #include<fcntl.h> // O_WRONLY #include<sys/stat.h> #include<time.h> // time int main() { int fd; int n, i; char buf[1024]; time_t tp; printf("I am %d process.\n", getpid()); // 說明進程ID if((fd = open("fifo1", O_WRONLY)) < 0) // 以寫打開一個FIFO { perror("Open FIFO Failed"); exit(1); } for(i=0; i<10; ++i) { time(&tp); // 取系統當前時間 n=sprintf(buf,"Process %d's time is %s",getpid(),ctime(&tp)); printf("Send message: %s", buf); // 打印 if(write(fd, buf, n+1) < 0) // 寫入到FIFO中 { perror("Write FIFO Failed"); close(fd); exit(1); } sleep(1); // 休眠1秒 } close(fd); // 關閉FIFO文件 return 0; }
//read #include<stdio.h> #include<stdlib.h> #include<errno.h> #include<fcntl.h> #include<sys/stat.h> int main() { int fd; int len; char buf[1024]; if(mkfifo("fifo1", 0666) < 0 && errno!=EEXIST) // 創建FIFO管道 perror("Create FIFO Failed"); if((fd = open("fifo1", O_RDONLY)) < 0) // 以讀打開FIFO { perror("Open FIFO Failed"); exit(1); } while((len = read(fd, buf, 1024)) > 0) // 讀取FIFO管道 printf("Read message: %s", buf); close(fd); // 關閉FIFO文件 return 0; }
5、信號量機制:
信號量是一種計數器,用於控制對多個進程共享的資源進行的訪問。它們常常被用作一個鎖機制,在某個進程正在對特定的資源進行操作時,信號量可以防止另一個進程去訪問它。
信號量是特殊的變量,它只取正整數值並且只允許對這個值進行兩種操作:等待(wait)和信號(signal)。(P、V操作,P用於等待,V用於信號)
P(sv):如果sv的值大於0,就給它減1;如果它的值等於0,就掛起該進程的執行
V(sv):如果有其他進程因等待sv而被掛起,就讓它恢復運行;如果沒有其他進程因等待sv而掛起,則給它加1
簡單理解就是P相當於申請資源,V相當於釋放資源
操作系統課程里面大量提到過信號量機制,故在此不贅述。
#include<stdio.h>
#include<stdlib.h> #include<sys/sem.h> // 聯合體,用於semctl初始化 union semun { int val; /*for SETVAL*/ struct semid_ds *buf; unsigned short *array; }; // 初始化信號量 int init_sem(int sem_id, int value) { union semun tmp; tmp.val = value; if(semctl(sem_id, 0, SETVAL, tmp) == -1) { perror("Init Semaphore Error"); return -1; } return 0; } // P操作: // 若信號量值為1,獲取資源並將信號量值-1 // 若信號量值為0,進程掛起等待 int sem_p(int sem_id) { struct sembuf sbuf; sbuf.sem_num = 0; /*序號*/ sbuf.sem_op = -1; /*P操作*/ sbuf.sem_flg = SEM_UNDO; if(semop(sem_id, &sbuf, 1) == -1) { perror("P operation Error"); return -1; } return 0; } // V操作: // 釋放資源並將信號量值+1 // 如果有進程正在掛起等待,則喚醒它們 int sem_v(int sem_id) { struct sembuf sbuf; sbuf.sem_num = 0; /*序號*/ sbuf.sem_op = 1; /*V操作*/ sbuf.sem_flg = SEM_UNDO; if(semop(sem_id, &sbuf, 1) == -1) { perror("V operation Error"); return -1; } return 0; } // 刪除信號量集 int del_sem(int sem_id) { union semun tmp; if(semctl(sem_id, 0, IPC_RMID, tmp) == -1) { perror("Delete Semaphore Error"); return -1; } return 0; } int main() { int sem_id; // 信號量集ID key_t key; pid_t pid; // 獲取key值 if((key = ftok(".", 'z')) < 0) { perror("ftok error"); exit(1); } // 創建信號量集,其中只有一個信號量 if((sem_id = semget(key, 1, IPC_CREAT|0666)) == -1) { perror("semget error"); exit(1); } // 初始化:初值設為0資源被占用 init_sem(sem_id, 0); if((pid = fork()) == -1) perror("Fork Error"); else if(pid == 0) /*子進程*/ { sleep(2); printf("Process child: pid=%d\n", getpid()); sem_v(sem_id); /*釋放資源*/ } else /*父進程*/ { sem_p(sem_id); /*等待資源*/ printf("Process father: pid=%d\n", getpid()); sem_v(sem_id); /*釋放資源*/ del_sem(sem_id); /*刪除信號量集*/ } return 0; }
6、共享內存:
共享內存是在多個進程之間共享內存區域的一種進程間的通信方式,由IPC為進程創建的一個特殊地址范圍,它將出現在該進程的地址空間中。其他進程可以將同一段共享內存連接到自己的地址空間中。所有進程都可以訪問共享內存中的地址,就好像它們是malloc分配的一樣。如果一個進程向共享內存中寫入了數據,所做的改動將立刻被其他進程看到。
共享內存是IPC最快捷的方式,因為共享內存方式的通信沒有中間過程,而管道、消息隊列等方式則是需要將數據通過中間機制進行轉換。共享內存方式直接將某段內存段進行映射,多個進程間的共享內存是同一塊的物理空間,僅僅映射到各進程的地址不同而已,因此不需要進行復制,可以直接使用此段空間。
注意:共享內存本身並沒有同步機制,需要程序員自己控制。
////share_mem_client #include<stdio.h> #include<stdlib.h> #include<sys/shm.h> // shared memory #include<sys/sem.h> // semaphore #include<sys/msg.h> // message queue #include<string.h> // memcpy // 消息隊列結構 struct msg_form { long mtype; char mtext; }; // 聯合體,用於semctl初始化 union semun { int val; /*for SETVAL*/ struct semid_ds *buf; unsigned short *array; }; // P操作: // 若信號量值為1,獲取資源並將信號量值-1 // 若信號量值為0,進程掛起等待 int sem_p(int sem_id) { struct sembuf sbuf; sbuf.sem_num = 0; /*序號*/ sbuf.sem_op = -1; /*P操作*/ sbuf.sem_flg = SEM_UNDO; if(semop(sem_id, &sbuf, 1) == -1) { perror("P operation Error"); return -1; } return 0; } // V操作: // 釋放資源並將信號量值+1 // 如果有進程正在掛起等待,則喚醒它們 int sem_v(int sem_id) { struct sembuf sbuf; sbuf.sem_num = 0; /*序號*/ sbuf.sem_op = 1; /*V操作*/ sbuf.sem_flg = SEM_UNDO; if(semop(sem_id, &sbuf, 1) == -1) { perror("V operation Error"); return -1; } return 0; } int main() { key_t key; int shmid, semid, msqid; char *shm; struct msg_form msg; int flag = 1; /*while循環條件*/ // 獲取key值 if((key = ftok(".", 'z')) < 0) { perror("ftok error"); exit(1); } // 獲取共享內存 if((shmid = shmget(key, 1024, 0)) == -1) { perror("shmget error"); exit(1); } // 連接共享內存 shm = (char*)shmat(shmid, 0, 0); if((int)shm == -1) { perror("Attach Shared Memory Error"); exit(1); } // 創建消息隊列 if ((msqid = msgget(key, 0)) == -1) { perror("msgget error"); exit(1); } // 獲取信號量 if((semid = semget(key, 0, 0)) == -1) { perror("semget error"); exit(1); } // 寫數據 printf("***************************************\n"); printf("* IPC *\n"); printf("* Input r to send data to server. *\n"); printf("* Input q to quit. *\n"); printf("***************************************\n"); while(flag) { char c; printf("Please input command: "); scanf("%c", &c); switch(c) { case 'r': printf("Data to send: "); sem_p(semid); /*訪問資源*/ scanf("%s", shm); sem_v(semid); /*釋放資源*/ /*清空標准輸入緩沖區*/ while((c=getchar())!='\n' && c!=EOF); msg.mtype = 888; msg.mtext = 'r'; /*發送消息通知服務器讀數據*/ msgsnd(msqid, &msg, sizeof(msg.mtext), 0); break; case 'q': msg.mtype = 888; msg.mtext = 'q'; msgsnd(msqid, &msg, sizeof(msg.mtext), 0); flag = 0; break; default: printf("Wrong input!\n"); /*清空標准輸入緩沖區*/ while((c=getchar())!='\n' && c!=EOF); } } // 斷開連接 shmdt(shm); return 0; }
//share_mem_server #include<stdio.h> #include<stdlib.h> #include<sys/shm.h> // shared memory #include<sys/sem.h> // semaphore #include<sys/msg.h> // message queue #include<string.h> // memcpy // 消息隊列結構 struct msg_form { long mtype; char mtext; }; // 聯合體,用於semctl初始化 union semun { int val; /*for SETVAL*/ struct semid_ds *buf; unsigned short *array; }; // 初始化信號量 int init_sem(int sem_id, int value) { union semun tmp; tmp.val = value; if(semctl(sem_id, 0, SETVAL, tmp) == -1) { perror("Init Semaphore Error"); return -1; } return 0; } // P操作: // 若信號量值為1,獲取資源並將信號量值-1 // 若信號量值為0,進程掛起等待 int sem_p(int sem_id) { struct sembuf sbuf; sbuf.sem_num = 0; /*序號*/ sbuf.sem_op = -1; /*P操作*/ sbuf.sem_flg = SEM_UNDO; if(semop(sem_id, &sbuf, 1) == -1) { perror("P operation Error"); return -1; } return 0; } // V操作: // 釋放資源並將信號量值+1 // 如果有進程正在掛起等待,則喚醒它們 int sem_v(int sem_id) { struct sembuf sbuf; sbuf.sem_num = 0; /*序號*/ sbuf.sem_op = 1; /*V操作*/ sbuf.sem_flg = SEM_UNDO; if(semop(sem_id, &sbuf, 1) == -1) { perror("V operation Error"); return -1; } return 0; } // 刪除信號量集 int del_sem(int sem_id) { union semun tmp; if(semctl(sem_id, 0, IPC_RMID, tmp) == -1) { perror("Delete Semaphore Error"); return -1; } return 0; } // 創建一個信號量集 int creat_sem(key_t key) { int sem_id; if((sem_id = semget(key, 1, IPC_CREAT|0666)) == -1) { perror("semget error"); exit(-1); } init_sem(sem_id, 1); /*初值設為1資源未占用*/ return sem_id; } int main() { key_t key; int shmid, semid, msqid; char *shm; char data[] = "this is server"; struct shmid_ds buf1; /*用於刪除共享內存*/ struct msqid_ds buf2; /*用於刪除消息隊列*/ struct msg_form msg; /*消息隊列用於通知對方更新了共享內存*/ // 獲取key值 if((key = ftok(".", 'z')) < 0) { perror("ftok error"); exit(1); } // 創建共享內存 if((shmid = shmget(key, 1024, IPC_CREAT|0666)) == -1) { perror("Create Shared Memory Error"); exit(1); } // 連接共享內存 shm = (char*)shmat(shmid, 0, 0); if((int)shm == -1) { perror("Attach Shared Memory Error"); exit(1); } // 創建消息隊列 if ((msqid = msgget(key, IPC_CREAT|0777)) == -1) { perror("msgget error"); exit(1); } // 創建信號量 semid = creat_sem(key); // 讀數據 while(1) { msgrcv(msqid, &msg, 1, 888, 0); /*讀取類型為888的消息*/ if(msg.mtext == 'q') /*quit - 跳出循環*/ break; if(msg.mtext == 'r') /*read - 讀共享內存*/ { sem_p(semid); printf("%s\n",shm); sem_v(semid); } } // 斷開連接 shmdt(shm); /*刪除共享內存、消息隊列、信號量*/ shmctl(shmid, IPC_RMID, &buf1); msgctl(msqid, IPC_RMID, &buf2); del_sem(semid); return 0; }
7、網際套接字:
套接字是計算機網絡課程、java課程上見過面的老朋友了。套接字機制不但可以單機的不同進程通信,而且使得跨網機器間進程可以通信。
套接字的創建和使用與管道是有區別的,套接字明確地將客戶端與服務器區分開來,可以實現多個客戶端連到同一服務器。
(1)服務器套接字連接過程描述:
首先,服務器應用程序用socket創建一個套接字,它是系統分配服務器進程的類似文件描述符的資源。 接着,服務器調用bind給套接字命名。這個名字是一個標示符,它允許linux將進入的針對特定端口的連接轉到正確的服務器進程。 然后,系統調用listen函數開始接聽,等待客戶端連接。listen創建一個隊列並將其用於存放來自客戶端的進入連接。 當客戶端調用connect請求連接時,服務器調用accept接受客戶端連接,accept此時會創建一個新套接字,用於與這個客戶端進行通信。
(2)客戶端套接字連接過程描述:
客戶端首先調用socket創建一個未命名套接字,讓后將服務器的命名套接字作為地址來調用connect與服務器建立連接。
只要雙方連接建立成功,我們就可以像操作底層文件一樣來操作socket套接字實現通信。
//server.c #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <netinet/in.h> #include <arpa/inet.h> int main(void) { //create socket int fd = socket(AF_INET, SOCK_DGRAM, 0); if(fd==-1) { perror("socket\n"); exit(-1); } printf("socket fd=%d\n",fd); //build connection address struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(6666); addr.sin_addr.s_addr = inet_addr("127.0.0.1"); int r; r = bind(fd,(struct sockaddr*)&addr,sizeof(addr)); if(r==-1) { perror("bind"); close(fd); exit(-1); } printf("bind address successful!\n"); //accept or send message char buf[255]; struct sockaddr_in from; socklen_t len; len = sizeof(from); while(1) { r = recvfrom(fd,buf,sizeof(buf)-1,0,(struct sockaddr*)&from,&len); if(r>0) { buf[r]=0; printf("The message from %s is:%s\n",inet_ntoa(from.sin_addr),buf); } else { break; } } //close socket close(fd); return 0; }
//client.c #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> int main(void) { //create socket int fd = socket(AF_INET,SOCK_DGRAM,0); if(fd==-1) { perror("socket"); exit(-1); } printf("create socket OK!\n"); //create an send address struct sockaddr_in addr={}; addr.sin_family = AF_INET; addr.sin_port = htons(6666); addr.sin_addr.s_addr=inet_addr("127.0.0.1"); //send the message to the specify address int r; char buf[255]; while(1) { r = read(0,buf,sizeof(buf)-1); if(r<=0) break; sendto(fd,buf,r,0,(struct sockaddr*)&addr,sizeof(addr)); } //close socket close(fd); return 0; }
tz@COI HZAU
2018/4/24