《UNIX环境高级编程》笔记——进程控制

前言

之前系统学习了一下进程间通信,然而又想到进程的相关知识也需要再复习一下,因此阅读《UNIX环境高级编程》进程控制篇章,主要记录UNIX系统进程控制,例如进程创建及终止的方式,并结合之前学习的进程间通信知识写一些简单实例。另外还对像僵尸进程和孤儿进程这样的概念进行梳理。

进程标识

每个进程都有一个非负整数表示的唯一进程ID。
进程ID可复用,意为当一个进程终止后,其进程ID就成为复用的候选者。
系统中存在专用进程:

  • ID为0的进程通常是调度进程,常被称为交换进程(swapper)。该进程是内核的一部分,不执行磁盘上的程序,因此也被称为系统进程。
  • ID为1的进程通常是init进程,在自举过程结束时由内核调用,负责在自举内核后启动一个Unix文件。init通常读取与系统有关的初始化文件,并将系统引导到一个状态。init进程不会终止。init是一个普通的用户进程但以超级用户特权运行。
  • 某些UNIX的虚拟存储器实现中,ID为2的进程是页守护进程,负责支持虚拟存储器系统的分页操作。

我们可以在Linux终端界面用ps -ef查看进程,下面展示部分结果,从中我们可以看到进程ID为1的init进程。

$ ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 18:34 ?        00:00:01 /sbin/init splash
root         2     0  0 18:34 ?        00:00:00 [kthreadd]
root         3     2  0 18:34 ?        00:00:00 [rcu_gp]
root         4     2  0 18:34 ?        00:00:00 [rcu_par_gp]
root         5     2  0 18:34 ?        00:00:00 [kworker/0:0-eve]
root         6     2  0 18:34 ?        00:00:00 [kworker/0:0H-kb]
root         7     2  0 18:34 ?        00:00:00 [kworker/0:1-eve]
root         8     2  0 18:34 ?        00:00:00 [kworker/u4:0-ev]
root         9     2  0 18:34 ?        00:00:00 [mm_percpu_wq]
root        10     2  0 18:34 ?        00:00:00 [ksoftirqd/0]
root        11     2  0 18:34 ?        00:00:00 [rcu_sched]
root        12     2  0 18:34 ?        00:00:00 [migration/0]
...

各参数解释如下:

  • UID:用户ID
  • PID:进程ID
  • PPID:父进程ID
  • C:进程的 CPU 处理器利用率
  • STIME:开始时间
  • TTY:启动进程的终端名
  • TIME:累计运行时间
  • CMD:该进程所运行的命令

进程通过调用函数 getpidgetppid 来查看当前进程的进程 ID 和当前进程父进程的 ID。

#include <unistd.h>
pid_t getpid(void);//返回当前进程ID
pid_t getppid(void);//返回父进程ID

fork

一个现有进程可以通过调用fork函数创建一个新进程

#include <unistd.h>
pid_t fork(void);
//子进程返回0,父进程返回子进程ID,出错返回-1

调用成功后,创建的新进程被称为子进程,调用fork的进程即为父进程。两个进程继续执行后面的指令。
注意该函数有两个返回值,子进程返回0,父进程返回新建的子进程ID。一个子进程只有一个父进程,且子进程可以通过调用getppid得到父进程ID,而一个父进程可以有多个子进程,且没有函数可以调用让父进程获取子进程ID,所以fork将新建立的子进程ID返回给父进程。

示例

#include <cstdio>
#include <iostream>
#include <cstdlib>
#include <cerrno>
#include <unistd.h>
using namespace std;
int main() {
    int value = 1;
    pid_t pid = fork();
    if (pid > 0) {
        cout << "Parent:" << endl;
        ++value;
        pid_t myid = getpid();
        cout << "PID:=" << myid << endl;
        cout << "Child_ID=" << pid << endl;
        cout << "value=" << value << endl;
        sleep(10);
    }
    else if (pid == 0) {
        sleep(5);
        pid_t myid = getpid();
        pid_t ppid = getppid();
        cout << "Child:" << endl;
        cout << "PID=" << myid << endl;
        cout << "Parent_ID=" << ppid << endl;
        cout << "value=" << value << endl;
        sleep(5);
    }
    else {
        perror("fork");
        return 1;
    }
    return 0;
}

运行结果
注意fork之后父进程和子进程谁先执行不太好说,我这里故意让子进程睡一会,所以看到子进程的输出在后面。另外可以看到,在父进程改变了变量值后,子进程中该变量的值并没有变化,因为新生成的子进程对变量进行了复制。
注:单纯地说复制也较为笼统,具体机理较为复杂,此处只关注进程的操作,不具体展开。
在这里插入图片描述

这里我们再查看进程,可以看到这两个ID为6691和6692的进程。每一行用户ID右侧第一个数字为当前进程ID,右二为当前进程的父进程ID。值的一提的是这里还有我打开的两个终端,和运行的ps -ef。从中可以看到父进程诞生自一个终端进程6669,ps -ef诞生自另一个终端进程6683,两个终端进程都诞生自ID为6660的进程。

在这里插入图片描述

exit

在main内执行return语句可以正常终止进程,另外调用exit、_exit、_Exit也能正常终止进程。_exit和_Exit立即进入内核,exit先执行一些清理处理然后返回内核。exit总是先执行一个标准I/O库的清理关闭操作,即对所有流调用fclose函数,输出缓冲中的所有数据都被冲洗(写到文件上)。

#include <stdlib.h>
void exit(int status);
void _Exit(int status);
#include <unistd.h>
void _exit(int status);

3个函数都有一个status参数,称为终止状态(或退出状态)。
main函数返回一个整形值与该值调用exit等价,因此正常退出时可以exit(0)

三个函数通知其父进程其是如何终止的方式,就是将退出状态作为参数传递给函数。在异常终止时,内核产生一个指示其异常终止原因的终止状态。终止进程的父进程能用waitwaitpid函数得到其终止状态。

小插曲:孤儿进程与僵尸进程

子进程终止时将其终止状态返回给父进程,但若父进程在子进程之前终止了,子进程就变成了孤儿进程。在一个进程终止时,内核会逐个检查所有活动进程,判断其是否是正要终止进程的子进程,若是则该进程的父进程ID改为1,即让init进程收养它。此方法保证每个进程都有一个父进程。

示例
来观察一下孤儿进程。

#include <cstdio>
#include <iostream>
#include <cstdlib>
#include <cerrno>
#include <unistd.h>
using namespace std;
int main() {
    pid_t pid = fork();
    if (pid > 0) {
        cout << "Parent:" << endl;
        pid_t myid = getpid();
        cout << "PID:=" << myid << endl;
        cout << "Child_ID=" << pid << endl;
        sleep(2);
    }
    else if (pid == 0) {
        sleep(2);
        pid_t myid = getpid();
        pid_t ppid = getppid();
        cout << "Child:" << endl;
        cout << "PID=" << myid << endl;
        cout << "Parent_ID=" << ppid << endl;
        sleep(2);//保证父进程终止
        ppid = getppid();
        cout << "Parent_ID=" << ppid << endl;
    }
    else {
        perror("fork");
        return 1;
    }
    return 0;
}

这里我们的结果与我们预想的不同,子进程变为孤儿进程后被init收养,那么按理来说其父进程ID应该为1,而不是什么1586,这里我又查了一个1586进程,发现这个进程的父进程才是1。
在这里插入图片描述
1586进程:
在这里插入图片描述
查阅相关博客说是因为运行在图形化用户界面的问题,接着我换成字符界面再运行,父进程ID是1了。

在这里插入图片描述
子进程在父进程之前终止,内核为每个终止进程保存了一定量的信息,父进程可以调用waitwaitpid函数可以得到这些信息。得到的信息包括进程ID、进程的终止状态以及该进程使用的CPU总量。内核可以释放终止进程所使用的的所有存储区,关闭所有打开文件。如果父进程未能对终止的子进程进行善后处理,即获取终止进程的有关信息并释放它仍占用的资源,那么这个终止的进程即变为僵尸进程。注意由init收养的进程不会变成僵尸进程,只要有一个子进程终止,init就会调用wait获取其终止状态。

示例
来观察一下僵尸进程。

#include <cstdio>
#include <iostream>
#include <cstdlib>
#include <cerrno>
#include <unistd.h>
using namespace std;
int main(){
        pid_t pid = fork();
        if(pid > 0){
                cout<<"Parent:"<<endl;
                pid_t myid = getpid();
                cout<<"PID:="<<myid<<endl;
                cout<<"Child_ID="<<pid<<endl;
                sleep(10);//保证子进程提前结束
        }
        else if(pid == 0){
                sleep(2);
                pid_t myid = getpid();
                pid_t ppid = getppid();
                cout<<"Child:"<<endl;
                cout<<"PID="<<myid<<endl;
                cout<<"Parent_ID="<<ppid<<endl;
        }
        else{
                perror("fork");
                return 1;
        }
        return 0;
}

运行结果:
在这里插入图片描述
通过查看进程可以观察到ID为2297的进程变成僵尸进程,后跟<defunct>
在这里插入图片描述

wait和waitpid

进程终止时内核向其父进程发送SIGCHLD信号。子进程终止是异步事件,即随时可能发生,所以这种信号也是内核向父进程发的异步信号。父进程可选择忽略该信号,或者提供一个该信号发生时即被调用执行的函数(信号处理程序)。

可以使用waitwaitpid函数来处理通知。

#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);
//若成功则返回进程ID
//若出错返回0(使用waitpid并打开了WHOANG)或-1

都有的参数:

  • statloc:一个整型指针,若不为空指针则终止进程的终止状态存放在其所指向的单元内,若为空指针则不保存。

statloc没有设为空,则其指向的整型状态字保存了终止状态,可使用以下4个互斥的宏可以获取进程终止的原因

说明
WIFEXITED(statloc)若为正常终止子进程返回的状态,则为真。在这种情况下可执行WEXITSTATUS(statloc)来获取进程传递给exit或_exit的参数的低8位。
WIFSIGNALED(statloc)若为异常终止子进程返回的状态,则为真。在这种情况下可执行可执行 WTERMSIG(statloc) 获取使子进程终止的信号编号。
WIFSTOPPED(statloc)若为当前暂停子进程返回的状态,则为真。在这种情况下可执行 WSTOPSIG(statloc)获取使子进程暂停的信号编号。
WIFCONTINUED(statloc)若在作业控制暂停后已经继续的子进程返回了状态,则为真。

作业控制是什么我还不懂,这里只简单引用书上的概念。

作业控制:它运行在一个终端上启动多个作业(进程组),它控制哪一个作业可以访问该终端以及哪些作业在后台运行。

waitpid特有的参数:

  • pid:作用如下
    – pid = -1 等待任一子进程。此情况下waitpid与wait等效
    – pid > 0 等待进程ID与pid相等的子进程
    – pid = 0 等待组ID等于调用进程组ID的任一子进程
    – pid < -1 等待组ID等于pid绝对值的任一子进程

  • optionswaitpid的操作,可以置0,即阻塞,也可以是以下常量位或的结果
    WCONTINUED pid指定的子进程在停止后已继续,但其状态未报告,则返回其状态,如果不设置则函数不会理会该状态
    WNOHANG 若由pid指定的子进程并不是立即可用的,则waitpid不阻塞,立即返回 0
    WUNTRACEDpid指定的任一子进程处于停止状态,并且其状态自停止以来还未报告过,则返回其状态,如果不设置则函数不会理会该状态

对于函数的返回值,成功时都返回进程ID,失败时一般返回-1,只有用waitpid函数并在options设置了WNOHAG时,进程状态未变化则返回0。

这两个函数的差别在于:
wait是阻塞的,在子进程终止前将一直阻塞,并且任一子进程终止wait就返回;
waitpid可以是非阻塞的,并且可以指定其所等待的子进程。

示例:
waitwaitpid差异比较:
先是wait示例程序,这里设置第一个子进程睡觉3,第二个睡觉6,意味着第一个子进程先终止。同时在while循环中设置每过一个时间单位输出一个…,目的是检验wait是否阻塞。

#include <cstdio>
#include <iostream>
#include <cstdlib>
#include <cerrno>
#include <sys/wait.h>
#include <unistd.h>
using namespace std;
int main() {
    pid_t pid = fork();
    if (pid > 0) {
        pid_t pid2 = fork();
        if (pid2 > 0) {//父进程
            cout << "Parent:" << endl;
            pid_t myid = getpid();
            cout << "PID:=" << myid << endl;
            cout << "Child1_ID=" << pid << endl;
            cout << "Child2_ID=" << pid2 << endl;
            pid_t cid;
            while (1) {
                sleep(1);
                cout << "..." << endl;
                cid = wait(NULL);
                if (cid == -1) {
                    perror("wait");
                    cout << "No zombies" << endl;
                    return 1;
                }
                cout << "CHILD " << cid << " exit" << endl;
            }
        }
        else if (pid2 == 0) {//子进程2
            sleep(6);
            pid_t myid = getpid();
            pid_t ppid = getppid();
            cout << "Child2:" << endl;
            cout << "CHILD2_ID=" << myid << endl;
            cout << "Parent_ID=" << ppid << endl;
            exit(0);
        }
        else {
            perror("fork");
            return 1;
        }
    }
    else if (pid == 0) {//子进程1
        sleep(3);
        pid_t myid = getpid();
        pid_t ppid = getppid();
        cout << "Child1:" << endl;
        cout << "CHILD1_ID=" << myid << endl;
        cout << "Parent_ID=" << ppid << endl;
        exit(0);
    }
    else {
        perror("fork");
        return 1;
    }
    return 0;
}

输出结果:
从输出结果可以看到果然第一个创建的进程先被回收,另外每次回收进程都只是输出一次…,证明wait函数阻塞,从接下来的对比可以看得更清楚。
在这里插入图片描述
这里我们先不慌,先让两个创建进程的睡觉时间换一下,验证wait是任一子进程终止wait就返回。输出结果如下。
在这里插入图片描述
之后是waitpid示例程序,这里我们还是让第一个创建的进程先终止,设置waitpid函数中的pid参数为pid2,专门等待第二个进程终止,同时options设置为WNOHANG即非阻塞。我们在输出No zombies的后等待一会,方便我们查看进程。

#include <cstdio>
#include <iostream>
#include <cstdlib>
#include <cerrno>
#include <sys/wait.h>
#include <unistd.h>
using namespace std;
int main() {
    pid_t pid = fork();
    if (pid > 0) {
        pid_t pid2 = fork();
        if (pid2 > 0) {//父进程
            cout << "Parent:" << endl;
            pid_t myid = getpid();
            cout << "PID:=" << myid << endl;
            cout << "Child1_ID=" << pid << endl;
            cout << "Child2_ID=" << pid2 << endl;
            pid_t cid;
            while (1) {
                sleep(1);
                cout << "..." << endl;
                cid = waitpid(pid2, NULL, WNOHANG);
                if (cid == -1) {
                    perror("wait");
                    cout << "No zombies" << endl;
                    sleep(10);
                    return 1;
                }
                else if (cid > 0) {
                    cout << "CHILD " << cid << " exit" << endl;
                }
                else {
                    cout << "Waiting" << endl;
                }
            }
        }
        else if (pid2 == 0) {//子进程2
            sleep(6);
            pid_t myid = getpid();
            pid_t ppid = getppid();
            cout << "Child2:" << endl;
            cout << "CHILD2_ID=" << myid << endl;
            cout << "Parent_ID=" << ppid << endl;
            exit(0);
        }
        else {
            perror("fork");
            return 1;
        }
    }
    else if (pid == 0) {//子进程1
        sleep(3);
        pid_t myid = getpid();
        pid_t ppid = getppid();
        cout << "Child1:" << endl;
        cout << "CHILD1_ID=" << myid << endl;
        cout << "Parent_ID=" << ppid << endl;
        exit(0);
    }
    else {
        perror("fork");
        return 1;
    }
    return 0;
}

观察输出我们可以看到waitpid可以是非阻塞的,如果子进程还在运行就直接返回0了。注意要让waitpid非阻塞要设置WNOHANG同时通过设置waitpid函数中的pid参数为pid2,因此这里只回收了第二个创建的进程。
在这里插入图片描述

发现进程4402确实不存在,进程4401还存在且未被回收。
在这里插入图片描述

尾声:书上还有很多内容,比如wait3、wait4、exec,还有一些比较理论的东西,在此处就不进行记录了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值