美文网首页LinuxLinux学习之路
APUE读书笔记-08进程控制(1)

APUE读书笔记-08进程控制(1)

作者: QuietHeart | 来源:发表于2020-05-14 13:51 被阅读0次

    1、简介

    本章我们将讲述UNIX系统提供的进程控制相关的内容。包括新进程的创建,程序的执行,以及进程的终止。我们也会看到进程属性相关的各种ID:真实(real user)用户/组ID,有效(effective user)用户/组ID,保存(saved)用户/组ID,以及进程的基本控制(控制原语)如何影响它们。本章也谈到了解释器文件以及system函数。最后我们通过讲述大多UNIX系统提供的进程记账机制结束本章,这也使得我们从一个不同的角度来看待进程控制函数。

    译者注

    原文参考

    参考: APUE2/ch08lev1sec1.html

    2、进程标识

    每一个进程用非负数字来唯一标识(即pid),由于进程pid具有唯一性,所以也常用做其他需要确保唯一性的标识的一部分。例如一些特殊的文件名称中就含有进程pid。

    尽管进程pid是唯一的,但是它可以在进程结束之后再度被重新利用。大多数系统会在进程结束之后延迟其pid的重复利用,防止随后的进程很快采用先前结束进程的pid,导致一些错误。

    另外有一些特殊的进程:pid为0的进程是调度进程(swapper),磁盘中没有这个进程对应的可执行文件,因为它是内核的一部分,作为系统进程;pid为1的进程一般就是init程序,它在内核加载的最后阶段被调用,其对应的可执行文件在早期的版本中是 /etc/init ,后来的版本中是 /sbin/init ,这个进程用来在系统内核启动之后运行一些操作系统必须的脚本,它会读取一些例如 /etc/rc* 或者 /etc/inittab , /etc/init.d/* 系统无关的脚本,然后系统将进入特定状态,例如多用户状态等。init进程不会死掉,它是一个一般用户进程,不像swapper那样是内核里面的系统进程,init用supersuser的权限运行,后面会提到init也是所有孤儿进程的父亲进程。

    每个系统的实现也有一些自己的核心进程集合,用来提供一些服务。例如在一些unix系统中pid为2的进程是 pagedaemon,支持虚拟内存系统的页机制。

    除了PID,还有进程还有其他的标识属性,如下是获取这些属性的相关函数:

    #include <unistd.h>
    pid_t getpid(void);
    

    返回:调用进程的PID.

    pid_t getppid(void);
    

    返回:调用进程的父进程PID.

    uid_t getuid(void);
    

    返回:调用进程的真实用户ID。

    uid_t geteuid(void);
    

    返回:调用进程的有效用户ID.

    gid_t getgid(void);
    

    返回:调用进程的真实ID组。

    gid_t getegid(void);
    

    返回:调用进程的有效ID组。

    注意:这些函数都不会返回错误。

    译者注

    原文参考

    参考: APUE2/ch08lev1sec2.html

    3、fork

    (1)创建进程使用fork来实现,fork函数如下

    #include <unistd.h>
    pid_t fork(void);
    

    返回:在子进程中返回为0;父进程中返回为子进程PID,如果出错返回1(实际值一般为-1)。

    通过fork创建的新进程叫做子进程。这个fork函数调用了一次(在父进程中),但是返回了两次(在父进程中,以及新出现的子进程中)。通过返回值可知是父还是子进程。因为没有获得一个进程子进程PID的函数,所以在父进程的fork返回中会返回子进程PID,以做记录和判断等用。因为所有子进程只可能有一个父进程,所以在子进程中fork返回0,子进程可以通过getppid来获得父进程的进程ID(进程PID为0的进程是内核中的swapper所以它不可能是某个进程的子进程)。

    在调用fork之后,产生的子进程和父进程会继续在代码中fork调用的后面继续执行。子进程是父进程的一个拷贝,它拷贝了父进程的数据空间,堆和栈空间,一定要注意这是一份拷贝,父子进程不会共享这部分内存的内容。父子进程共享的部分是代码段部分。

    由于fork一般会接着一个exec函数,当前的系统实现一般不会立即执行父进程数据,堆,栈的拷贝。一般会使用一种copy-on-write(COW)的技术,也就是说,这些区域起初是父子进程共享的,一旦有进程进行了修改这些区域的操作,内核才会创建相应区域内存的一份拷贝(一般是虚拟内存的页)。

    实际上有不同种类的fork函数。本文中的四种系统都支持vfork(2)函数,这个函数在后面会提到。

    • Linux 2.4.22 提供使用clone系统调用来创建新进程。这个系统调用是一个fork的通用形式,允许调用者控制在父子进程中共享哪部分数据。
    • FreeBSD 5.2.1 提供rfork系统调用,和Linux中的clone系统调用类似,rfork是从Plan 9操作系统中继承过来的。
    • Solaris 9 提供两个线程库,一个是POSIX的,一个是Solaris threads.两种fork的动作有所不同。对于POSIX,fork创建一个只包含调用线程的进程,但是Solaris的fork创建的进程包含调用线程的进程中的所有线程的拷贝。为了提供和posix类似的fork,solaris提供了一个fork1函数,它可以创建一个只拷贝调用线程的进程。

    (2)fork的使用

    下面给出一个使用fork函数的例子:

    int     glob = 6;       /* external variable in initialized data */
    char    buf[] = "a write to stdout\n";
    
    int
    main(void)
    {
        int       var;      /* automatic variable on the stack */
        pid_t     pid;
    
        var = 88;
        if (write(STDOUT_FILENO, buf, sizeof(buf)-1) != sizeof(buf)-1)
            err_sys("write error");
        printf("before fork\n");    /* we don't flush stdout */
    
        if ((pid = fork()) < 0) {
            err_sys("fork error");
        } else if (pid == 0) {      /* child */
            glob++;                 /* modify variables */
            var++;
        } else {
            sleep(2);               /* parent */
        }
    
        printf("pid = %d, glob = %d, var = %d\n", getpid(), glob, var);
        exit(0);
    }
    

    运行这个例子,得到如下结果:

    $ ./a.out
    a write to stdout
    before fork
    pid = 430, glob = 7, var = 89      child's variables were changed
    pid = 429, glob = 6, var = 88      parent's copy was not changed
    $ ./a.out > temp.out
    $ cat temp.out
    a write to stdout
    before fork
    pid = 432, glob = 7, var = 89
    before fork
    pid = 431, glob = 6, var = 88
    

    在上面的这个例子中,给出了一个使用fork的例子,并且通过这个例子我们应该知道:

    1. 如果fork之后,是parent先运行还是children这是不确定的,取决于内核的调度算法。
    2. 子进程是父进程的拷贝,包括文件缓存等都是一个副本,所以子进程修改不会影响父进程。
    3. 由于子进程拷贝了父进程的数据空间,如果使用库函数打印,重定向时可能同一个打印语句在文件中出现两次,因为缓存也拷贝了。

    (3)文件共享

    父子进程之间共享文件偏移量,这样在父子进程同时写一个文件的时候容易控制它们之间的配合;如果不共享的话,有些定位之类的操作很难配合进行或者很麻烦。当然如果父子不同步的话,两者对文件的输出可能会相互干扰。

    一般fork之后,有两种操作文件描述符号的情况:

    1. 父进程等待子进程结束。这时候父进程不需要对文件描述符号做任何事情,在子进程结束之后子进程读写的文件描述符号的偏移会自动更新。
    2. 父子进程进行它们各自的操作。这时候,父子进程需要关闭它们不需要的文件描述符号,防止两者之间互相干扰。这在网络服务的环境下面经常会用到。

    下面的图给出了调用fork之后父子进程打开共享文件的图示情况:

    parent process table entry                  file table                v-node table
    +------------------------+ ----------->+-------------------+     ---->+-----------+
    |      fd       file     |/       +--->| file status flags |    /     |   v-node  |
    |     flags    pointer   /        |    +-------------------+   /      |information|
    |     +-----+---------+ /|        |    |current file offset|  /       +-----------+
    | fd0:|     |         |/ |        |    +-------------------+ /        |   i-node  |
    |     +-----+---------+  |        |    |   v-node pointer  |/         |information|
    | fd1:|     |         |\ |        |    +-------------------+          |...........|
    |     +-----+---------+ \|        |                                   |  current  |
    | fd2:|     |         |\ \        |                                   | file size |
    |     +-----+---------+ \|\       |                                   +-----------+
    |     |               |  \ -------+--->+-------------------+
    |     |   ......      |  |\       |  ->| file status flags |    ----->+-----------+
    |     |               |  | \      | /  +-------------------+   /      |   v-node  |
    |     +---------------+  |  \    / /   |current file offset|  /       |information|
    +------------------------+   \  / /    +-------------------+ /        +-----------+
                                  \/ /     |   v-node pointer  |/         |   i-node  |
                                  /\/      +-------------------+          |information|
                                 / /\                                     |...........|
    child  process table entry  / /  \                                    |  current  |
    +------------------------+ / /    ---->+-------------------+          | file size |
    |      fd       file     |/ / -------->| file status flags |          +-----------+
    |     flags    pointer   / / /         +-------------------+
    |     +-----+---------+ /|/ /          |current file offset|  ------->+-----------+
    | fd0:|     |         |/ / /           +-------------------+ /        |   v-node  |
    |     +-----+---------+ /|/            |   v-node pointer  |/         |information|
    | fd1:|     |         |/ /             +-------------------+          +-----------+
    |     +-----+---------+ /|                                            |   i-node  |
    | fd2:|     |         |/ |                                            |information|
    |     +-----+---------+  |                                            |...........|
    |     |               |  |                                            |  current  |
    |     |   ......      |  |                                            | file size |
    |     |               |  |                                            +-----------+
    |     +---------------+  |
    +------------------------+
    

    除了打开的文件之外,还有许多父子进程共享的内容,具体参见参考资料给出列表。

    父子进程不同的地方是:

    • fork返回值不同。
    • 进程PID不同。
    • 进程的父进程PID不同,子进程的父进程PID设置为父进程,父进程的父进程PID不变。
    • 子进程的tms_utime, tms_stime, tms_cutime, 和 tms_cstime取值设置为0.
    • 父进程设置的文件锁不会被子进程继承。
    • 在子进程中会把申请的警钟清空。
    • 子进程会把申请的信号集合清空。

    fork失败的原因有两个:

    1. 系统中进程数目太多(一般这是因为出现了什么问题而导致的)
    2. 当前用户 UID的进程数目超过了系统的限制。可以通过CHILD_MAX指定每个用户UID同时可以运行的进程的数目。

    fork 一般有两种使用的方法:

    1. 进程想要复制自己,并且在同时执行另外的代码。这在网络中很常见:服务器端监听,接收到一个客户请求,之后它fork一个子进程来处理客户请求,然后父进程继续监听其他的请求。
    2. 进程想要执行一个不同的程序:一般都是fork之后调用exec来做的。

    有些系统把2)的fork紧接这exec合并成了一个操作spawn. unix 把操作分成了两个部分,因为许多时候fork不需紧接着exec或者fork和exec之间需要做一些修改进程属性的操作等等。

    Single UNIX Specification 在高级的real-time选项组包含了spawn接口。这些接口不是fork和exec的替代。他们用来支持很难高效地执行fork的系统,尤其是那些没有内存管理硬件支持的系统。

    译者注

    原文参考

    参考: APUE2/ch08lev1sec3.html

    相关文章

      网友评论

        本文标题:APUE读书笔记-08进程控制(1)

        本文链接:https://www.haomeiwen.com/subject/oflqnhtx.html