班级:2009211311
学号:
姓名: schnee
目 录
1. 实验目的... 3
2. 实验预备内容... 3
3. 环境说明... 3
4. 实验内容... 4
4.1. 进程的创建... 4
程序1
4.1.1题目要求:... 4
4.1.2程序设计说明:... 4
4.1.3源代码:... 4
4.1.4运行结果:... 5
4.1.5分析:... 5
4.2. 进程的控制... 6
程序2(a)
4.2.1题目要求:... 6
4.2.2程序设计说明:... 6
4.2.3源代码:... 6
4.2.4运行结果:... 7
4.2.5分析:... 7
程序2(b)
4.2.1题目要求:... 7
4.2.2程序设计说明:... 8
4.2.3源代码:... 8
4.2.4运行结果:... 9
4.2.5分析:... 10
4.3. 进程的软中断通信... 11
程序3(a)
4.3.1题目要求:... 11
4.3.2程序设计说明:... 11
4.3.3源代码:... 12
4.3.4运行结果:... 14
4.3.5分析:... 14
程序3(b)
4.3.1题目要求:... 14
4.3.2程序设计说明:... 14
4.3.3源代码:(略) 15
4.3.4运行结果及分析:... 15
4.4. 进程的管道通信... 17
程序4
4.4.1题目要求:... 17
4.4.2程序设计说明:... 17
4.4.3源代码:... 17
4.4.4运行结果:... 19
4.4.5分析:... 19
5. 思考... 20
1. 实验目的
1) 加深对进程概念的理解,明确进程和程序的区别
2) 进一步认识并发执行的实质
3) 分析进程争用资源的现象,学习解决进程互斥的方法
4) 了解Linux/windows系统中进程通信的基本原理
2. 实验预备内容
1) 阅读Linux的sched.h源码文件,加深对进程管理概念的理解
2) 阅读Linux的fork()源码文件,分析进程的创建过程
3. 环境说明
此实验采用的是Win7下虚拟机VMware-workstation-6.5.3-185404及kanas-ubuntu-10.10-desktop-i386。
直接编写文件在终端用命令行执行。
虚拟机分配8G 内存中的512M。
操作尚未取得root权限。
ubuntu用户名jrayty。
4. 实验内容
4.1. 进程的创建
程序一:
4.1.1题目要求:
编写一段程序,使用系统调用fork()创建两个子进程。当此程序运行时,在系统中有一个父进程和两个子进程活动。让每一个进程在屏幕上显示一个字符:父进程显示字符“a”,子进程分别显示字符“b”和“c”。试观察记录屏幕上的显示结果,并分析原因。
4.1.2程序设计说明:
此程序相对简单。详见源代码。
4.1.3源代码:
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
using namespace std;
int main()
{
pid_t p1, p2;
while((p1=fork()) == -1); //create child progress 1
if(p1 == 0)
{
putchar('b'); //child progress 1 print 'b'
exit(0);
}
else
{
while((p2=fork()) == -1); //create child progress 2
if(p2 == 0)
{
putchar('c'); //child progress 2 print 'c'
exit(0);
}
else
{
putchar('a'); //parent progress print 'a'
exit(0);
}
}
return 0;
}
4.1.4运行结果:
4.1.5分析:
从截图可见运行结果有abc, bac, acb等。
这是因为三个进程间没有同步措施,所以父进程和两个子进程的输出次序带有随机性。因此实际上三者的各种组合都可能出现。
4.2. 进程的控制
程序2.0:
4.2.1题目要求:
修改已经编写的程序,将每个进程输出一个字符改为每个进程输出一句话,再观察程序执行时屏幕上出现的现象,并分析原因。
4.2.2程序设计说明:
直接改自第一个程序。
其间加sleep() 是为了规范下格式。
详见源代码。
4.2.3源代码:
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
using namespace std;
int main()
{
pid_t p1, p2;
while((p1=fork()) == -1); //create child progress 1
if(p1 == 0)
{
puts("I'm the Child Progress 1 !!!");
exit(0);
}
else
{
while((p2=fork()) == -1);//create child progress 2
if(p2 == 0)
{
puts("I'm the Child Progress 2 !!!");
exit(0);
}
else
{
sleep(4);
puts("I'm the Parent Progress !!!");//parent progress
exit(0);
}
}
return 0;
}
4.2.4运行结果:
4.2.5分析:
可见运行情况和第一个程序是很像的。
虽然换成了字符串,但同一个进程里puts字符串并不会被中断。
程序2:
4.2.1题目要求:
如果在程序中使用系统调用lockf()来给每一个进程加锁,可以实现进程之间的互斥,观察并分析出现的现象。
4.2.2程序设计说明:
学习lockf()函数学了好久。。。
程序中运用了多个sleep()并且让每个进程多输出了几次,从而使结果更明显。
且让父进程休息较久,从而让子进程都执行完。
4.2.3源代码:
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
using namespace std;
int main()
{
pid_t p1, p2;
int i;
while((p1=fork()) == -1); //create child progress 1
if(p1 == 0)
{
lockf(1, 1, 0);//lock
for(i=0; i<4; i++)
{
sleep(1);
puts("I'm the Child Progress 1 !!!");
}
lockf(1, 0, 0);//unlock
exit(0);
}
else
{
while((p2=fork()) == -1);//create child progress 2
if(p2 == 0)
{
lockf(1, 1, 0);//lock
for(i=0; i<4; i++)
{
sleep(1);
puts("I'm the Child Progress 2 !!!");
}
lockf(1, 0, 0);//unlock
exit(0);
}
else
{
puts("I'm the Parent Progress !!!");
sleep(10);
puts("I'm the Parent Progress !!!");
exit(0);
}
}
return 0;
}
4.2.4运行结果:
******************************************若是去掉lockf(),则运行结果如下
4.2.5分析:
从上面运行结果可以看出。无论加不加锁,两个子进程运行的次序是随机的。但是加锁后一个进程会全部输出完了才转给另一个进程。从而我们可以看出lockf()对两个进程对标准输出流的互斥使用。
lockf(fileno(fp),F_LOCK,0L)
函数的第一个参数是加锁的文件,上述程序用的是1,即标准输出流;
第二个参数是操作,1表示加锁0表示解锁。
第三个参数是文件长度,文中用0表示整个文件。
4.3. 进程的软中断通信
程序3:
4.3.1题目要求:
(a)使用系统调用fork()创建两个子进程,再用系统调用signal()让父进程捕捉键盘上来的中断信号(即按Del/CTRL+C键).当捕捉到中断信号后,父进程用系统调用kill()向两个子进程发出信号,子进程捕捉到信号后分别输出下列信息后终止:
Child Process 1 is killed by Parent!
Child Process 2 is killed by Parent!
父进程等待两个子进程终止后,输出如下的信息后终止:
Parent Process is killed!
4.3.2程序设计说明:
1)题目解析和问题解决
一, 要求Del后引发父进程的动作。实际就是触发软中断SIGINT。
二, 要求在中断到来前两子进程处于等待状态,中断到来后立刻动作。对此,我自定义了两个函数my_wait()函数和my_stop()函数,通过对flag标志位的操作来实现。flag为真时等待,中断到来后通过my_stop()函数使flag改为假,从而退出等待的死循环。
虽然这样效率很低,但是在这个小程序里还是可以的。
三, 至于如何控制父进程杀死子进程后再自杀,我用了signal()函数预留给用户自定义的10和12号信号,即SIGUSR1和SIGUSR2。让两个子进程分别监听这两个信号,父进程被触发后分别向两个子进程发出这两个信号杀死他们,然后再退出即可。
2)知识点
补充学习到的函数的定义。
1)软中断信号预置函数signal (sig , function)
sig —— 系统给定的软中断信号中的序号或名称。
function —— 与软中断信号关联的函数名,当进程在运行过程中捕捉到指定的软中断信号后,中断当前程序的执行转到该函数执行。
注意:软中断信号必须提前预置,然后才可以在程序运行中捕获。
2)发送软中断信号函数 int kill ( pid , sig )
pid——表示一个或一组进程的标识符:
当pid>0时,将信号发送给指定pid的进程;
当pid=0时,将信号发送给同组的所有进程;
当pid=-1时,将信号发送给以下所有满足条件的进程:该进程用户标识符等于发送进程有效用户标识符;
sig——软中断信号的序号或名称
功能:向指定进程标识符pid的进程发软中断信号sig。本章中用来实现父进程给子进程发终止执行软中断信号。
3)父子父同步的实现
在进程同步中,使用exit()和wait()实现了父进程等子进程终止的同步,但是这种同步方法不能实现子进程对父进程的等待。要实现子进程对父进程的等待可以使用父进程向子进程发软中断信号,子进程接收信号的方式实现。
这两种同步方式相结合,可以实现父→子→父的同步序列。
实现父→子→父同步的步骤如下:
⑴子进程使用signal()预置软中断处理函数,然后等待父进程发软中断信号;
⑵父进程中使用kill()发软中断信号给子进程,再用wait(0)等待子进程结束;
⑶子进程接收到软中断信号后转去执行中断处理函数
⑷子进程在中断处理返回后,使用exit(0)终止执行,向父进程发终止信息。
⑸父进程使用wait(0)接收到子进程的终止信息后结束等待,并终止自己的程序的执行。
4.3.3源代码:
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
using namespace std;
bool flag;
pid_t p1, p2;
void my_wait(int q)
{
while(flag);
}
void my_stop(int q)
{
flag=false;
}
int main()
{
signal(SIGINT, my_stop); //Del or Ctrl+C trigger
while((p1=fork()) == -1); //create child progress 1
if(p1 == 0)
{
flag=true; //p1 set waiting
signal(SIGUSR1, my_stop);//set SIGUSR1 to stop
my_wait(0); //waiting
lockf(1, 1, 0); //lock
puts("\nChild Process 1 is killed by Parent!");
lockf(1, 0, 0); //unlock
sleep(1);
exit(0);
}
else
{
while((p2=fork()) == -1); //create child progress 2
if(p2 == 0)
{
flag=true; //p2 set waiting
signal(SIGUSR2, my_stop);//set SIGUSR2 to stop
my_wait(0); //waiting
lockf(1, 1, 0); //lock
puts("\nChild Process 2 is killed by Parent!");
lockf(1, 0, 0); //unlock
sleep(1);
exit(0);
}
else
{
flag=true; //parent set waiting
my_wait(0);
kill(p1, SIGUSR1);
kill(p2, SIGUSR2);
wait(NULL);
wait(NULL);
puts("\nParent Process is killed!");
exit(0);
}
}
return 0;
}
4.3.4运行结果:
4.3.5分析:
如上图,可见两个子进程的执行顺序还是随机的。
程序3.2:
4.3.1题目要求:
(b)在上面的程序中增加语句signal(SIGINT,SIG_IGN)和signal(SIGQUIT,SIG_IGN),观察执行结果,并分析原因。
4.3.2程序设计说明:
signal(SIGTINT, SIG_IGN);//后台进程读中断信号,默认挂起
signal(SIGQUIT, SIG_IGN);//程序终止信号,默认操作写dump-core文件
4.3.3源代码:(略)
4.3.4运行结果及分析:
***************************signal(SIGINT, my_wait)加到第二个else里面
***************************加signal(SIGINT, SIG_IGN)
**************************加signal(SIGINT, SIGIGN)后signal(SIGINT, my_wait)加到第二个else里面
分析:
1)单纯在程序3的基础上把signal(SIGINT, my_wait)加到第二个else里面后一开始子进程不再输出了。这是因为SIGINT信号不只向父进程还向子进程发出了信号,而子进程并未对它重定义故就执行默认定义直接被杀死了。于是子进程实际上是被SIGINT杀死的而不是被父进程kill的。
2)单纯地加了signal(SIGINT, SIGIGN)后虽然表面看起来和不加它没区别,但是本质却完全不同了。现在子进程确实是被父进程杀死的。因为SIGINT已经被忽略了。
3)加signal(SIGINT, SIGIGN)并且signal(SIGINT, my_wait)加到第二个else里面后输出也是一样,不会再发生子进程不输出的情况了。因为现在子进程的SIGINT已经被忽略了。
*******************************加了signal(SIGQUIT,SIG_IGN)
分析:在程序3的基础上在两个进程的前面加入signal(SIGQUIT, SIG_IGN), 运行结果如上图输入ctrl+C时还是不变但是^\后直接退出了,所有的进程都没有输出,并没有被屏蔽。
资料说
signal(SIGTINT, SIG_IGN);//后台进程读中断信号,默认挂起
signal(SIGQUIT, SIG_IGN);//程序终止信号,默认操作写dump-core文件
但是我还是没有太了解。
4.4. 进程的管道通信
程序4:
4.4.1题目要求:
编制一段程序,实现进程的管道通信。使用系统调用pipe()建立一条管道线,两个子进程P1和P2分别向管道各写一句话:
Child 1 is sending a message!
Child 2 is sending a message!
而父进程则从管道中读出来自于两个子进程的信息,显示在屏幕上。要求父进程先接收子进程P1发来的消息,然后再接收子进程P2发来的消息。
4.4.2程序设计说明:
为了避免两个进程抢占管道,我把第二个子进程延时了,并且对管道加锁,从而形成独占,避免冲突产生。
而父进程只需用之前的wait()函数即可确保在两个子进程后执行。
4.4.3源代码:
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
using namespace std;
const int PIPE_MAX = 0x7f; //pipe buffer max size
const int RW_MAX = 0x40; //r/w buffer max size
int main()
{
pid_t p1, p2;
int river[2]; //1 in, 0 out
char rb[PIPE_MAX], wb[PIPE_MAX];
pipe(river); //create pipe
while((p1=fork()) == -1); //create child progress 1
if(p1 == 0)
{
lockf(river[1], 1, 0); //lock
sprintf(wb, "Child 1 is sending a message!\n");
write(river[1], wb, RW_MAX);
lockf(river[1], 0, 0); //unlock
exit(0);
}
else
{
while((p2=fork()) == -1);//create child progress 2
if(p2 == 0)
{
sleep(1); //wait p1
lockf(river[1], 1, 0);//lock
sprintf(wb, "Child 2 is sending a message!\n");
write(river[1], wb, RW_MAX);
lockf(river[1], 0, 0);//unlock
exit(0);
}
else
{
wait(NULL); //wait p1
read(river[0], rb, RW_MAX);
printf("%s\n", rb);
wait(NULL); //wait p2
read(river[0], rb, RW_MAX);
printf("%s\n", rb);
exit(0);
}
}
return 0;
}
4.4.4运行结果:
4.4.5分析:
管道通信通过系统调用pipe()初始化一个二元组为管道,1出0进。
5. 思考
1)系统是怎么创建进程的?
答:新进程通过克隆老进程或是当前进程来创建。系统调用fork或clone可以创建新任务,复制发生在核心状态下地核心中。
在类UNIX系统中,除了根进程之外,如果想要在系统之中创建一个新的进程,唯一的方法就是利用一个已存在的进程通过系统调用fork()函数来实现,fork()函数被调用时,它会将调用它的进程复制出一个副本(一般会共享代码段,其它的数据段和堆栈等会复制个新的),原进程是复制得到进程的父进程,两个进程除了进程号不同,是一模一样,fork 调用结束后将 0 返回给子进程,将子进程的进程号返回给父进程,在调用fork函数之后,父子进程都将从位于fork函数的调用点之后的指令处开始执行,因为是两个不同的进程,它们的执行是并行的,先后次序是不定的。
(参考 《Linux内核完全解析》和 编程爱好者网站相关文章)
2)可执行文件加载时进行了哪些处理?
答:当操作系统装载一个可执行文件的时候,首先操作系统判断该文件是否是一个合法的可执行文件。如果是操作系统将按照段表中的指示为可执行程序分配地址空间。
加载文件最重要的是完成两件事:加载程序段和数据段到内存,以及进行外部定义符号的重定位。
下面是ELF文件为例的可执行文件加载处理:
1, 内核读文件的头部,然后根据文件的数据指示分别读入各种数据结构,找到可加载的段加载到内存中。
2, 内核分析出文件对应的动态连接器名称并加载动态连接器。
3, 内核在新进程的堆栈中设置一些标记-值对,以指示动态连接器的相关操作。
4, 内核把控制传递给动态连接器。
5, 动态连接器检查程序对外部文件(共享库)的依赖性,并在需要时对其进行加载。
6, 动态连接器对程序的外部引用进行重定位。
7, 动态连接器进行程序运行的初始化。
8, 动态连接器把控制传递给程序,从文件头部定义的程序进入点开始执行。
(参考execve()的内核代码和网上相关文章)
3)当首次调用新创建进程时,其入口在哪里?
答:当首次调用新创建进程时,入口是fork()之后的语句。
fork系统调用创建的子进程继承了原进程的context,也就是说fork调用成功后,子进程与父进程并发执行相同的代码。但由于子进程也继承了父进程的程序指针,所以子进程是从fork()后的语句开始执行(也就是新进程调用的入口)。另外fork在子进程和父进程中的返回值是不同的。在父进程中返回子进程的PID,而在子进程中返回0。所以可以在程序中检查PID的值,使父进程和子进程执行不同的分支。
(参考网上相关文章)
4)进程通信有什么特点?
答:(参考网上文章,个人怀疑“特点”难道就是“分类”吗?)
linux下进程间通信的几种主要手段简介:
管道(Pipe)及有名管道(named pipe):管道可用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信;
信号(Signal):信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数);
报文(Message)队列(消息队列):消息队列是消息的链接表,包括Posix消息队列system V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。
共享内存:使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。
信号量(semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。
套接口(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到其它类Unix系统上:Linux和System V的变种都支持套接字。