一、 实验目的
1、掌握Linux中进程的控制。
2、掌握Linux中通过管道进行通信的方法;
3、掌握Linux中通过共享内存进行通信的方式;
二、 实验仪器设备
PC机、Ubuntu环境。
三、 实验原理
(一)进程控制
进程是操作系统的核心,是操作系统的调度单位。Linux系统通过进程号(非负整数)来唯一地标识每个进程。
system()函数及exec函数执行程序时,其机制是不同的:system()函数会创建一个子进程来执行程序,当程序执行完成后返回父进程继续执行;exec函数则会用新进程将自己完全覆盖,原进程后面代码不会再执行。
(二)进程通信
每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,如图所示。
进程间通信方式的种类包括:
(1)管道(Pipe)及命名管道(named pipe):管道可用于具有亲缘关系进程间的通信;命名管道,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。
(2)信号(Signal):信号是在软件层次上对中断机制的一种模拟,它是比较复杂的通信方式,用于通知进程有某事件发生,一个进程收到一个信号与处理器收到一个中断请求效果上可以说是一样的。
(3)消息队列(Messge Queue):消息队列是消息的链接表,包括Posix消息队列SystemV消息队列。它克服了前两种通信方式中信息量有限的缺点,具有写权限的进程可以按照一定的规则向消息队列中添加新消息;对消息队列有读权限的进程则可以从消息队列中读取消息。
(4)共享内存(Shared memory):可以说这是最有用的进程间通信方式。它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种通信方式需要依靠某种同步机制,如互斥锁和信号量等。
(5)信号量(Semaphore):主要作为进程之间以及同一进程的不同线程之间的同步和互斥手段。
(6)套接字(Socket):这是一种更为一般的进程间通信机制,它可用于网络中不同机器间的进程间通信,应用非常广泛。
四、 实验内容及注意事项
1、调用system()函数及exec函数执行程序;
2、在Linux 中编写简单的管道程序,完成消息或文件的传输,了解管道操作的基本步骤;
3、编写共享内存程序,实现两个进程间通信的方式。
五、 实验组织运行
根据本实验指导书,学生自主训练为主。
六、 实验步骤
(一)进程控制
system()函数及exec函数族均可在进程中开始其它进程,但二者在执行过程上有区别。请通过下面的程序体会二者的作用及区别。
1、system函数的调用
①编写以下程序:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int ret;
ret=system("ps -l");
printf("OK ret = %d \n", ret);
return 0;
}
②将程序的运行结果抓图如下:
2、exec()函数族的调用
当进程调用一种exec函数时,该进程执行的程序完全替换为新程序,而新程序从其main函数开始执行
①编写以下程序:
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
int ret;
/*调用execlp()函数,这里相当于调用了"ps -l"命令*/
ret = execlp("ps", "ps", "-l", NULL);
printf("ps OK %d",ret);
return 0;
}
②将程序的运行结果抓图如下:
③请分析本程序的执行结果与system()函数调用有何区别?为什么?
答:system()函数会创建一个子进程来执行程序,当程序执行完成后返回父进程继续执行;exec函数则会用新进程将自己完全覆盖,原进程后面代码不会再执行。
④如果仍采用调用exec()函数族的方式来调用开始其它进程,但要得到最后的输出 “OK”,请修改程序实现,将代码粘贴如下,并将修改部分标红。
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
int ret;
if(fork() == 0){
ret = execlp("ps", "ps", "-l", NULL);
}
wait(0);
printf("ps OK %d",ret);
return 0;
}
(二)基本管道操作
管道(PIPE)是Linux中最常见的IPC机制,它实际上是在进程间开辟一个固定大小的缓冲区,需要发布信息的进程运行写操作,需要接受信息的进程运行读操作。管道是单向的字节流,它把一个进程的标准输出和另一个进程的标准输入连接在一起。
管道的特点:
(1)管道式半双工的,数据只能单向流动;需要相互通信时,就要建立两个管道。
(2)只能用于具有亲缘关系的进程之间的通信(也就是父子进程或者兄弟进程之间,有名管道则突破了这一限制)。
(3)管道构成一种独立的文件系统,对于它的读写也可以使用普通的read()和write()等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内核的内存空间中。
(4)数据的读出和写入都是单向的。一个进程向管道中写的数据被管道另一端的进程读出。写入的数据每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。
(5)如果要实现父子进程的双向通信,可以创建两个管道来进行。
用pipe()函数创建的管道两端处于一个进程中,由于管道是主要用于在不同进程间通信的,因此这在实际应用中没有太大意义。实际上,通常先是创建一个管道,再通过fork()函数创建一子进程,该子进程会继承父进程所创建的管道 。
1、创建管道实验pipe.c
#include <stdio.h>
#include <stdlib.h>
int main()
{
int x, fd[2];
char buf[30], s[30];
pipe(fd);
x = fork();
if(x == 0)
{
sprintf(buf,"This is an pipend!");
write(fd[1], buf, 30);
exit(0);
}
else
{
wait(0);
read(fd[0], s, 30);
printf("read: %s \n", s);
}
}
2、请将程序运行结果抓图如下:
运行结果:
3、请分析程序回答:该管道的数据流向为:
答:从子进程到父进程
4、程序中“wait(0);”,其功能是什么?可否不要?为什么?
功能:阻塞读进程,先执行子进程写操作,再让父进程进行读操作
可否不要(请通过程序验证后,根据结果分析):可以
为什么:read系统调用在执行时如果要读取的内容没有数据会将自己阻塞起来,等有数据可以读取了再继续执行。
5、修改以上程序,实现管道流向由父进程写、子进程读。请将实现代码粘贴如下,并将其中修改部分标红。
#include <stdio.h>
#include <stdlib.h>
int main()
{
int x, fd[2];
char buf[30], s[30];
pipe(fd);
x = fork();
if(x != 0)
{
sprintf(buf,"This is an pipend!");
write(fd[1], buf, 30);
exit(0);
}
else
{
read(fd[0], s, 30);
printf("read: %s \n", s);
}
}
6、在上面程序的基础上,编写程序pipe-1.c,实现在父子进程之间建立双向管道通信。请将代码及程序运行结果粘贴如下:
Pipe-1.c代码:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int x,fd1[2],fd2[2];
char buf[30],s[30];
pipe(fd1);
pipe(fd2);
x=fork();
if(x!=0)
{
read(fd1[0],s,30);
printf("father read from son:%s\n",s);
sprintf(buf,"Son! Im ur father!");
write(fd2[1],buf,30);
}
else
{
sprintf(buf,"Father, Im ur son!");
write(fd1[1],buf,30);
read(fd2[0],s,30);
printf("son read from father: %s\n",s);
exit(0);
}
}
程序运行结果:
(三)共享内存通信
所谓共享内存(shared memory)就是多个进程间共同使用同一段物理内存空间,它是通过将同一段物理内存映射到不同进程的虚空间中来实现的。由于映射到不同进程的虚空间中,不同进程可以直接读写内存,不需要进行内存的复制,所以共享内存的效率很高。
如图所示,同一块物理内存被映射到进程A、B各自的进程地址空间。进程A能够看到进程B对共享内存中数据的更新。由于多个进程共享同一块内存区域,必然需要某种同步机制,比如:互斥锁、信号量等
共享内存实现方法:
(1)创建共享内存。通过shmget( )函数从内存中获得或创建一个IPC共享内存区域,并返回相应的标识符。
(2)映射共享内存。通过shmat( )函数将创建的共享内存映射到具体的进程空间中去。该函数返回共享内存在进程空间中的地址,用户可以通过指针,使用不带缓冲的I/O读写这块共享内存。
(3)撤销映射。在对共享内存读写完毕后,通过shmdt( )函数将共享内存从进程空间分离出去。
1、创建共享内存区。程序代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <sys/shm.h>
int main()
{
int shm_id;
shm_id = shmget(IPC_PRIVATE, 4096, 0666);
if(shm_id < 0)
{
perror("shmget id < 0 ");
exit(0);
}
printf("成功建立共享内存区域: %d \n", shm_id);
system("ipcs -m");
}
将程序运行结果抓图,并圈出其中创建的共享内存:
2、映射共享内存,并向共享内存中写入内容。
#include <stdio.h>
#include <sys/shm.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
int shm_id;
char *shm_buf;
shm_id = atoi(argv[1]);
shm_buf=shmat(shm_id, 0, 0);
printf("写入数据到共享内存: \n");
sprintf (shm_buf, "欢迎来到共享内存空间!");
printf("%s \n", shm_buf);
}
将程序运行结果抓图如下:
3、映射共享内存,并从共享内存中读出内容。
#include <stdio.h>
#include <sys/shm.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
int shm_id;
char *shm_buf, str[80];
shm_id = atoi(argv[1]);
shm_buf=shmat(shm_id, 0, 0);
printf("读取共享内存数据: \n");
sprintf(str, shm_buf);
printf("%s \n", str);
system("ipcs -m");
}
将程序运行结果抓图如下:
4、撤销共享内存的映射,并释放内存空间。程序如下:
#include <stdio.h>
#include <sys/shm.h>
int main(int argc, char *argv[])
{
int shm_id;
char *shm_buf;
shm_id = atoi(argv[1]);
shm_buf=shmat(shm_id, 0, 0);
shmdt(shm_buf);
shmctl(shm_id, IPC_RMID, NULL);
system("ipcs -m");
}
将程序运行结果抓图如下: