实验四 进程间通信
一、实验目的
1. 掌握利用管道机制实现进程间的通信的方法
2. 掌握利用消息缓冲队列机制实现进程间的通信的方法 3. 掌握利用共享存储区机制实现进程间的通信的方法 4. 了解Linux系统中进程软中断通信的基本原理
二、实验学时
2学时
三、实验内容
1.掌握实现进程间通信的系统调用的功能和方法
进程通信,是指进程之间交换信息。从这个意义上讲,进程之间的同步、互斥也是一种信息交换,也是一种通信。但是,这里所说的“通信”是指进程之间交换较多的信息这样一种情况,特别是在由数据相关和有合作关系的进程之间,这种信息交换是十分必要和数量较大的。
进程间通信是协调解决多个进程之间的约束关系,实现进程共同进展的关键技术,是多道系统中控制进程并发执行必不可少的机制。 (1)进程的通信方式:
a. 直接通信是指信息直接传递给接收方,如管道。在发送时,指定接收方的地址或标识,也可以指定多个接收方或广播式地址, send(Receiver, message)。在接收时,允许接收来自任意发送方的消息,并在读出消息的同时获取发送方的地址, receive(Sender,message)。
b. 间接通信:借助于收发双方进程之外的共享数据结构作为通信中转,如消息队列。这种数据结构称为缓冲区或信箱。通常收方和发方的数目可以是任意的。 (2)进程间通信的类型:
a. 共享存储器系统:基于共享数据结构的通信方式:只能传递状态和整数值(控制信息),包括进程互斥和同步所采用的信号量机制。速度快,但传送信息量小,编程复杂,属于低级通信;基于共享存储区的通信方式:能够传送任意数量的数据,属于高级通信。
b. 消息传递系统:在消息传递系统中,进程间的数据交换以消息为单位,用户直接利用系统提供的一组通信命令(原语)来实现通信。 c. 管道通信:管道是一条在进程间以字节流方式传送的通信通道。它由OS 核心的缓冲区(通 常几十KB)来实现,是单向的;在实质上,是一个有OS 维护的特殊共享文件,常用于命 令行所指定的输入输出重定向和管道命令。在使用管道前要建立相应的管道,然后才可使用。 (3)Linux 管道
a. 无名管道:一个可以在文件系统中长期存在的、具有路径名的文件。用系统调用mknod( )建立。它克服无名管道使用上的局限性,可让更多的进程也能利用管道进行通信。因而其它进程可以知道它的存在,并能利用路径名来访问该文件。对有名管道的访问方式与访问其他文件一样,需先用open( )打开。通过pipe 系统调用可以创建无名管道,得到两个文件描述符,分别用于写和读。
1
– int pipe(int fd[2]); //头文件unistd.h,返回值:0(成功),-1(发生错误)
– 文件描述符 fd[0]为读端,fd[1]为写端
– 通过系统调用 write 和read 进行管道的写和读
read(fd,buf,nbyte)从fd所指示的文件中读出nbyte个字节的数据,并将它们送至由指针buf所指示的缓冲区中。如该文件被加锁,等待,直到锁打开为止。write(fd,buf,nbyte)把nbyte 个字节的数据,从buf所指向的缓冲区写到由fd所指向的文件中。如文件加锁,暂停写入,直至开锁。 参数定义:int read(fd,buf,nbyte); /* write(fd,buf,nbyte)参数定义同read*/
int fd; char *buf; unsigned nbyte;
– 进程间双向通信,通常需要两个管道
– 只适用于父子进程之间或父进程安排的各个子进程之间
b. Linux 中的有名管道,可通过mknod 系统调用建立:指定mode 为S_IFIFO,或调用C 库函数mkfifo 产生
– int mknod(const char *path, mode_t mode, dev_t dev) – int mkfifo(const chat *path, mode_t mode) (4)消息缓冲机制
消息(message) 与窗口系统中的“消息”不同。通常是不定长数据块。消息的发送不需要接收方准备好,随时可发送。相应的数据结构: type message buffer = record
sender size text next end
Linux 消息:
消息队列(message queue):每个message 不定长,由类型(type)和正文(text)组成 Linux 消息队列API:头文件sys/types.h, sys/ipc.h, sys/msg.h
–msgget 依据用户给出的整数值key,创建新消息队列或打开现有消息队列,返回一个消息
队列 ID;
–msgsnd 发送消息;
–msgrcv 接收消息,可以指定消息类型;没有消息时,返回-1; –msgctl 对消息队列进行控制,如删除消息队列;
通过指定多种消息类型,可以在一个消息队列中建立多个虚拟信道
注意:消息队列不随创建它的进程的终止而自动撤销,必须用 msgctl(msgqid, IPC_RMID, 0)。另外,msgget 获得消息队列ID 之后,fork 创建子进程,在子进程中能继承该消息队列ID 而不必再一次msgget。
int msgget(key_t key, int flag);
语法格式:msgqid=msgget(key, flag)
功能:获得一个消息的描述符,该描述符指定一个消息对流以便于其他系统调用。 其中,key 由系统规定类型,sys/type.h。flag 本身由操作允许权和控制命令值相 “或”得到,如:IPC_CREAT|0400 表示是否该队列应被创建,IPC_EXCL|0400 表示该队列的创建应是互斥的。msgqid是该系统调用返回的描述符,失败则返回-1。
2
int msgsnd(int id, struct msgbuf *msgp, int size, int flag);
功能:发送一消息。其中,id 为返回消息队列的描述符。msgp 指向用户存储区的一个构造体指针。size 指示由msgp 指向的数据结构中字符数组的长度,即消息长度,该数组的最大值由MSGMAX 字体可调用参数来确定。flag 规定当核心用尽内部缓冲空间时应执行的动作:若在flag中未设置IPC_NOWAIT,则当该消息队列中的字节数超过一最大值时,或系统范围的消息数超过某一最大值时,调用msgsnd 进程阻塞;若设置IPC_NOWAIT,则在此情况下,msgsnd 直接返回。
int msgrcv(int id, struct msgbuf *msgp, int size, int type, int flag);
语法格式:count=msgrcv(id, msgp, size, type, flag) 功能:接收一消息,count 返回消息正文的字节数。其中,struct msgbuf{long mtyoe; char mtext[ ];};id 消息队列的描述符。msgp 用来存放欲接收消息的用户数据结构的地址。size 指示msgp 中数据数组的大小。
type 为0 接收该队列的第一个消息;为正,接收类型为type 的第一个消息;为负,接收小于或等于type 绝对值的最低类型的第一个消息。
flag 规定倘若该队列无消息,核心应当做什么事。若设置IPC_NOWAIT,则立即返回;若在flag中设置MSG_NOERROR,且所接收的消息大学大于size,核心截断所接收的消息。
int msgctl(int id, int cmd, struct msgid_ds *buf);
功能:查询一个消息描述符的状态,设置它的状态及删除一个消息描述符。其中,id 用来识别该消息的描述符。
cmd 规定命令的类型:IPC_STAT 将与id 相关联的消息队列首标读入buf; IPC_SET 为这个消息队列设置有效的用户和小组表示及操作允许权和字节的数量;IPC_RMID 删除id 的消息队列。
buf 是含有控制参数或查询结果的用户数据结构的地址。
struct msgid_ds{
struct ipc_perm msgperm; //许可权结构 short pad1[7]; //由系统使用 ushort onsg_qnum; //队列上消息 ushort msg_qbytes; //队列上最大字节数 ushort msg_lspid; //最后发送消息的PID ushort msg_lrpid; //最后接收消息的PID time_t msg_stime; //最后发送消息的时间 time_t msg_rtime; //最后接收消息的时间 time_t msg_ctime; //最后更改消息的时间
}
struct ipc_perm{ ushort uid; //当前用户id ushort uid; //当前进程组id ushort cuid; //创建用户id ushort cgid; //创建进程组id ushort mode; //存取许可权 {short pad1; long pad2;} //由系统使用 }
(5)共享存储区(shared memory)
相当于内存,可以任意读写和使用任意数据结构(当然,对指针要注意),需要进程互斥和同步的辅助来确保数据一致性。
Linux 的共享存储区:
– 创建或打开共享存储区(shmget):依据用户给出的整数值key,创建新区或打开现有区,返回一个共享存储区ID。
– 连接共享存储区(shmat):连接共享存储区到本进程的地址空间,可以指定虚拟地址或由系统分配,返回共享存储区首地址。父进程已连接的共享存储区可被fork 创建的子进程继承。
– 拆除共享存储区连接(shmdt):拆除共享存储区与本进程地址空间的连接。
– 共享存储区控制(shmctl):对共享存储区进行控制。如:共享存储区的删除需要显式
3
调用shmctl(shmid, IPC_RMID, 0);
– 头文件:sys/types.h, /sys/ipc.h, sys/shm.h
int shmget(key_t key, int size, int flag);
语法格式:shmid=shmget(key, size, flag)
功能:创建一个关键字为key,长度为size 的共享存储区。其中,size 为存储区的字节数。key、flag 与系统调用msgget 相同。
int shmat(int id, char *addr, int flag);
语法格式:virtaddr=shmat(id, addr, flag)
功能:从逻辑上将一个共享存储区附接到进程的虚拟地址空间上。
id 为共享存储区的标识符。addr 用户要使用共享存储区附接的虚地址,若为0,系统选择一个适当的地址来附接该共享区。
flag 规定对此区的读写权限,以及系统是否应对用户规定的地址做舍入操作:如果flag 中设置了shm_rnd 即表示操作系统在必要时舍去这个地址;如果设置了shm_rdonly,表示只允许读操作。
viraddr 是附接的虚地址。
int shmdt(char *addr);
功能:把一个共享存储区从指定进程的虚地址空间断开,当调用成功,返回0 值;不成功,返回-1。addr 为系统调用shmat 所返回的地址。
int shmctl(int id, int cmd, struct shmid_ds *buf);
功能:对与共享存储区关联的各种参数进行操作,从而对共享存储区进行控制。调用成功返回0,否则-1。其中,id 为被共享存储区的描述符。
cmd 规定命令的类型:IPC_STAT 返回包含在指定的shmid 相关数据结构中的状态信息,并且把它放置在用户存储区中的*buf 指针所指的数据结构中。执行此命令的进程必须有读取允许权;IPC_SET 对于指定的shmid,为它设置有效用户和小组标识和操作存取权;IPC_RMID 删除指定的shmid 以及与它相关的共享存储区的数据结构;SHM_LOCK 在内存中锁定指定的共享存储区,必须是超级用户才可以进行此项操作。
buf 是一个用户级数据结构地址。
2. 编写程序实现进程的管道通信。
参考程序4.1
#include { int fd[2]; char outpipe[100],inpipe[100]; pipe(fd); /*创建一个管道*/ while ((pid1=fork( ))==-1); if(pid1==0) { lockf(fd[1],1,0); sprintf(outpipe,\"child 1 process is 4 sending message!\");/*把串放入数组outpipe中*/ write(fd[1],outpipe,50); sleep(5); lockf(fd[1],0,0); exit(0); } else { while((pid2=fork( ))==-1); if(pid2==0) { lockf(fd[1],1,0); sprintf(outpipe,\"child 2 process is sending message!\"); write(fd[1],outpipe,50); sleep(5); printf(\"%s\\n\lockf(fd[1],0,0); wait(0); exit(0); read(fd[0],inpipe,50); } printf(\"%s\\n\else exit(0);} {wait(0); } } read(fd[0],inpipe,50); /*从管道中读长为 50字节的串*/ 阅读并运行程序并回答以下问题: 问题1:该程序中使用的管道是有名管道还是无名管道?程序中红色部分的含义是什么? 问题2:程序的含义是什么?运行结果是什么?为什么? 3.编写两个程序client.c和server.c,分别用于消息的发送和接收。 { 参考程序4.2 server(); server.c参考程序如下: #include int i; ); /*创建75#消息队列*/ do msgqid=msgget(MSGKEY,0777); { /*打开75#消息队列*/ msgrcv(msgqid,&msg,1030,0,0); for(i=10;i>=1;i--) { /*接收消息*/ printf(\"(server)received\\n\"); msg.mtype=i; }while(msg.mtype!=1); printf(\"(client)sent\\n\"); msgctl(msgqid,IPC_RMID,0); /* msgsnd(msgqid,&msg,1024,0); 删除消息队列,归还资源*/ /*发送消息*/ exit(0); } } exit(0); main() } 5 main() client(); { } 建议该题目的运行方法为: 将上述两个程序分别编译为server和client,并按以下方式执行: ./server & /*当在前台运行某个作业时,终端被该作业占据;而在后台运行作业时,它不会占据终端。可以使用&命令把作业放到后台执行。该命令的一般形式为:命令&*/ ipcs –q /* 输出有关信息队列(message queue)的信息*/ ./client 阅读并运行程序并回答以下问题: 问题1:运行结果是什么?该程序为什么需要在后台运行server.c?若不如此会出现什么现象?为什么? 问题2:两个程序的含义是什么?请解释其运行结果的含义? 4.使用共享存储区来实现两个进程之间的通信。 { 参考程序4.3 #include }while(*addr); shmid=shmget(SHMKEY,1024,0777);/* 打 开共享存储区*/ shmctl(shmid,IPC_RMID,0); /*撤消共享存 addr= (int *)shmat(shmid,0,0); /*获取共享储区,归还资源*/ exit(0); 存储区的首地址*/ for (i=29;i>=0;i--) } { main() { while (*addr!=-1);/*判断是否可写*/ *addr=i;/*写数据*/ while ((p1=fork())==-1);/*父进程*/ printf(\"%d\ if (p1==0) SERVER();/*子进程p1*/ printf(\"(client)sent\\n\"); while ((p2=fork())==-1); /*父进程*/ } if (p2==0) CLIENT();/*子进程p2*/ exit(0); wait(0); /*父进程*/ } wait(0); /*父进程*/ } void SERVER()/*接收进程*/ 阅读并运行程序并回答以下问题: 问题1:运行结果是什么? 问题2:两个程序的含义是什么?请解释其运行结果的含义? 6 5*.编制一段程序,使其实现进程的软中断通信。 要求:使用系统调用fork()创建两个子进程,再用系统调用signal()让父进程捕捉键盘上来的中断信号(即按Del键);当捕捉到中断信号后,父进程用系统调用kill()向两个子进程发出信号,子进程捕捉到信号后分别输出下列信息后终止: Child process 1 is killed by parent! Child process 2 is killed by parent! 父进程等待两个子进程终止后,输出如下的信息后终止: Parent process is killed! 参考资料 1 .信号的基本概念 每个信号都对应一个正整数常量 ( 称为 signal number, 即信号编号。定义在系统头文件 信号与中断的相似点: ( 1 )采用了相同的异步通信方式; ( 2 )当检测出有信号或中断请求时,都暂停正在执行的程序而转去执行相应的处理程序; ( 3 )都在处理完毕后返回到原来的断点; ( 4 )对信号或中断都可进行屏蔽。 信号与中断的区别: ( 1 )中断有优先级,而信号没有优先级,所有的信号都是平等的; ( 2 )信号处理程序是在用户态下运行的,而中断处理程序是在核心态下运行; ( 3 )中断响应是及时的,而信号响应通常都有较大的时间延迟。 信号机制具有以下三方面的功能: ( 1 )发送信号。发送信号的程序用系统调用 kill( ) 实现; ( 2 )预置对信号的处理方式。接收信号的程序用 signal( ) 来实现对处理方式的预置; ( 3 )收受信号的进程按事先的规定完成对相应事件的处理。 2.信号的发送 信号的发送,是指由发送进程把信号送到指定进程的信号域的某一位上。如果目标进程正在一个可被中断的优先级上睡眠,核心便将它唤醒,发送进程就此结束。一个进程可能在其信号域中有多个位被置位,代表有多种类型的信号到达,但对于一类信号,进程却只能记住其中的某一个。 进程用 kill( ) 向一个进程或一组进程发送一个信号。 3 .对信号的处理 当一个进程要进入或退出一个低优先级睡眠状态时,或一个进程即将从核心态返回用户态时,核心都要检查该进程是否已收到软中断。当进程处于核心态时,即使收到软中断也不予理睬;只有当它返回到用户态后,才处理软中断信号。对软中断信号的处理分三种情况进行: ( 1 )如果进程收到的软中断是一个已决定要忽略的信号( function=1 ),进程不做任何处理便立即返回; 7 ( 2 )进程收到软中断后便退出( function=0 ); ( 3 )执行用户设置的软中断处理程序。 4. 所涉及的中断调用 1) kill( ) 系统调用格式 int kill(pid,sig) 参数定义 int pid,sig; 其中, pid 是一个或一组进程的标识符,参数 sig 是要发送的软中断信号。 ( 1 ) pid>0 时,核心将信号发送给进程 pid 。 ( 2 ) pid=0 时,核心将信号发送给与发送进程同组的所有进程。 ( 3 ) pid=-1 时,核心将信号发送给所有用户标识符真正等于发送进程的有效用户标识号的进程。 2 ) signal( ) 预置对信号的处理方式,允许调用进程控制软中断信号。 系统调用格式 signal(sig,function) 头文件为 #include signal(sig,function) int sig; void (*func) ( ) 其中 sig 用于指定信号的类型, sig 为 0 则表示没有收到任何信号,余者如下表: 值 名 字 01 SIGHUP 02 SIGINT 03 SIGQUIT 04 SIGILL 05 SIGTRAP 06 SIGIOT 07 SIGEMT 08 SIGFPE 09 SIGKILL 10 SIGBUS 11 SIGSEGV 12 SIGSYS 13 SIGPIPE 14 SIGALRM 15 SIGTERM 16 SIGUSR1 17 SIGUSR2 说 明 挂起( hangup ) 中断,当用户从键盘按 ^c 键或 ^break 键时 退出,当用户从键盘按 quit 键时 非法指令 跟踪陷阱( trace trap ),启动进程,跟踪代码的执行 IOT 指令 EMT 指令 浮点运算溢出 杀死、终止进程 总线错误 段违例( segmentation violation ),进程试图去访问其虚地址空间以外的位置 系统调用中参数错,如系统调用号非法 向某个非读管道中写入数据 闹钟。当某进程希望在某时间后接收信号时发此信号 软件终止( software termination ) 用户自定义信号 1 用户自定义信号 2 8 18 SIGCLD 19 SIGPWR 某个子进程死 电源故障 function :在该进程中的一个函数地址,在核心返回用户态时,它以软中断信号的序号作为参数调用该函数,对除了信号 SIGKILL , SIGTRAP 和 SIGPWR 以外的信号,核心自动地重新设置软中断信号处理程序的值为SIG_DFL ,一个进程不能捕获 SIGKILL 信号。 function 的解释如下: ( 1 ) function=1 时,进程对 sig 类信号不予理睬,亦即屏蔽了该类信号; ( 2 ) function=0 时,缺省值,进程在收到 sig 信号后应终止自己; ( 3 ) function 为非 0 ,非 1 类整数时, function 的值即作为信号处理程序的指针。 参考程序4.4 #include } while((p1=fork( ))= =-1); /* 创建子进 else 程 p1*/ if (p1>0) { { wait_mark=1; while((p2=fork( ))= =-1); /* 创建子进signal(16,stop); /* 接收到软中断信程 p2*/ 号 16 ,转 stop*/ if(p2>0) waiting( ); { lockf(1,1,0); wait_mark=1; printf(\"Child process 1 is killed by parent!\\n\"); lockf(1,0,0); signal(SIGINT,stop); /* 接收到 ^c 信号, exit(0); 转 stop*/ waiting( ); } } kill(p1,16); /* 向 p1 发软中断信号 16*/ void waiting( ) kill(p2,17); /* 向 p2 发软中断信号 17*/ { wait(0); /* 同步 */ wait(0); while(wait_mark!=0); printf(\"Parent process is killed!\\n\"); } exit(0); void stop( ) } { else wait_mark=0; { } 运行结果: 屏幕上无反应,按下c^即ctrl+c (接收到SIGINT信号)后,显示 Parent process is killed! 9 分析原因: 上述程序中, signal( ) 都放在一段程序的前面部位,而不是在其他接收信号处。这是因为 signal( ) 的执行只是为进程指定信号值 16 或 17 的作用,以及分配相应的与 stop( ) 过程链接的指针。因而, signal( ) 函数必须在程序前面部分执行。你会发现Ctrl-c后,只会打印 Parent process is killed ,这是因为当你Ctrl-c的时候,系统会给父进程及其两个子进程都发送 SIGINT信号, 对于父进程来说,收到这个信号自然是调用 stop函数了,但是对于两个子进程来说,默认的对这个信号的处理就是退出(exit),所以你看不到子进程的打印。 本方法通信效率低,当通信数据量较大时一般不用此法。 四、思考题 1.上述哪些通信机制提供了发送进程和接收进程之间的同步功能?这些同步是如何进行的? 2.上述通信机制各有什么特点,它们分别适合于何种场合? 10 因篇幅问题不能全部显示,请点此查看更多更全内容
Copyright © 2019- igat.cn 版权所有 赣ICP备2024042791号-1
违法及侵权请联系:TEL:199 1889 7713 E-MAIL:2724546146@qq.com
本站由北京市万商天勤律师事务所王兴未律师提供法律服务