〖Linux〗Linux高级编程 - 进程间通信(Interprocess Communication)
[转自: http://blog.csdn.net/Paradise_for_why/article/details/5550619]
这一章就是著名的IPC,这个东西实际的作用和它的名字一样普及。例如我们浏览网页,打印文章,等等。
IPC总共有五种类型:
-
共享内存(Shared Memory):最容易理解的一种,就像一个特工把情报放在特定地点(内存),另一个特工再过来取走一样。
-
内存映射(Mapped Memory):和共享内存几乎相同,除了特工们把地点从内存改成了文件系统。
-
管道(Pipes):从一个进程到另一个进程的有序通信,用电话来比喻再恰当不过了。
-
FIFOs:和管道和类似,唯一的区别是FIFOs比管道更神通一些,允许没有关系的进程之间的有序通信。
-
套接字(Sockets):为什么说浏览网页也是IPC?就是因为它。
5.1 共享内存(Shared Memory)
- 共享内存是最快捷的进程间通信方式。访问共享内存的效率和访问进程自己的非共享内存的效率是相同的,而且这种通信方式不需要任何额外的系统调用。
- 系统不会自动为共享内存处理同步问题,这个问题必须由用户自己解决。
- 共享内存的步骤通常是:
- 一个进程申请一块共享内存,即在它的页表中加入新的一项
- 所有进程Attach该共享内存,即从申请内存的进程中拷贝对应的页表
- 使用该内存进行通讯
- 结束后所有进程detach该共享内存
- 申请共享内存的进程在确定所有进程都detach后,释放该内存
- 由于共享内存是通过页表来实现的,我们可以得出一个结论:共享内存的大小是页面大小的整数倍,页面的大小可以通过getpagesize()来得到,通常在Linux下该值是4KB
- 相关的API函数:
- 申请共享内存:shmget,返回共享内存segment的id
- Attach,Detach函数:shmat,shmdt。需要共享内存segment的id
- 释放申请的内存:shmctl。一定要记得释放!调用exit和exec会自动detach,但不会自动释放。
- 使用 ipcs -m来观看当前系统存在的共享内存
例子:原程序链接,依据这个例子进行简单修改一下
1 /* 2 * ============================================================================= 3 * 4 * Filename: sharememory_read.c 5 * 6 * Description: 7 * 8 * Version: 1.0 9 * Created: 2014年11月04日 19时52分28秒 10 * Revision: none 11 * Compiler: gcc 12 * 13 * Author: lwq (28120), scue@vip.qq.com 14 * Organization: 15 * 16 * ============================================================================= 17 */ 18 #include <stdlib.h> 19 20 /********************************************************** 21 *实验要求: 创建两个进程,通过共享内存进行通讯。 22 *功能描述: 本程序申请和分配共享内存,然后轮训并读取共享中的数据,直至 23 * 读到“end”。 24 *日 期: 2010-9-17 25 *作 者: 国嵌 26 **********************************************************/ 27 #include <unistd.h> 28 #include <stdlib.h> 29 #include <stdio.h> 30 #include <string.h> 31 #include <sys/types.h> 32 #include <sys/ipc.h> 33 #include <sys/shm.h> 34 #include <getopt.h> 35 #include "sharememory.h" 36 37 void read_shm(struct shared_use_st *shared_stuff); 38 void write_shm(struct shared_use_st *shared_stuff); 39 void del_shm(); 40 41 void usage(){ 42 fprintf(stderr, "\nusage: %s -r|-w\n\n" 43 "-r read mode\n" 44 "-w write mode\n" 45 "\n", "shared_memory"); 46 exit(1); 47 } 48 49 #define READ (1) 50 #define WRITE (2) 51 #define OPTNONE (0) 52 53 // 全局变量 54 void *shared_memory=(void *)0; 55 56 /* 57 * 程序入口 58 * */ 59 int main(int argc, char **argv) 60 { 61 int running=RUNNING; 62 struct shared_use_st *shared_stuff; 63 int shmid; 64 int operation=OPTNONE; /* 读/写操作 */ 65 66 /*----------------------------------------------------------------------------- 67 * getopt start 68 *----------------------------------------------------------------------------*/ 69 int choice; 70 while (1) 71 { 72 static struct option long_options[] = 73 { 74 /* Use flags like so: 75 {"verbose", no_argument, &verbose_flag, 'V'}*/ 76 /* Argument styles: no_argument, required_argument, optional_argument */ 77 {"version", no_argument, 0, 'v'}, 78 {"help", no_argument, 0, 'h'}, 79 {"read", no_argument, 0, 'r'}, 80 {"write", no_argument, 0, 'w'}, 81 {0,0,0,0} 82 }; 83 84 int option_index = 0; 85 86 /* Argument parameters: 87 no_argument: " " 88 required_argument: ":" 89 optional_argument: "::" */ 90 91 choice = getopt_long( argc, argv, "vhrw", 92 long_options, &option_index); 93 94 if (choice == -1) 95 break; 96 97 switch( choice ) 98 { 99 case 'v': 100 101 break; 102 103 case 'h': 104 usage(); 105 break; 106 107 case 'r': 108 operation=READ; 109 break; 110 111 case 'w': 112 operation=WRITE; 113 break; 114 115 case '?': 116 /* getopt_long will have already printed an error */ 117 usage(); 118 break; 119 120 default: 121 /* Not sure how to get here... */ 122 return EXIT_FAILURE; 123 } 124 } 125 if (operation == OPTNONE) { 126 usage(); 127 } 128 /*----------------------------------------------------------------------------- 129 * getopt end 130 *----------------------------------------------------------------------------*/ 131 132 /*创建共享内存*/ 133 shmid=shmget((key_t)1234,sizeof(struct shared_use_st),0666|IPC_CREAT); 134 if(shmid==-1) { 135 fprintf(stderr,"shmget failed\n"); 136 exit(EXIT_FAILURE); 137 } 138 139 /*映射共享内存*/ 140 shared_memory=shmat(shmid,(void *)0,0); 141 if(shared_memory==(void *)-1) { 142 fprintf(stderr,"shmat failed\n"); 143 exit(EXIT_FAILURE); 144 } 145 146 printf("Memory attached at 0%08x\n",(int)((intptr_t)shared_memory)); 147 148 /*让结构体指针指向这块共享内存*/ 149 shared_stuff=(struct shared_use_st *)shared_memory; 150 151 /*控制读写顺序*/ 152 // lwq: 使之能读取上一条消息 153 if (operation == READ && shared_stuff->written_by_you != HADWROTE) 154 shared_stuff->written_by_you=HADREAD; 155 156 switch(operation) { 157 case READ: 158 read_shm(shared_stuff); 159 break; 160 161 case WRITE: 162 write_shm(shared_stuff); 163 break; 164 165 default: 166 usage(); 167 break; 168 } 169 170 del_shm(); 171 exit(EXIT_SUCCESS); 172 } 173 174 // 读取共享内存 175 void read_shm(struct shared_use_st *shared_stuff){ 176 while(1) { 177 if(shared_stuff->written_by_you == HADWROTE) { 178 printf("You wrote:%s",shared_stuff->some_text); 179 shared_stuff->written_by_you=HADREAD; 180 if(strncmp(shared_stuff->some_text,"end",3)==0) { 181 break; 182 } 183 } 184 else { 185 usleep(100000); 186 } 187 } 188 } 189 190 // 写入共享内存 191 void write_shm(struct shared_use_st *shared_stuff){ 192 char buffer[BUFSIZ] = {0}; 193 while(1) { 194 while(shared_stuff->written_by_you!=HADREAD); /* 等待读写完成 */ 195 printf("Enter some text:"); 196 fgets(buffer,BUFSIZ,stdin); 197 strncpy(shared_stuff->some_text,buffer,TEXT_SZ); /* 复制进去 */ 198 shared_stuff->written_by_you=HADWROTE; 199 if(strncmp(buffer,"end",3)==0) { 200 break; 201 } 202 } 203 } 204 205 // 删除共享内存 206 void del_shm(){ 207 /*删除共享内存*/ 208 if(shmdt(shared_memory)==-1) { 209 fprintf(stderr,"\nshmdt failed\n"); 210 exit(EXIT_FAILURE); 211 } 212 else { 213 fprintf(stderr, "\ndelete shared_memory: 0x%08x\n", (int)((intptr_t)shared_memory)); 214 } 215 }
编译:gcc sharememory.c -o sharememory
执行:
1. 以读取模式打开程序(进程1): ./sharememory -r
2. 以写入模式打开程序(进程2): ./sharememory -w
更新介绍:http://www.cs.cf.ac.uk/Dave/C/node27.html
5.2 进程信号量
- 信号量(Semaphore)的概念前面已经介绍过了。Linux对用来同步进程的信号量采取了一种特别的实现方式。这些信号量也就被称为进程信号量(Process Semaphore)。(这一节下面所提到的所有信号量默认都是指进程信号量)
- 相关的API函数:
- 申请:semget
- 释放:semctl。需要注意的是信号量不会被自动释放,我们必须显式释放它。
- Wait和Post:semop
- 使用ipcs -s来观看当前系统存在的信号量
例子:原程序链接
1 /* 2 * ============================================================================= 3 * 4 * Filename: semaphore_simple.c 5 * 6 * Description: 7 * 8 * Version: 1.0 9 * Created: 2014年11月04日 21时21分25秒 10 * Revision: none 11 * Compiler: gcc 12 * 13 * Author: lwq (28120), scue@vip.qq.com 14 * Organization: 15 * 16 * ============================================================================= 17 */ 18 #include <stdlib.h> 19 20 #include <unistd.h> 21 #include <sys/types.h> 22 #include <sys/stat.h> 23 #include <fcntl.h> 24 #include <stdlib.h> 25 #include <stdio.h> 26 #include <string.h> 27 #include <sys/sem.h> 28 29 union semun 30 { 31 int val; 32 struct semid_ds *buf; 33 unsigned short *arry; 34 }; 35 36 static int sem_id = 0; 37 38 static int set_semvalue(); 39 static void del_semvalue(); 40 static int semaphore_p(); 41 static int semaphore_v(); 42 43 int main(int argc, char *argv[]) 44 { 45 char msg[1024] = {0}; 46 int i = 0; 47 int mypid=getpid(); 48 49 //创建信号量 50 sem_id = semget((key_t)12345, 1, 0666 | IPC_CREAT); 51 52 if(argc > 1) 53 { 54 //程序第一次被调用,初始化信号量 55 if(!set_semvalue()) { 56 fprintf(stderr, "Failed to initialize semaphore\n"); 57 exit(EXIT_FAILURE); 58 } 59 } 60 for(i = 0; i < 10; ++i) { 61 //进入临界区 62 if(!semaphore_p()) 63 exit(EXIT_FAILURE); 64 //向屏幕中输出数据 65 snprintf(msg, sizeof(msg)-1, "%s, index: %03d, pid: %05d", argv[1], i, mypid); 66 printf("%s\n", msg); 67 fflush(stdout); 68 usleep(100000); 69 //离开临界区,休眠随机时间后继续循环 70 if(!semaphore_v()) 71 exit(EXIT_FAILURE); 72 } 73 74 if(argc > 1) 75 { 76 //如果程序是第一次被调用,则在退出前删除信号量 77 sleep(3); 78 del_semvalue(); 79 } 80 exit(EXIT_SUCCESS); 81 } 82 83 static int set_semvalue() 84 { 85 //用于初始化信号量,在使用信号量前必须这样做 86 union semun sem_union; 87 88 sem_union.val = 1; 89 if(semctl(sem_id, 0, SETVAL, sem_union) == -1) 90 return 0; 91 return 1; 92 } 93 94 static void del_semvalue() 95 { 96 //删除信号量 97 union semun sem_union; 98 99 if(semctl(sem_id, 0, IPC_RMID, sem_union) == -1) 100 fprintf(stderr, "Failed to delete semaphore\n"); 101 } 102 103 static int semaphore_p() 104 { 105 //对信号量做减1操作,即等待P(sv) 106 struct sembuf sem_b; 107 sem_b.sem_num = 0; 108 sem_b.sem_op = -1;//P() 109 sem_b.sem_flg = SEM_UNDO; 110 if(semop(sem_id, &sem_b, 1) == -1) 111 { 112 fprintf(stderr, "semaphore_p failed\n"); 113 return 0; 114 } 115 return 1; 116 } 117 118 static int semaphore_v() 119 { 120 //这是一个释放操作,它使信号量变为可用,即发送信号V(sv) 121 struct sembuf sem_b; 122 sem_b.sem_num = 0; 123 sem_b.sem_op = 1;//V() 124 sem_b.sem_flg = SEM_UNDO; 125 if(semop(sem_id, &sem_b, 1) == -1) 126 { 127 fprintf(stderr, "semaphore_v failed\n"); 128 return 0; 129 } 130 return 1; 131 }
编译:gcc semaphore_simple.c -o semaphore_simple
执行:./semaphore_simple XXX &; ./semaphore_simple YYY
5.3 内存映射
- 内存映射使得不同的进程可以通过一个共享文件来互相通信。
- 相关的API函数:
- 映射:mmap
- 同步:msync。用来指定对文件的修改是否被buffer。
- 释放:munmap。在程序结束的时候会自动unmap
- mmap的其他用法:
- 可以替代read和write,有时使用内存映射后的效率比单纯使用I/O操作来的更快
- 在内存映射文件中构建structure,修改structure再次将文件映射到内存中可以快速的将structure恢复到原来的状态
- 把/dev/zero文件映射到内存中。该文件可以提供无限的0,并且写到该文件的所有内容将被直接丢弃
5.4 管道(Pipes)
- 管道是单向的,即一个线程写,另一个线程读,无法互换
- 如果写的速度太快,造成管道满了,那么写的线程就会被block;如果读的速度太快,造成管道空了,那么读的进程就会被block。因此事实上我们可以说管道自动实现了同步机制
- 我们可以通过调用pipe函数来生成一对pipe file description。(为什么是一对?因为一个读一个写)。可是,生成的pipe file description无法传送给不相关的进程(因为做为file descriptor即使它拿到了也没法用)。但是我们注意到fork之后父进程所有的file descriptor在子进程中依然有效,因此管道最大的作用是在父子进程之间通信。或者更确切的说,是在有共同祖先的进程之间通信。
- 典型的创建管道的流程如下:
- 用pipe生成2个pipe file description(简称fds)。然后调用fork
- 在父进程关闭fds[0](或fds[1]),并以只读(或只写)方式打开fds[1](或fds[0])。在子进程中关闭fds[1](或fds[0]),并以只写(或只读)方式打开fds[0](或fds[1])。打开的函数是fdopen。
- 开始通信。结束后用close函数关闭剩下的fds。
- 这里有一个技巧:可以利用管道来达成重定向stdin, stdout和stderr。注意到dup2这个API可以把一个file descriptor复制到另一个上。
- 事实上,我们有一对更为简洁的函数popen/pclose来完成上面的一系列复杂的操作。popen有两个参数:
- 第一个参数接受一个exec,子进程将执行这个exec
- 第二个参数为”w”或者”r”,”w”表示父进程写子进程读,”r”则反之
- 返回值为管道的一端,也就是一个file descriptor
- pclose用来关闭popen返回的file descriptor
- FIFO(First In First Out)文件事实上是一个有名字的管道,换句话说,他可以用来让“不相干”的程序互相通信。
- 我们使用mkfifo函数来创建一个FIFO文件
- 我们可以使用任何的低级I/O函数(open, write, read, close等)以及C库I/O函数(fopen, fprintf, fscanf, fclose等)来操作FIFO文件。
- Linux的管道和Windows下的命名管道(Named Pipes)的区别
- Windows的命名管道更像一个套接字(sockets),它可以通过网络让不同主机上的程序进行通信
- Linux的管道允许有多个reader和writer,每个reader和writer进行读/写的最大容量为 PIPE_BUF(4KB),如果有多个writer同时写,他们写的东西会被分为一个一个的chunk(每个4KB)并允许交错写。(例如进程A有两个 chunk,A1,A2。进程B也有两个chunk,B1,B2。A和B同时写,则顺序可能为A1,B1,A2,B2) Windows的管道允许在同一个管道上有多个reader/writer对,他们之间读写的数据没有交叉。
5.5 套接字(Sockets)
- 套接字的特点:
- 它是双向通信的
- 它是进程间通信的,包括其他机器上的进程
- 套接字有三个参数:
- 通讯类型(communication style)
- 连接(connection)类型:保证所有的包按发送的顺序到达接受方。(类似于电话)如果包丢失或者抵达顺序错误,会自动重发。
- datagram类型:所有包单独发送,可能会出现丢失或者晚发早到的现象。(类似于邮寄)
- 命名空间(namespace):描述套接字的地址是如何表示的,例如本地就是文件名,internet上就是ip地址。
- 协议(protocol):通讯协议,常用的有TCI/IP,AppleTalk等。
- 通讯类型(communication style)
- 相关的API(套接字也是通过file descriptor来表示的):
- socket:创建一个socket
- closes:销毁一个socket
- connect:在两个socket之间创建一个连接。这个API通常由客户端调用。
- bind:给服务器的一个套接字绑定一个地址,服务器端调用。
- listen:让一个套接字开始侦听,准备接受请求,服务器端调用。
- accept:接受一个连接请求,并且为该连接创建一个新的套接字,服务器端调用。
- 服务器端的生命流程:
- 创建一个connection类型的socket
- 给该socket绑定一个地址
- 调用listen来enable该socket(listen可以指定最多有多少个请求在等待队列中,如果等待队列满了,又有新的请求到达的时候,则该请求被拒绝)
- 对于收到的连接请求调用accept来接受
- 关闭socket
- 本地socket(local socket)
- 如果是同一台电脑上的两个进程需要通信的话,可以使用本地socket。这种情况下socket的地址是文件路径。注意进程必须对该路径拥有可写权限,否则无法建立连接
- 完成之后使用unlink来关闭一个socket