Linux进程(三)系统中进程控制功能详解

进程控制是指对进程的生命周期进行有效的管理,实现进程的创建、撤销以及进程各状态之间的转换等控制功能。进程控制的目标是使多个进程能平稳地并发执行,充分共享系统资源。

1.1 进程控制的功能

进程控制的功能使控制进程在整个生命周期中各种状态之间的转换(不包括就绪态与运行态之间的转换,它们是由进程调度来实现的)。为此,内核提供了几个原子性的操作函数,称为原语。他与普通函数的区别使它的各个指令的执行是不可分割的,要么全部完成,要么一个也不做,因此可以看作是一条广义的指令。用于进程控制的原语主要有创建、终止、阻塞和唤醒等。

(1)创建进程

创建原语的主要任务是根据创建者提供的有关参数(包括进程名,进程优先级,进程代码起始地址,资源清单等信息),建立进程的PCB。具体的操作是:先申请一个空闲的PCB结构,调用资源的分配程序为它分配所需的资源,将有关信息填入PCB,状态设置为就绪态,然后把他插入就绪队列中。

(2)撤销进程

撤销原语用于在一个进程运行终止时,撤销这个进程并释放进程占用的资源。撤销的操作过程是:找到被撤销的进程的PCB,将它从所在队列中摘出,释放进程所占用的资源,最后消去进程的PCB。

(3)阻塞进程

阻塞原语用于完成从运行态到等待态的转换工作。当正在运行的进程需要等待某一事件而无法执行下去时,它就调用阻塞原语把自己转入等待态,插入到相应的等待队列中;最后调用进程调度程序,从就绪(可执行)队列中选择一个进程投入运行。

(4)唤醒进程

唤醒原语用于完成等待态到就绪态的转换工作。当处于等待态的进程所等待的事件出现时,内核会调用唤醒原语唤醒被阻塞的进程。操作过程是:在等待队列中找到该进程,置进程的当前状态为就绪态,然后将他从等待队列中撤出并插入到就绪队列中。

1.2 Linux系统的进程控制

在Linux系统中,进程控制的功能是由内核的进程控制子系统实现的,并以系统调用的形式提供给用户进程或其他系统进程使用。

1.2.1 进程的创建与映像更换

系统启动时执行初始化程序,启动进程号为1的init进程运行。系统总所有的其他进程都是由init进程衍生而来的。除init进程外,每个进程都是由另一个进程创建的。新建的进程称为子进程,创建子进程的叫父进程。

Unix/Linux系统创建进程的方式与众不同。它不是一步构造出新的进程,而是采用先复制再变身两个步骤,即先按照父进程创建一个子进程,然后再更换进程映像开始执行

1.2.1.1 创建进程

创建一个进程的系统调用是

fork()

创建进程采用的方法是克隆,即父进程复制一个子进程。做法是:先获得一个空闲的PCB,为子进程分配一个PID,然后将父进程的PCB中的代码即资源复制给子进程的PCB,状态置为可执行状态。建好PCB后将其链接入进程链表和可执行队列中。此后,子进程和父进程并发执行。父子进程执行的是同一个代码,使用的是同样的资源,它与父进程的区别仅仅在于PID(进程号)、PPID(父进程号)和子进程运行相关的属性(如状态,累计运行时间等),而这些是不能从父进程那里继承来的。

fork()系统调用

【功能】创建一个新的子进程

【调用格式】 int fork();

【返回值】0 向子进程返回的返回值,总为0

  • 0 向父进程返回的返回值,它是子进程的PID
  • 1 创建失败

【说明】若fork()调用成功,则它向父进程返回子进程的PID,并向新建的子进程返回0。

当一个进程成功执行了fork()后,从调用点之后分裂成了两个进程:一个是父进程,从fork()后的代码从继续运行;另一个是新建的子进程,从fork()后的代码处开始运行。

与一般函数不同,fork()是“一次调用,两次返回”,因为在调用成功后,已经是两个进程了。由于子进程是从父进程那里复制的代码,因此父子进程执行的是同一个程序,它们在执行时的区别只在于得到的返回值不同。父进程得到的返回值时子进程的PID;子进程得到的返回值是0。

若不考虑fork()的返回值,则父子进程的行为就完全一样了,但创建一个子进程的目的是想让它做另一件事。所以,通常的做法是:在fork()调用后,通过判读fork()的返回值,分别为父进程和子进程设计不同的执行分支。这样父子进程虽是同一个代码,执行路线却分道杨彪。

例: 一个简单的fork_test程序:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
        int rid;
        rid = fork();

        if(rid < 0){
                printf("fork error!");
                return 0;
        }

        if(rid > 0){
                printf("I am parent,my rid is %d,my PID is %d\\\\n",rid,getpid());
        }else{
                printf("I am child,my rid is %d,my PID is %d\\\\n",rid,getpid());
        }

        return 1;
}

注:程序中的getpid()是一个系统调用,他返回本教程的教程表示号PID。

fork_test程序运行时,父子进程将会输出不同的信息

由于两进程时并发的,它们的输出信息的先后次序不确定,有可能父先子后,也可能相反。

1.2.1.2 更换进程映像

进程映像是指进程所执行的程序代码及数据。fork()是将父进程的执行映像拷贝给子进程,因而子进程实际上是父进程的克隆体。单用户通常需要的是创建一个新进程,它执行的是一个不同的程序。Linux系统的做法是,先用fork()克隆一个子进程,然后再子进程中调用exec(),使其脱胎换骨,变为一个全新的进程。

exec()系统调用的功能是根据参数指定的文件名找到程序文件,把他装入内存,覆盖原来进程的映像,从而形成一个不同于父进程的全选的子进程。除了进程映像被更换外,子进程的PID及其他PCB属性保存不变,实际上是一个新的进程“借壳”原来的子进程开始运行。

exec() 系统调用

【功能】改变进程的映像,使其执行另外的程序

【调用格式】exec()是一系列系统调用,共有6种调用格式,其中 execve() 才是真正的系统调用,其余是对其包装后的C库函数。

    int execve(char *path,char *argc[],char *envp[]);

    int execl(char *path,char *arg0,char *arg1,...,char *argn,0);

    int execle(char *path,char *arg0,char *arg1,...,char *argn,0,char *exvp[]);

【参数说明】path为要执行的文件的路径名,argv[]为运行参数数组,envp[]为运行环境数组。arg0为程序的名称,arg1~argn为程序的运行参数,0表示参数结束。

例如:

    execl("/bin/echo","echo","hello!",0);

    execle("/bin/ls","ls","-l","/bin",0,NULL);

前者表示更换进程映像为/bin/echo文件,执行的命令行是“echo hello!”

后者表示更换进程映像为/bin/ls文件,执行的命令行是“ls -l /bin”

【返回值】调用成功后,不返回,调用失败后,返回-1。

与一般函数不同,exec()是“一次调用,零次返回”,因为调用成功后,进程的映像已经被替换,无处可以返回。下图描述了用exec()系统调用更换进程映像的流程。子进程开始运行后,立刻调用exec(),变身成功后即开始执行性的程序了。

例:一个简单的fork-exec_test程序:

#include <stdio.h>
#include <unistd.h>

int main()
{
        int rid;
        rid = fork();
        if(rid > 0){
                printf("I am parent\\\\n");
        }else{
                printf("I am child,l'll change to echo!\\\\n");
                execl("/bin/echo","echo","hello!",(char *)0);
        }
        return 0;
}

fork返回后,父子进程分别执行各自的分支,父进程输出信息“I am parent”.子进程输出信息“I am child,l'll change to echo!”,然后调用exec(),变换为echo程序。echo随即开始执行并输出字符串“hello”。

1.2.2 进程的终止与等待

1.2.2.1 进程的终止与退出状态

导致一个进程终止运行的方式有两种:一是程序中使用退出语句主动终止运行,我们称其为正常终止;另一种是被某个信号杀死(例如:在程序运行时按Ctrl+C终止其运行)称为非正常终止。

用C语言编程时,我们可以通过以下4种方式主动退出:

    (1)调用exit(status)函数来结束程序;

    (2)在main()函数种调用return status语句结束;

    (3)在main()函数中调用return语句结束;

    (4)main()函数结束。

以上4种情况都会使进程正常终止,前3种为显示地终止程序的运行,后一种为隐式地终止。正常终止的进程可以返回给系统一个退出状态,即前2种语句中的status。通常的约定是:0表示正常状态;非0表示异常状态,不同取值表示异常的具体原因。例如对一个计算程序,可以约定退出状态为0表示计算成功,为1表示运算数出错,为2表示运算符出错等。如果程序结束时没有指定的退出状态(如后两者),则他的退出状态时不确定的。

设置退出状态的作用时通知父进程有关此次运行的状况,以便父进程做出相应的处理。因此,显示地结束程序并返回退出状态时一个好的Linux/Unix编程习惯,这样的程序可以将自己的运行状况告知系统,因而能更好地与系统和其他程序合作。

1.2.2.2 终止进程

进程无论以那种方式结束,都会调用一个exit()系统调用,通过这个系统调用终止自己的运行,并及时通知父进程回收本进程。exit()系统调用完成以下操作:

  • 释放进程除PCB外的几乎所有资源;
  • 向PCB写入进程退出状态和一些统计信息;
  • 置进程态为“僵死态”;
  • 向父进程发送"子进程终止"信号;
  • 调用进程调度程序切换CPU的运行进程。

至此,子进程已变为"僵尸进程",它不再具备任何执行条件,只是PCB还在。保留PCB的目的时为了保存有关该进程运行的重要信息,比如这个进程的退出状态、运行时间的统计、收到信号的数目等。子进程的最后回收工作由父进程负责。父进程收集子进程的信息后将其PCB撤销。如果某一个进程由于某种原因先于子进程终止,有它创建的子进程就会变成"孤儿进程"。当系统中出现孤儿进程时,init进程将会发现并收养它,成为它的父进程。由于init进程不会退出,所以所有的进程都会被收养,最后,在系统关机之前,init进程要负责结束所有的进程。

exit()系统调用 【功能】使进程主动终止

【调用格式】void exit (int status);

【参数说明】status是要传给父进程的一个整数,用于父进程通报进程运行的结构状态。status的含义通常是:0表示正常终止;非0表示运行有错,异常终止。

1.2.2.3 等待与收集过程

在并发执行的环境中,父子进程的运行速度是无法确定的。在许多情况下,我们希望父子进程的进展能有某种同步关系。比如,父进程需要等待子进程的运行结果才能继续执行下一步计算,或父进程要负责子进程的回收工作,他必须在子进程结束后才能退出。这时就需要通过wait()系统调用来阻塞父进程,等待子进程结束。

当父进程调用wait()时,自己立即被阻塞,由wait()检查是否有僵死子进程,如果找到就收集它的信息,然后撤掉它的PCB;否则就阻塞下去,等待子进程发来的终止信号。父进程被信号唤醒后,执行wait(),处理子进程的回收工作,经wait()收集后,子进程才真正的消失。

wait( )系统调用 【功能】阻塞进程直到子进程结束;收集子进程。

    【调用格式】int wait(int *statloc)

    【参数说明】*statloc保存了子进程的一些状态。如果是正常退出,则字节莫为0,第2字节为退出状态;如果是非正常退出(即被某个信号终止),则其末字节不为0,末字节的低7位为导致进程终止的信号的信号值,若不关心子进程是如何终止的,可以用NULL作参数,即wait(NULL)。

    【返回值】>0 子进程的PID;
    -1 调用失败;
    0 其他;
    下图描述了wait()系统调用等待子进程的流程

例:一个简单的wait-exit_test程序:

#include <stdio.h>
#include <stdlib.h>

int main()
{
        int rid,cid,status;
        rid = fork();
        if(rid < 0){
                printf("fork error!\\\\n");
                exit(-1);
        }if(rid == 0){
                printf("I am child.I will sleep a while.\\\\n");
                sleep(10);
                exit(0);
        }

    cid = wait(&status);
    printf("I catched a child with PID of %d\\\\n",cid);

    if((status & 0377) == 0){
            printf("It exited normoally with status of %d\\\\n",status>>8);
    }else{
            printf("It is terminated by signal %d\\\\n",status&0177);
            exit(0);
    }

    return 0;
}

执行过程为:父进程在创建子进程失败时会用exit(-1)退出。成功创建子进程后,父进程会调用wait()阻塞自己;子进程运行,先输出信息,睡眠10秒后调用exit(0)退出父进程发信号,自己结束。父进程被唤醒后,从wait()返回,根据获得的子进程的PID和退出状态判断子进程的运行情况并输出相应

1.3 Shell命令的执行过程

Shell程序的功能就是执行Shell命令,执行命令的主要方式就是创建一个子进程,让这个子进程来执行命令的映像文件。因此,Shell进程是所有在其下执行的命令的父进程。下图所示是Shell执行命令的大致过程,从中可以看到一个进程从诞生到消失的整个过程。

Shell进程初始化完成后,在屏幕上显示命令提示符,等待命令行输入。接收到一个命令行后,Shell对其进行解析,确定要执行的命令及其选项和参数,以及命令的执行方式,然后创建一个子Shell进程。

子进程诞生后立即更换进程映像为要执行的命令的映像文件,运行该命令直至结束。如果命令行后面没有带有后台运行符“&”,则子进程在前台开始运行。此时,Shell阻塞自己,等待命名执行结束。如果命令行后带有“&”,则子进程在后台开始运行,同时Shell也继续执行下去。它立即显示命令提示符,接受下一个命令。命令子进程执行结束后,向父进程Shell进程发送信号,由Shell对子进程进行回收处理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Dola_Pan

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值