进程控制

和文件描述符相似,Unix 使用一个非负整数来代表进程,但是当一个进程销毁后,新创建的进程一般不会立即使用刚刚销毁的进程的 ID。一般而言,进程 ID 为零的进程是调度进程,一般也叫做交换进程。进程 ID 为 1 的是 init 进程是初始化进程,在系统启动时由内核创建。 init 进程绝不会终止 ,它是运行在超级用户特权下的用户进程(而交换进程是系统进程)。

init 进程是所有进程的祖先。

fork

使用 fork 可以创建一个子进程:

pid_t fork(void);

fork 调用一次,然后分别在父进程和子进程中返回一次,不同之处在于 父进程中 fork 的返回值为子进程的 PID ,子进程中 fork 的返回值为 0。

调用 fork 后子进程和父进程拥有相同的正文段,除非父进程或子进程对某个变量进行了修改,否则内核不会拷贝这些数据,也就是写时复制技术。

此外,fork 后的子进程和父进程拥有相同的:

  • 相同的正文段

  • 相同的 IO 重定向

  • 相同的文件描述符

  • 相同的权限和用户标示

  • 相同的进程环境

  • 相同的储存映像

但是,子进程不继承父进程的:

  • 已获取的文件锁

  • 未处理的闹钟

  • 未处理的信号集

fork 失败的原因

fork 返回的失败原因只有内存不足,但是事实上造成此问题的原因并不是一定就是内存不足,真实原因有四种:

  • 内存不足

  • numa 架构下,进程启动的时候绑定了node,导致只有一个 node 里的内存在起作用

  • numa 架构下,如果所有内存都插到一个槽,其它 node 就会没内存

  • 进程数量超过限制

之所以返回的失败代码只有一种,原因是:

fork 的调用链为:fork -> do_fork -> copy_process,其中 copy_process 中的部分代码如下:

static struct task_struct *copy_process(...){
   ...
   //注意这里!!!!!!
   //申请整数形式的 pid 值
   if (pid != &init_struct_pid) {
      retval = -ENOMEM;
       pid = alloc_pid(p->nsproxy->pid_ns);
       if (!pid)
           goto bad_fork_cleanup_io;
   }
   ...
   bad_fork_cleanup_io:
      if (p->io_context)
         exit_io_context(p);
   ...
   fork_out:
    return ERR_PTR(retval);
}

可以看到:无论 alloc_pid 返回的是何种类型的失败,其错误类型都写死的返回 -ENOMEM,而 ENOMEM 代表的是 Out of memory 的意思

而 alloc_pid 的代码节选如下:

//file:kernel/pid.c
struct pid *alloc_pid(struct pid_namespace *ns){
   //第一种情况:申请 pid 内核对象失败
   pid = kmem_cache_alloc(ns->pid_cachep, GFP_KERNEL);
   if (!pid)
      goto out;

   //第二种情况:申请整数 pid 号失败
   //调用到alloc_pidmap来分配一个空闲的pid
   tmp = ns;
   pid->level = ns->level;
   for (i = ns->level; i >= 0; i--) {
      nr = alloc_pidmap(tmp);
      if (nr < 0)
         goto out_free;

      pid->numbers[i].nr = nr;
      pid->numbers[i].ns = tmp;
      tmp = tmp->parent;
   }

   ...
   out:
      return pid;
   out_free:
      goto out;
}

另外 pid 在内核中并不是一个简单的整数类型,而是一个结构体,所以内存分配时会失败

第二中情况是申请进程号的时候如果失败,也会返回错误

通过这里我们还额外学习到了另外一个知识!一个进程并不只是申请一个进程号就够了。而是通过一个 for 循环去申请了多个。

假如说当前创建的进程是一个容器中的进程,那么它至少得申请两个 PID 号才行。一个 PID 是在容器命名空间中的进程号,一个是根命名空间(宿主机)中的进程号。

这也符合我们平时的经验。在容器中的每一个进程其实我们在宿主机中也都能看到。但是在容器中看到的进程号一般是和在宿主机上看到的是不一样的。

exec

如果父进程期望子进程执行其它程序,那么就需要在子进程中执行 exec 来替换当前的内存映像:

int execl(const char *pathname, const char *arg, ... /*, (char *) NULL */);
int execlp(const char *file, const char *arg, ... /*, (char *) NULL */);
int execle(const char *pathname, const char *arg, ... /*, (char *) NULL, char *const envp[] */);
int execv(const char *pathname, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
int fexecve(int fd, char *const argv[], char *const envp[]);

可以看到,存在两个函数族,这两个函数族的分别是 execl 和 execv。后面的 l 代表 list,v 代表 vector。execl 族函数要求向程序传递参数时以列表的形式传递,并且最后一个参数设置为空指针以指示结束。execv 族函数要求将参数放到一个字符数组中。

另外,以 e 结尾的函数使用 envp 初始化程序环境,而不是使用当前环境。

pathname 和 file 都代表了程序的路径,如果不是路径,那就在 PATH 中查找,如果查找到的不是二进制文件,就将其视为脚本语言。

重要

POSIX.1 明确要求在执行 exec 时关闭打开的目录流

进程退出

一个进程正常退出有以下几种方法:

  • 在 main 函数中调用 return 0

  • 在函数任意位置调用 exit

  • 调用 _exit 或者 _Exit

  • 进程的最后一个线程在启动函数中调用 return

  • 进程的最后一个进程调用 pthread_exit

exit 和 _Exit 的不同之处是 exit 会在退出时调用使用 atexit 函数注册的清理函数,而 _Exit 则不进行处理就立即退出

waitpid

如果一个进程已经终止,但是父进程却没有执行 waitpid 函数,则此进程被称为 僵死进程 ,僵死进程使用 ps 命令打印出来的状态为 Z。僵死进程的 PID 不会被回收,如果系统中存在大量的僵死进程,可能会导致系统没法创建新进程。

如果僵死进程的父进程结束了,那么僵死进程会被 init 进程收养,然后由 init 进程执行善后工作

当子进程结束后,内核会向其父进程发送 SIGCHLD 信号,当父进程接受到 SIGCHLD 信号而调用 waitpid 函数时,函数调用会立即返回,否则父进程会被阻塞至子进程结束

waitpid 有两个形式:

pid_t wait(int *wstatus);
pid_t waitpid(pid_t pid, int *wstatus, int options);

wstatus 是一个输出指针,用来获取子进程的状态。如果对子进程状态不关心,可以设为空指针。waitpid 的 options 可以根据需要将 waitpid 设置为非阻塞的。

wstatus 需要使用宏来查看子进程的状态,这些宏以 WIF 开头。

何时为真

WIFEXITED(wstatus)

进程正常终止

WEXITSTATUS(wstatus)

进程异常终止

WIFSIGNALED(wstatus)

进程暂停

WTERMSIG(wstatus)

wait 函数会在任一子进程结束时返回,而 waitpid 根据 pid 的形式,具有以下作用:

含义

pid == -1

等待任一子进程

pid > 1

等待进程 ID 与 pid 相等的进程

pid == 0

等待与调用进程同组的任一进程

pid < -1

等待组进程等于 pid 绝对值的任一子进程

备注

在杀死进程时,一个常用的方式是使用 kill -9 发送 SIGKILL 信号给进程。但是有两种情况下此方法不适用:

  • 该进程是僵死进程。进程已经释放所有资源,但是父进程还没有释放。这种情况下要等待父进程结束

  • 该进程处于核心态,并在等待不可获得的资源,处于核心态的资源会忽略所有信号

解决的办法也有两种:

  • 通过 cat /proc/proc_id/status 查找其父进程将其杀死

  • 重启