和文件描述符相似,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 中查找,如果查找到的不是二进制文件,就将其视为脚本语言。

当 exec 成功后,直接执行相关二进制文件的内容,否则返回 -1 并置 errno

exec 的常见错误为:

错误字符串

原因

Bad address

参数列表最后一个不是空指针

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

进程退出

进程退出的原因不外乎两种:

  • 进程自己退出

  • 进程收到系统信号后退出

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

  • 在 main 函数中调用 return 0

  • 在函数任意位置调用 exit

  • 调用 _exit 或者 _Exit

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

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

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

进程异常退出的原因可能是:

  • 进程读写了已经关闭的文件,因为 SIGPIPE 信号而退出

  • 段错误

  • 被其它进程 kill

  • 进程缺少需要的动态库或符号而退出

waitpid

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

下面是 waitpid 的函数签名:

pid_t waitpid(pid_t pid, int *wstatus, int options);
  • pid 的参数有四种含义:

    含义

    < -1

    等待进程组 id 为 pid 绝对值的所有组内进程

    -1

    等待任意子进程

    0

    等待进程组 id 为 pid 的所有组内进程

    > 0

    等待进程 id 为 pid 的子进程

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

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

    何时为真

    WIFEXITED(wstatus)

    进程正常终止

    WEXITSTATUS(wstatus)

    进程异常终止

    WIFSIGNALED(wstatus)

    进程暂停

    WTERMSIG(wstatus)

    进程因信号终止

  • 参数 options 为零或以下常量:

    常量含义

    WNOHANG

    非阻塞模式

    WUNTRACED

    子进程暂停时也返回。但结束时不理会

    WCONTINUED

    子进程恢复执行时也返回

守护进程

要创建一个守护进程,遵循以下步骤:

  1. 调用 umask(0) 将文件模式创建屏蔽字设置为 0。以防止父进程影响此进程的权限

  2. 调用 fork ,然后使父进程退出

  3. 调用 sedsid 创建一个新会话

  4. 将当前工作路径更改为根路径

  5. 关闭不需要的文件描述符

  6. 某些守护进程打开 /dev/null 来忽略标准输入/输出和错误的影响。stdout 可以根据需要屏蔽,因为 systemd 会收集来自 stdout 的日志

之所以进行这些步骤,根本原因是守护程序可能从终端运行,但是终端作为一个应用软件,其生存时间是很短的,但是守护程序却需要长时间在后台运行,因此需要执行上面的步骤来断开终端连接,将自己放到后台运行

Linux 对上述步骤进程了封装,形成了 daemon 函数,进程只需要调用 daemon 函数就能将自己变成守护进程。其函数签名为:

#include <unistd.h>

/**
 *@param nochdir 是否将当前路径更改到根路径
 *@param noclose 是否将标准输入、标准输出和标准错误重定向到 /dev/null
 */
int daemon(int nochdir, int noclose);

绑定父子进程的生命周期

使用 prctl 可以让子进程接收到父进程的相关信号。例如让父子进程同时退出

prctl(PR_SET_PDEATHSIG, SIGKILL);

僵死进程

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

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

要防止出现僵尸进程,有三种办法:

  • 父进程调用 waitpid 回收子进程的资源

  • 通过两次 fork 让 init 进程变成子进程的父进程

  • 在代码中添加 signal(SIGCHLD, SIG_IGN)

其中,第二种代码的写法是:

if(fork() == 0){
   if(fork() == 0){
      // do something
   }
   exit(0);
}

第三种写法的原理如下:

当子进程的状态发生改变后产生 SIGCHLD 信号,这时父进程需要调用 wait 函数来确定发生了什么。

SIG_DFL 和 SIG_IGN 两个函数句柄可以分别用来处理 SIGCHLD 信号。

  • SIG_DFL 的默认方式是不理会这个信号,也不丢失子进程的状态。会产生僵死进程

  • SIG_IGN 的默认方式是丢弃子进程的状态,也不会产生僵尸进程

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

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

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

解决的办法也有两种:

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

  • 重启

linux线程函数 pthread_atfork 的深入理解

转自

在进行linux系统里开发时,经常会调用linux的系统函数fork来产生一个子进程,如果父子进程都没有用到pthread线程相关函数,则就不存在需要理解pthread_atfork的函数的必要。问题是有时候既要考虑多线程,又要考虑多进程,这个时候就要仔细理解pthread_atfork这个函数的作用了。

在父进程调用fork函数派生子进程的时候,如果父进程创建了pthread的互斥锁(pthread_mutex_t)对象,那么子进程将自动继承父进程中互斥锁对象,并且互斥锁的状态也会被子进程继承下来:如果父进程中已经加锁的互斥锁在子进程中也是被锁住的,如果在父进程中未加锁的互斥锁在子进程中也是未加锁的。在父进程调用fork之前所创建的pthread_mutex_t对象会在子进程中继续有效,而pthread_mutex_t对象通常是全局对象,会在父进程的任意线程中被操作(加锁或者解锁),这样就无法通过简单的方法让子进程明确知道被继承的 pthread_mutex_t对象到底有没有处于加锁状态。因此 pthread线程库就有了 pthread_atfork 这个函数,该函数的原型声明如下:

int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));

该函数通过3个不同阶段的回调函数来处理互斥锁状态。参数如下:

阶段

作用

prepare

将在fork调用创建出子进程之前被执行,它可以给父进程中的互斥锁对象明明确确上锁。这个函数是在父进程的上下文中执行的,正常使用时,我们应该在此回调函数调用 pthread_mutex_lock 来给互斥锁明明确确加锁,这个时候如果父进程中的某个线程已经调用pthread_mutex_lock给互斥锁加上了锁,则在此回调中调用 pthread_mutex_lock 将迫使父进程中调用fork的线程处于阻塞状态,直到prepare能给互斥锁对象加锁为止。

parent

是在fork调用创建出子进程之后,而fork返回之前执行,在父进程上下文中被执行。它的作用是释放所有在prepare函数中被明明确确锁住的互斥锁。

child

是在fork返回之前,在子进程上下文中被执行。和parent处理函数一样,child函数也是用于释放所有在prepare函数中被明明确确锁住的互斥锁。

函数成功返回0, 错误返回错误码。

通过这个函数可以确保子进程继承pthread_mutex_t对象处在未加锁状态。该函数的正常的用法如下:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <errno.h>

#define ERROR(err, msg) do { errno = err; perror(msg); exit(-1); } while(0)

int count = 0;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

void prepare() {
   int err;
   printf("prepare: pthread_mutex_lock ...\n");
   err = pthread_mutex_lock(&lock);
   if (err != 0) ERROR(err, "prepare: pthread_mutex_lock failed");
   printf("prepare: lock start...\n");
}

void parent() {
   int err;
   printf("parent: pthread_mutex_unlock ...\n");
   err = pthread_mutex_unlock(&lock);
   if (err != 0) ERROR(err, "parent: pthread_mutex_unlock");
}

void child() {
   int err;
   printf("child: pthread_mutex_unlock ...\n");
   err = pthread_mutex_unlock(&lock);
   if (err != 0) ERROR(err, "child: pthread_mutex_unlock");
}

void* thread_proc(void* arg) {
      while(1) {
         pthread_mutex_lock(&lock);
         count++;
         printf("parent thread:  count:%d\n",count);
         sleep(10);
         pthread_mutex_unlock(&lock);
         sleep(1);
      }
      return NULL;
}

int main(int argc,char * argv[])
{
   int err;
   pid_t pid;
   pthread_t tid;
   pthread_create(&tid, NULL, thread_proc, NULL);
   err = pthread_atfork(prepare, parent, child);
   if (err != 0) ERROR(err, "atfork");

   sleep(1);
   printf("parent is about to fork ...\n");
   pid = fork();
   if (pid < 0) ERROR(errno, "fork");
   else if (pid == 0) {
      int status;
      printf("child running\n");
      while(1) {
         pthread_mutex_lock(&lock);
         count ++;
         printf("child: count:%d\n",count);
         sleep(2);
         pthread_mutex_unlock(&lock);
         sleep(1);
      }
      exit(0);
   }

   pthread_join(tid, NULL);

   return 0;
}

编译方法(低版本GCC) :

gcc -O2 -o atfork -lpthread atfork.c

如果GCC版本比较低用-lpthread来链接pthread库,高版本的gcc例如gcc 7.40通过-lpthread编译时会出现如下错误 :

undefined reference to `pthread_atfork'

将 -lpthread 改成 -pthread 即可。

高版本GCC编译链接:

gcc -O2 -o atfork -pthread atfork.c

运行 atfork 发现 main函数会在prepare回调中等待:prepare: pthread_mutex_lock,直到获得互斥锁以后再执行子进程创建。

Last moify: 2024-11-25 09:35:02
Build time:2025-07-18 09:41:42
Powered By asphinx