首页 > 代码库 > 读书笔记-APUE第三版-(8)进程控制

读书笔记-APUE第三版-(8)进程控制

进程ID

每个进程都有一个唯一的进程ID。几个特殊进程:

  1. 0号进程是内核进程,通常是调度进程swapper。
  2. 1号进程init,是用户进程(以root权限运行/sbin/init),负责初始化。
  3. 几个重要函数:getpid(进程ID)/getppid(父进程ID)/getuid(进程真实用户ID)/geteuid(进程有效用户ID)/getgid(进程真实用户组ID)/getegid(进程有效用户组ID)。

fork/exec/wait例程

fork家族函数用于创建子进程(父子进程关系下节具体介绍),子进程往往调用exec家族函数执行新程序(fork+exec操作在有些系统中被称为spawn孵化),而wait家族函数用于获取子进程终止状态。

system函数使用/bin/sh执行命令,以下是使用fork/exec/wait实现的简单版本

#include<sys/wait.h>
#include<errno.h>
#include<unistd.h>
int system(constchar *cmdstring) /* version without signal handling */
{
    pid_t pid;
    int status;
    if (cmdstring == NULL)
        return(1); /* always a command processor withUNIX */
    if ((pid = fork()) < 0) {
        status = -1;
    } else if (pid == 0) { /* fork返回值为0,表示是在子进程中*/
        execl("/bin/sh", "sh","-c", cmdstring, (char *)0);
        _exit(127); /* execl error */
    } else { /* 在父进程中,fork返回子进程pid */
        while (waitpid(pid, &status, 0) < 0) {
            if (errno != EINTR) {
                status = -1; /* error other than EINTR fromwaitpid() */
                break;
            }
        }
    }
    return(status);
}

  1. fork函数一次调用,在父进程和自己成中两次返回,由于父进程可以fork多个子进程,所以设计成在父进程中返回子进程pid,而在子进程返回0,因为子进程可以通过getpid和getppid获取自身pid和父进程pid。常见应用场景是在网络编程中,父进程while循环监听用户请求,当接收到用户请求,fork出子进程进行处理。注:linux系统中,fork通过clone系统调用实现。
  2. waipid函数使用参数0等待指定子进程返回,wait函数家族包括wait(等待任一子进程返回)/waitpid(等待指定子进程&组返回,并能通过第三个参数设置阻塞选项)/waittid(进一步扩展,能获取导致进程终止的信号信息等)/wait3(还能够返回进程使用的资源)/wait4(其他wait函数的入口)。
  3. exec家族函数的作用是替换掉当前进程上下文(text/data/heap/stack等),执行新的程序(不会创建新进程)。exec家族函数包括execl/execlp/execle/execv/execvp/execve,各个函数主要区别在参数上,其中l表示是列表形式,v表示是指针数组形式,e表示环境变量,p表示命令参数是相对路径,会在PATH路径中搜索。

父子进程

子进程和父进程共享只读的text段,针对bss段、对、栈,现代操作系统使用COW(copy-on-write)技术,只有发生改变时,才会拷贝相应的内存页。

父子进程关系

子进程会继承父进程的大量属性,其中一些重要属性包括:真实/有效用户信息,进程组/会话信息,工作目录,环境变量,资源限制等。

父子进程最明显的区别是:子进程的tms时间统计信息被清零,子进程不会继承文件锁,未决闹钟&信号等(后续章节讨论)。

子进程和父进程返回先后顺序是不确定的,如果用户程序对父子进程执行顺序有依赖,需要自行处理,比如使用信号实现等待通知机制等。

  1. 内核为每个正在终止的进程保留了少量信息(pid,终止状态,CPU时间等),便于父进程获取其终止状态。
  2. 如果子进程在父进程之前结束,而父进程没有wait,子进程会变成僵尸进程。
  3. 如果父进程先结束,子进程的父进程会变成init进程(pid为1),所以如果要避免僵尸进程的产生,可以两次调用fork,即在子进程中再次调用fork,然后退出。这样第二个fork出来的进程由于其父进程退出,所以被init进程接管。

文件共享

子进程会dup父进程打开的文件描述符(共享文件描述符close-on-exec标记),包括标准输出、输入和错误输出。


如图,父子进程共享file tableentry,位置偏移量一致,所以要父子进程读写同一文件时要注意同步。

设置进程用户ID

之前提到,子进程会继承父进程的uid和euid(有效用户ID),可以调用setuid(setgid)修改进程用户(组)。

  1. 如果是root用户调用,会同时修改进程的uid、euid和备份euid(saved set-user-id)。登陆后,由login(root进程)设置用户ID。
  2. 非root用户只能修改euid,而且只能修改成之前的uid或者备份euid,否则出错。
  3. 如果自进程执行exec方法,而且执行程序的set-user-ID位被设置,那么euid被设置被执行程序属主ID。
  4. 备份euid复制euid。正常情况下,uid=euid=备份euid。
  5. 程序编写遵循“最小权限“模型,当且仅当程序需要高权限时,才调用setuid提升权限,操作完之后再调用setuid恢复权限。

其他进程相关函数

  1. 进程审计:acct开启进程审计功能,系统记录已终止进程的统计信息,包括用户ID,启动时间,CPU时间等。Linux系统审计记录保存在/var/log/account/pacct,需要用fread读取acct结构体信息。
  2. 进程调度:进程调整nice值来设置运行优先级(你越nice,你的优先级越低,人艰不拆。。),相关函数:nice/getpriority/setpriority
  3. 进程时间:调用clock_t times(structtms *buf )函数,其中返回值为时钟时间,而tms结构体被以下内容填充:

struct tms {
    clock_t tms_utime; /* user CPU time */
    clock_t tms_stime; /* system CPU time */
    clock_t tms_cutime; /* user CPU time, terminated children */
    clock_t tms_cstime; /* system CPU time, terminated children */
};

注意:相关时间已经通过每秒滴答数(_SC_CLK_TCK)转化成了秒数,但它是从过去任一时间开始统计的,所以其绝对值无意义。一般分别在进程开始和结束调用times,再计算之间的时间差。