操作系统内核-实验二

实验目的

  1. 加深对进程概念的理解,明确进程和程序的区别;
  2. 进一步认识并发执行的实质;
  3. 分析进程争用资源的现象,学习解决进程互斥的方法;
  4. 了解Linux系统中进程通信的基本原理;

实验步骤

函数介绍

fork函数

  • 在Linux中,fork函数是非常重要的函数,它的作用是从已经存在的父进程中创建一个子进程。

    1
    2
    #include<unistd.h>		//引用fork时的头文件
    pid_t fork(void); //fork的返回类型为空1
  • 当一个进程调用fork函数之后,就有两个二进制代码相同的进程,运行在相同的位置,但是每个进程都开始自己的活动。

  • 调用fork函数,当控制转移到内核中的fork代码后,内核开始做如下工作:

    1. 分配新的内存块和内核数据结构给子进程;
    2. 将父进程部分数据结构内容拷贝至子进程;
    3. 将子进程添加到系统进程列表;
    4. fork返回开始调度器,调度。
  • 调用fork函数返回值的三种情况:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    int fork(); 
    pid = fork();
    if(pid < 0) { //错误
    printf("fork error\n");
    exit(0);
    }
    if(pid == 0) { //子进程
    printf("The child process is running now!\n");
    exit(0);
    }

    if(pid > 0) { //父进程
    printf("The parent process is running now!\n");
    exit(0);
    }
    1. pid < 0 ,运行错误;
    2. pid > 0 ,运行父进程;
    3. pid = 0 ,运行子进程。

wait函数

  • 函数原型:

    1
    2
    3
    #include <sys/types.h>		/*提供类型pid_t的定义*/
    #include <wait.h>
    int wait(int *status)
  • 函数功能:父进程一旦调用了wait函数就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。

  • 备注:

    • 当父进程忘了用wait函数等待已终止的子进程时,子进程就会进入一种无父进程的状态,此时子进程就是僵尸进程;

    • wait()要与fork()配套出现,如果在使用fork()之前调用wait(),wait()的返回值则为-1,正常情况下wait()的返回值为子进程的PID;

    • 如果先终止父进程,子进程将继续正常进行,只是它将由init进程(PID 1)继承,当子进程终止时,init进程捕获这个状态;

    • 参数status用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。但如果我们对这个子进程是如何死掉毫不在意,只想把这个僵尸进程消灭掉,我们就可以设定这个参数为NULL,就像下面这样:

      1
      pid = wait(NULL);
    • 如果成功,wait会返回被收集的子进程的进程ID,如果调用进程没有子进程,调用就会失败,此时wait返回-1,同时errno被置为ECHILD。

    • 如果参数status的值不是NULL,wait就会把子进程退出时的状态取出并存入其中,
      这是一个整数值(int),指出子进程是正常退出还是被非正常结束的,以及正常结束时的返回值,或被哪一个信号结束的等信息。由于这些信息被存放在一个整数的不同二进制位中,所以用常规的方法读取会非常麻烦,因此设计了一套专门的宏(macro)来完成这项工作,其中最常用的两个如下:

      1. WIFEXITED(status)
        这个宏用来指出子进程是否为正常退出的,如果是,它会返回一个非零值。

      2. WEXITSTATUS(status)

        当WIFEXITED返回非零值时,我们可以用这个宏来提取子进程的返回值,如果子进程调用exit(5)退出,WEXITSTATUS(status)就会返回5;如果子进程调用exit(7),WEXITSTATUS(status)就会返回7。如果进程不是正常退出的,也就是说,
        WIFEXITED返回0,这个值就毫无意义。

exit函数

进程结束最常用函数,表示正常结束;

1
2
#include <stdio.h>  
void exit(int status);

kill函数

删除执行中的程序和任务,异常结束;

1
kill(int PID,int IID);

signal函数

1
void(* signal(int sig, void(* func)(int)))(int);
  • 设置处理信号的功能指定使用sig指定的信号编号处理信号的方法。 参数func指定程序可以处理信号的三种方式之一:

    1. 默认处理(SIG_DFL):信号由该特定信号的默认动作处理;
    2. 忽略信号(SIG_IGN):忽略信号,即使没有意义,代码执行仍将继续;
    3. 函数处理程序:定义一个特定的函数来处理信号
  • 参数

    • SIG

      设置处理功能的信号值;

    • FUNC

      指向函数的指针;

    • 返回值

      返回类型与参数func的类型相同。

  • 常用的signal

    信号 功能
    SIGNHUP 挂起 1
    SIGINT 键盘中断,键盘按Ctrl+C 2
    SIGQUIT 键盘按QUIT 3
    SIGILL 非法指令 4
    SIGTRAP 跟踪中断 5
    SIGIOT IOT指令 6
    SIGBUS 总线错 7
    SIGFPE 浮点运算溢出 8
    SIGKILL 要求终止进程 9
    SIGUSR1 用户定义信号#1 10
    SIGSEGV 段违法 11
    SIGUSR2 用户定义信号#2 12
    SIGPIPE 向没有读进程的管道上写 13
    SIGALRM 定时器告警,时间到 14
    SIGTERM kill发出的软件结束信号 15
    SIGSTKFLT Linux专用,数学协处理器的栈异常 16
    SIGCHLD 子进程死 17
    SIGCOUNT 若已停止则继续 18
    SIGPWR 电源故障 30

pipe函数

管道是一种把两个进程之间的标准输入和标准输出连接起来的机制,从而提供一种让多个进程间通信的方法,当进程创建管道时,每次都需要提供两个文件描述符来操作管道。其中一个对管道进行写操作,另一个对管道进行读操作。对管道的读写与一般的IO系统函数一致,使用write()函数写入数据,使用read()读出数据。

1
2
#include<unistd.h>
int pipe(int filedes[2]);

返回值:成功,返回0,否则返回-1。

参数数组包含pipe使用的两个文件的描述符。

  • fd[0]:读管道;
  • fd[1]:写管道;
  • fd[2]:提供进程使用的文件描述符数组。

必须在fork()中调用pipe(),否则子进程不会继承文件描述符。两个进程不共享祖先进程,就不能使用pipe。

编制实现软中断通信的程序

程序分析

使用系统调用fork()创建两个子进程,再用系统调用signal()让父进程捕捉键盘上来的中断信号(即按Ctrl+c键),当父进程接受到这两个软中断的其中某一个后,父进程用系统调用kill()向两个子进程分别发送整数值为16和17软中断信号,子进程获得对应软中断信号后,分别输出下列信息后终止:

1
2
Child process 1 is killed by parent !!
Child process 2 is killed by parent !!

父进程调用wait()函数等待两个子进程终止后,输出以下信息后终止:

1
Parent process is killed!! 

程序流程图

程序源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>

void waiting();
void stop();

int wait_mark;

int main()
{
int p1,p2; /*定义两个进程号变量*/
while((p1=fork())<0); /*创建子进程p1*/
if(p1>0){
while((p2=fork())<0); /*创建子进程p2*/
if(p2>0){
printf("Father PID:%d\n",getpid());
wait_mark=1; /*不往下执行,直到捕捉到键盘上传来的信号*/
signal(SIGINT,stop); /*接收到^c信号,转stop*/
waiting();
kill(p1,16); /*向p1发软中断信号16*/
kill(p2,17); /*向p2发软中断信号17*/
wait(0); /*同步*/
wait(0);
printf("Parent process is killed!\n");
exit(0);
}
else{
printf("Child2 PID:%d\n",getpid());
wait_mark=1;
signal(17,stop); /*接收到软中断信号17,转stop*/
signal(SIGINT,SIG_IGN); /*屏蔽从键盘上传来的中断信号,使得子进程可以接收到父进程传来的软中断信号*/
waiting(); /*不往下执行*/
lockf(stdout,1,0);
printf("Child process 2 is killed by parent!\n");
lockf(stdout,0,0);
exit(0);
}
}
else{
printf("Child1 PID:%d\n",getpid());
wait_mark=1;
signal(16,stop); /*接收到软中断信号16,转stop*/
signal(SIGINT,SIG_IGN); /*屏蔽从键盘上传来的中断信号,使得子进程可以接收到父进程传来的软中断信号*/
waiting(); /*不往下执行*/
lockf(stdout,1,0);
printf("Child process 1 is killed by parent!\n");
lockf(stdout,0,0);
exit(0);
}
}

void waiting(){
while(wait_mark!=0);
}

void stop(){
wait_mark=0;
}

程序运行结果

运行结果分析

​ 程序运行两次,软中断后,结果输出顺序不相同。理论上,父程序在同一时间下达指令,软中断子程序1和子程序2并发执行,并发执行并不是同时执行,而可能是交叉执行,两个子程序执行完成时间不一致,即第一次运行程序2先完成,第二次运行程序1先完成。

编制实现进程的管道通信的程序

程序分析

使用系统调用pipe()建立一条管道线,两个子进程分别向管道写一句话:

1
2
Child process 1 is sending a message!  
Child process 2 is sending a message!

父进程从管道中先后读出来自于两个子进程p1、p2的消息,显示在屏幕上。

程序流程图

程序源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>

int pid1,pid2; /*定义两个进程号变量*/

main()
{
printf("Father PID:%d\n",getpid());
int fd[2];
char outpipe[100],inpipe[100]; /*定义两个字符数组*/
pipe(fd); /*创建一个管道*/
while ((pid1=fork())<0); /*如果进程1创建不成功,则空循环*/
if(pid1==0){ /*如果子进程1创建成功,pid1为进程号*/
printf("Child1 PID:%d\n",getpid());
lockf(fd[1],1,0); /*锁定管道*/
sprintf(outpipe,"child 1 process is sending message!"); /*把串放入数组outpipe中*/
write(fd[1],outpipe,50); /*向管道写长为50字节的串*/
lockf(fd[1],0,0); /*解除管道的锁定*/
exit(0); /*结束进程1*/
}
else{
while ((pid2=fork())<1); /*若进程2创建不成功,则空循环*/
if(pid2==0){
printf("Child2 PID:%d\n",getpid());
lockf(fd[1],1,0); /*锁定管道*/
sprintf(outpipe,"child 2 process is sending message!");
write(fd[1],outpipe,50);
lockf(fd[1],0,0); /*解除管道的锁定*/
exit(0); /*结束进程2*/
}
else
{
wait(0); /*同步,等待子进程1结束*/
read(fd[0],inpipe,50); /*从管道中读长为50字节的串*/
printf("%s\n",inpipe); /*显示读出的数据*/
wait(0); /*同步,等待子进程2结束*/
read(fd[0],inpipe,50); /*从管道中读长为50字节的串*/
printf("%s\n",inpipe); /*显示读出的数据*/
exit(0); /*结束父进程*/
}
}
}

程序运行结果

运行结果分析

根据对源代码的注释分析,父进程必须先接收子程序1的消息,再接收子程序2的消息。因此必须先创建成功子程序1,再创建子程序2,这里用到了两个while()循环如下

1
2
while ((pid1=fork())<0);		/*如果进程1创建不成功,则空循环*/
while ((pid2=fork())<0); /*如果进程2创建不成功,则空循环*/

因此会先打印出子程序1的PID和消息,再打印子程序2的PID和消息。

实验难点与收获

难点

  1. 对并发执行的理解;
  2. 对管道通信过程的学习等;

收获

  1. 学习了进程管理的相关函数如fork()、wait()、kill()、signal()、pipe()等的使用;
  2. 知道了并发执行并不是同一时间执行两个程序,对并发有了更确切的理解;
  3. 深入理解了进程互斥的概念;
  4. 了解了Linux系统中进程通信的基本原理等;

实验思考

通过这次实验,我对操作系统进程管理的内容有了更深入的理解。在linx操作系统中,进程的创建需要调用fork函数,该函数调用一次,返回两次。深入理解了进程互斥的概念,即两个或两个以上的进程,不能同时进入关于同一组共享变量的临界区域。进程互斥通过lockf()来实现。通过kill()函数和signal()函数深入理解进程的之间的软中断。前者是发送软中断信号,后者是接收软中断信号。通过pipe()函数理解进程之间的管道通信。