实验三 使用Linux高级IPC 陈毅东
提纲 进程间通信概述 目标问题——哲学家进餐问题 Linux高级IPC机制 实现的其他问题 实习题 问题描述 错误与不好的解法 结束 提纲 进程间通信概述 目标问题——哲学家进餐问题 问题描述 错误与不好的解法 并行度较高的解法 Linux高级IPC机制 概述 System V信号灯 System V共享内存区 实现的其他问题 实习题
进程间通信概述(1):引子 输出结果是什么? #include <unistd.h> #include <sys/types.h> int result; main() { pid_t pid; result=0; pid=fork(); if(pid<0) exit(-1); else{ sleep(3); result=result+10; exit(0); } while(wait((int*)0)!=-1); printf("%d\n", result); exit(0); } if(pid){ pid=fork(); if(pid<0) exit(-1); if(pid==0){ sleep(3); result=result+20; exit(0); } } 输出结果是什么?
进程间通信概述(2) 进程是相互独立的,进程间的通信需要专门的机制。 进程之间的通信可以经由文件系统,但实际使用较为复杂(例如,需要锁机制)。 UNIX IPC (InterProcess Communication)机制是各种进程通信方式的统称。 Linux下的进程通信手段基本上是从Unix平台上的进程通信手段继承而来的。
进程间通信概述(3) 对于UNIX的发展,贝尔实验室和BSD在进程间通信方面的侧重点有所不同: 贝尔实验室对Unix早期的进程间通信手段进行了系统的改进和扩充,形成了“System V IPC”,通信进程局限在单个计算机内; BSD则主要考虑跨计算机的进程间通信,形成了基于套接口(socket)的进程间通信机制。
进程间通信概述(4) 最初的Unix IPC:信号、管道、FIFO; System V IPC:消息队列、信号量、共享内存区; 返回 进程间通信概述(4) 最初的 UNIX IPC System V IPC 基于Socket的IPC Linux IPC POSIX IPC 最初的Unix IPC:信号、管道、FIFO; System V IPC:消息队列、信号量、共享内存区; POSIX IPC:消息队列、信号量、共享内存区。
哲学家进餐问题的描述 五个哲学家围坐在一张圆桌周围,每个哲学家面前都有一碗米饭,相邻的两碗之间有一支筷子(如图)。 返回 哲学家进餐问题的描述 五个哲学家围坐在一张圆桌周围,每个哲学家面前都有一碗米饭,相邻的两碗之间有一支筷子(如图)。 哲学家的生活包含两种活动:即吃饭和思考。当一个哲学家觉得饿时,他就试图分两次去取他左边和右边的筷子,每次拿起一支,但不分次序。如果成功地获得了一双筷子,他就开始吃饭,吃完以后放下筷子继续思考。这样,问题就是,为每个哲学家写一段程序来描述其行为,要求不死锁。
错误与不好的解法(1) 解法一:可能进入“死锁”状态 若每个哲学家进程都运行到此句后发生进程切换,则进入死锁。 #define N 5 void philosopher(int i) { while(TRUE){ think(); take-chopstick(i); take-chopstick((i+1)%N); eat(); put-chopstick(i); put-chopstick((i+1)%N); } 若每个哲学家进程都运行到此句后发生进程切换,则进入死锁。
错误与不好的解法(2) 解法二:可能进入“饥饿”状态 这种解法可能会造成下面情况:哲学家们不断地重复“拿起各自左边的筷子又放下”的动作,谁也不能进餐。注意:这时和解法一的状态不同,这时进程都没有阻塞。 #define N 5 void philosopher(int i) { while(TRUE){ think(); do{ take-chopstick(i); if(can-take-chopstick((i+1)%N)) break; else put-chopstick(i); }while(TRUE); eat(); put-chopstick(i);put-chopstick((i+1)%N); } 不妨假设此函数能做到“测试且设置”。
错误与不好的解法(3) 解法三:可行但效率低下 返回 错误与不好的解法(3) 解法三:可行但效率低下 本解法从理论上可行,但从实际角来看,有一局限性:同一时刻只能有一位哲学家进餐。而这里有五支筷子,实际上应能允许两位哲学家同时进餐。 #define N 5 typedef int semaphore; semaphore mutex=1; void philosopher(int i) { while(TRUE){ think(); down(mutex); take-chopstick(i); take-chopstick((i+1)%N); eat(); put-chopstick(i); put-chopstick((i+1)%N); up(mutex); }
并行度较高的解法(1) void philosopher(int i) { while(TRUE){ think(); take-chopsticks(i); eat(); put-chopsticks(i); } } #define N 5 #define LEFT (i+N-1)%N #define RIGHT (i+1)%N #define THINKING 0 #define HUNGRY 1 #define EATING 2 typedef int semaphore; int state[N]; semaphore mutex=1; semaphore s[N];
并行度较高的解法(2) void put-chopsticks(int i) void take-chopsticks(int i) { { 返回 并行度较高的解法(2) void take-chopsticks(int i) { down(&mutex); state[i]=HUNGRY; test(i); up(&mutex); down(&s[i]); } void put-chopsticks(int i) { down(&mutex); state[i]=THINKING; test(LEFT); test(RIGHT); up(&mutex); } void test(i) { if(state[i]==HUNGRY &&state[LEFT]!=EATING&&state[RIGHT]!=EATING){ state[i]=EATING;up(&s[i]); }
概述 System V IPC包含了三种机制,在实现“哲学家进餐问题”时,我们只使用信号灯机制和共享存储区机制。主要的函数如下: 返回 概述 System V IPC包含了三种机制,在实现“哲学家进餐问题”时,我们只使用信号灯机制和共享存储区机制。主要的函数如下: 信号灯 共享内存区 头文件 <sys/sem.h> <sys/shm.h> 创建或打开IPC semget shmget 控制IPC操作 semctl shmctl IPC操作函数 semop shmat, shmdt System V IPC对象以key_t类型的值作为其名字。 System V IPC对象以一定的存取权限来控制其访问。
返回 System V IPC的名字 System V IPC是有名的,这样可以支持无亲缘关系的进程访问同一的IPC对象。其名字的类型为key_t,可以由ftok函数赋予或直接取值IPC_PRIVATE。 ftok函数 原型:#include <sys/types.h> #include <sys/ipc.h> key_t ftok(const char * pathname, int id); 功能:把已存在的路径名和一整数标识符转换成一个key_t值,称为IPC键。 返回值:成功时返回IPC键,出错返回-1。 说明:1、ftok产生的键值不会是IPC_PRIVATE; 2、不能保证ftok生成的键值唯一; 3、用于产生键的文件不能在该IPC对象存活其 内删除。
System V IPC对象的存取权限 为防止共享的IPC对象被非法访问,必须为IPC对象设置存取权限。 返回 System V IPC对象的存取权限 为防止共享的IPC对象被非法访问,必须为IPC对象设置存取权限。 System V IPC对象的存取权限和文件系统中文件的存取权限类似,也用9位分3组表示,三组分别代表属主、组成员和其他用户对该IPC对象的存取权限;每组中三位,只用其中的两位表示是否可读和是否可写。一般为了安全,在创建IPC对象时应该设置存取权限制为0600,表示仅仅对属主是可读,可写的。
返回 System V信号灯 semget semctl semop down和up的实现
semget(1)——函数说明 原型:#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semget(key_t key, int nsems, int oflag); 功能:创建或打开信号灯集合。 返回值:成功返回非负信号灯标识符,出错返回-1。 说明:1、key是欲创建或打开的信号灯集合的名字; 2、nsems指明信号灯集合中包含的信号灯数; 3、oflag是一个位信息标志,含两部分信息,即存 取权限和控制字段。低9位表示存取权限,控 制字段中IPC_CREAT位和IPC_EXCL位的设 置情况与参数key共同决定了本调用的操作。
返回 semget(2)——工作流程 if (key==IPC_PRIVATE) 创建新信号灯集并返回其id; else if (与key相关的信号灯集合存在) if ((oflag&IPC_CREAT)&&(oflag&IPC_EXCL)) 返回-1; else if (访问权限允许) 返回与key相关的信号灯集id; else 返回-1; else if (oflag&IPC_CREAT) 创建新信号灯集并返回其id; else 返回-1;
semctl(1)——函数说明 原型:#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semctl(int semid, int semnum, int cmd, union semun arg); 功能:对semid标识的信号灯集合进行控制。 返回值:成功返回非负值,出错返回-1。 说明:1、cmd是对信号灯集的控制命令,有GETVAL, SETVAL,GETALL,SETALL,IPC_RMID 等,我们只用最后的两个。 2、semnum标识该信号灯集中某成员(以0为第 一个),仅用于GETVAL,SETVAL等命令。 3、arg是可选的,取决于参数cmd,GETALL或 SETALL等命令就要用到参数arg。
semctl(2)——union semun 返回 semctl(2)——union semun union semun的定义为: union semun{ int val; //仅用于SETVAL命令 struct semid_ds * buf; //用于IPC_SET等命令 ushort * array; //用于SETALL等命令 } 注意,本联合未在出现在任何系统头文件中,因此必须由应用程序声明。
semop(1)——函数说明 原型:#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semop(int semid, struct sembuf * opsptr, size_t nops); 功能:对semid标识的信号灯集合中信号灯进行操作。 返回值:成功返回0,出错返回-1。 说明:1、opsptr是指向结构struct sembuf的指针,可 以是这种类型的结构数组的头指针。数组的每 个元素包含对信号灯集合中一个信号的的操作 的信息,从而可实现同时对多信号灯的操作。 2、nops指出opsptr指向的结构数组中元素数。
semop(2)——struct sembuf 返回 semop(2)——struct sembuf struct sembuf的定义为: struct sembuf{ short sem_num; //信号灯号:0,1,…,nsems-1 short sem_op; //信号灯操作:<0, 0, >0 short sem_flg; //操作标识:这里我们只要置0 } sem_op不同值对应的操作(设信号灯当前值为sv): sem-op<0: start: if (sv>=abs (sem_op)) sv=sv-abs(sem_op); else {sleep; goto start;} sem_op>0: sv=sv+sem_op; 唤醒所有阻塞于该信号量的进程; sem_op==0:测试sv是否为0。
down和up的实现 void down(int sid, int sn) void up(int sid, int sn) { { 返回 down和up的实现 void down(int sid, int sn) { struct sembuf op; op.sem_num=sn; op.sem_op=-1; op.sem_flg=0; semop(sid, &op, 1); } void up(int sid, int sn) { struct sembuf op; op.sem_num=sn; op.sem_op=1; op.sem_flg=0; semop(sid, &op, 1); }
返回 System V共享内存区 shmget shmctl shmat和shmdt 使用共享内存区的一般流程
返回 shmget 原型:#include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> int shmget(key_t key, size_t size, int oflag); 功能:创建或打开共享内存区。 返回值:成功返回非负内存区标识符,出错返回-1。 说明:1、size指明共享内存区大小(字节为单位); 2、key和oflag的说明基本和semget相同; 3、打开或创建一个共享内存区,并没提供调用进 程访问该内存区的手段。必须调用shmat。
返回 shmctl 原型:#include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> int shmctl(int shmid, int cmd, struct shmid_ds *buff); 功能:对shmid标识的共享内存区进行控制。 返回值:成功返回0,出错返回-1。 说明:1、参数cmd是对共享内存区的控制命令,可以是 IPC_RMID,IPC_SET或IPC_STAT,我们仅 用第一个; 2、buff主要用于命令IPC_SET和IPC_STAT,命 令为IPC_RMID时可直接提供NULL;
返回 shmat和shmdt 原型:#include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> void * shmat(int shmid, const void * shmaddr, int flag); int shmdt(const void * shmaddr); 功能:shmat用于将一个打开的共享内存区附接到调用进程的地址空间。shmdt用于切断这个内存区域。 返回值:出错返回-1,shmat成功返回映射区的起始地址而shmdt成功时候返回0。 说明:shmat中shmaddr和flag可用来影响映射区起始地址的选取。一般应该分别提供NULL和0。
使用共享内存区的一般流程 使用System V共享内存区时的一般流程: 用shmget创建或打开一个共享内存区 返回 使用共享内存区的一般流程 使用System V共享内存区时的一般流程: 用shmget创建或打开一个共享内存区 用shmat将打开的共享内存区附接到进程地址空间 对共享内存区进行操作 用shmdt切断共享内存区与本进程地址空间的联系 用shmctl拆除共享内存区(用命令IPC_RMID)
返回 实现的其他问题 如何创建多个进程 创建一个进程链 创建一个进程扇 创建一个进程树 程序的结束 程序失败后如何释放资源
创建一个进程链 1 2 3 注意:本程序未考虑fork失败的情况。 4 #define N 4 返回 创建一个进程链 1 2 3 4 #define N 4 #include <stdio.h> #include <sys/types.h> #include <unistd.h> main() { int i; for (i=1;i<N;++i) if (fork()) break; fprintf(stderr, "#%d is process %ld with parent %ld\n", i, (long)getpid(), (long)getppid()); } 注意:本程序未考虑fork失败的情况。
创建一个进程扇 1 2 3 4 注意:本程序未考虑fork失败的情况。 #define N 4 返回 创建一个进程扇 1 2 3 4 #define N 4 #include <stdio.h> #include <sys/types.h> #include <unistd.h> main() { int i; for (i=1;i<N;++i) if (fork()==0) break; fprintf(stderr, "#%d is process %ld with parent %ld\n", i, (long)getpid(), (long)getppid()); } 注意:本程序未考虑fork失败的情况。
创建一个进程树 2a 1 3a 3b 3c 3d 2b 注意:本程序未考虑fork失败的情况。 #define N 4 返回 创建一个进程树 2a 1 3a 3b 3c 3d 2b #define N 4 #include <stdio.h> #include <sys/types.h> #include <unistd.h> main() { int i, id=0; for (i=1;i<N;++i) if (fork()==0) id=i; fprintf(stderr, "#%d is process %ld with parent %ld\n", id, (long)getpid(), (long)getppid()); } 注意:本程序未考虑fork失败的情况。
程序的结束 在什么时机结束程序 程序结束时候要记得回收IPC资源 返回 程序的结束 在什么时机结束程序 以某个外部文件的存在与否来决定:每个哲学家在“吃”完后都检测某个特定的文件(例如:~/quit)是否已经被创建。若是则结束程序。 利用信号(signal)截取键盘Ctrl-C信号。 程序结束时候要记得回收IPC资源
程序失败后如何释放资源 杀掉垃圾进程 删除垃圾IPC资源 使用ps –ax查看所有进程 使用killall name进行删除 返回 程序失败后如何释放资源 杀掉垃圾进程 使用ps –ax查看所有进程 使用killall name进行删除 删除垃圾IPC资源 使用ipcs [-a]查看所有IPC资源 使用ipcrm [shm|sem] id进行删除
实习题 理解实现进程通信的难点 练习创建多个进程的方法 解决哲学家进餐问题 实现不好的或有错误的算法,观察运行情况 返回 实习题 理解实现进程通信的难点 练习创建多个进程的方法 解决哲学家进餐问题 实现不好的或有错误的算法,观察运行情况 实现正确的算法(需要检查) 也实现其他方法