写在前面:本博客为本人原创,严禁任何形式的转载!本博客只允许放在博客园(.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