Linux进程管理、进程创建、线程实现、僵尸进程

目录

概述

在 Linux 系统中,通过复制一个现有的进程来创建一个全新的进程。调用 fork() 的进程称为父进程,新产生的进程称为子进程。调用结束时,在返回点这个相同位置上,父进程恢复执行,子进程开始执行。fork() 系统调用从内核返回两次:一次回到父进程,另一次回到新产生的子进程。

通常,创建新的进程都是为了执行不同的程序,接着调用exec()这组函数就可创建新地址空间,并将新程序载入其中。fork()底层是由clone()系统调用实现的。

最终,通过 exit() 系统调用推出执行,这个函数终结进程并释放资源。父进程可通过 wait4() 系统调用查询子进程是否终结(这使得父进程拥有等待特定进程的能力)。进程退出设置为僵死状态,直到其父进程调用 wait() 或 waitpid() 为止。

进程描述符及任务结构

每个进程都是一个 task_struct 结构体,内核使用双向链表 task_list 来存储。task_struct 包含的数据用来描述进程:打开的文件、地址空间、挂起信号、进程状态及其他信息。

分配进程描述符

Linux 通过 slab 分配器分配 task_struct,这样能达到对象复用和缓存着色(cache coloring) 的目的。slab 分配器在内核栈底创建新的结构 thread_info,该结构含指向 task_struct 的指针。

struct thread_info {
    struct task_struct    *task;
    struct exec_domain    *exec_domain;
    __u32                 flags;
    __u32                 status;
    __u32                 cpu;
    int                   preempt_count;
    mm_segment_t          addr_limit;
    struct restart_block  restart_block;
    void                  *sysenter_return;
    int                   uaccess_err;
};


效果如图:
下过

进程描述符的存放

进程描述符 pid 默认最大值被设置为 32768。
系统管理员修改可通过/proc/sys/kernel/pid_max 来提高上限。

进程状态

task_struct 的 state 域描述进程状态。进程五状态如下:

  • TASK_RUNNING:就绪或者运行态。
  • TASK_INTERRUPTIBLE: 进程被阻塞或者睡眠。条件达成或收到信号可进入运行态。
  • TASK_UNINTERRUPTIBLE:也是阻塞或者睡眠,只有条件达成响应,对信号或中断不响应。
  • __TASK_TRACED: 被其他进程跟踪的进程,如 pstrace 对调试程序跟踪。
  • __TASK_STOPPPED:停止执行。通常收到 SIGSTOP 等信号进入此状态。


状态转化如图:
这里写图片描述

进程家族树

所有进程都是 pid 为 1 的 init 进程后代。
task_struct 中有指向父进程 task_struct 的 parent 指针,还包含一个称为 children 的子进程链表。

进程创建

首先使用 fork() 拷贝当前进程创建一个子进程。子进程与父进程唯一区别仅为 pid,ppid 及某些资源的统计量(例如,挂起的信号)。exec() 函数负责读取可执行文件并将其载入地址空间开始运行。

写时拷贝

为避免把所有资源都复制给新进程的效率浪费,fork() 采用 copy-on-write 页实现。fork() 的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符。一般情况下,进程创建后马上执行可执行文件,这种优化可避免拷贝大量根本就不会使用的数据。

fork()

fork()、vfork() 和 __clone() 库函数都通过相应参数标志调用 clone(),然后clone()调用 do_fork(),do_fork() 调用 copy_process() 函数。
该函数之中为新进程创建内核栈、task_info、task_struct,并清零 task_struct 某些成员(主要是统计信息),大多数数据依然未修改。子进程设置为 TASK_UNINTERRUPTIBLE 确保不会投入运行。分配 pid,根据传递给 clone() 的参数标志,选择拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。最后返回一个子进程的指针。

vfork()

vfork() 与 fork() 的唯一区别是不拷贝父进程的页表项。子进程作为父进程一个单独的线程在它的地址空间运行,父进程被阻塞,直到子进程退出或执行 exec()。子进程不能像地址空间写入。

线程实现

创建线程

创建线程和创建进程只是调用clone()时参数标志不一样:

    clone(CLONE_VM | CLONE_FD | CLONE_FILES | CLONE_SIGHAND, 0);

参数就是共享的资源。由上可知,线程和父进程共享虚拟空间(VM)、文件系统资源、文件描述符和信号处理程序,而fork()的实现是:

    clone(SIGCHLD, 0);

内核线程

内核线程和普通线程区别在于没有独立的地址空间,只在内核空间运行。内核进程和普通进程一样,可以被调度,也可以被抢占。

进程终结

由 do_exit() 函数负责。进程终结会设置相关 task_struct 成员,删除相关内核定时器,输出记账信息,释放其地址空间(如果没有共享),减少相关资源引用计数,调用 exit_notify() 向父进程发送信号,给自己的子进程寻找养父,养父为线程组的其他进程或为 init 进程,并把 state 设置为 EXIT_ZOMBIE 状态。do_exit()函数再调用 schedule() 切换到新的进程,而自己成为僵尸进程不会再被调度,所以这是进程执行的最后一段代码。do_exit() 永不返回。至此进程占用的内存仅剩内核栈、thread_info 结构和 task_struct 结构。此时进程还存在的唯一目的就是向其父进程提供信息,父进程检索到信息后,或者通知内核那是无关的信息后,内核释放进程剩余内存。

删除进程描述符

在调用 do_exit() 后,尽管进程成为僵尸进程,但是系统还保留了它的进程描述符。这样可让系统有办法在该进程终结后仍能获得它的信息。因此,进程终结时所需的清理工作和进程描述符的删除分开执行。在该进程的父进程获知该进程的信息后,或者父进程通知内核它并不关注那些信息后,它的子进程(当前僵尸进程)的 task_struct 结构被释放。

如何处理僵尸进程

1.使用父进程调用 wait() 或 waitpid() 函数等待子进程,父进程阻塞。

2.异步方式,安装 signal_handler,捕获SIGCHILD信号,在信号处理函数中调用wait()函数等待。但是由于信号不支持排队,为避免信号高发时丢失信号,在处理函数中应使用 waitpid() 函数的非阻塞版本,循环对每一个子进程进行 wait 处理。方法如下:

    while(pid = waitpid(-1, &status, WNOHANG)) > 0){
        //printf("child %d exit\n", pid);
    }

参数设置为-1是因为:

-1 meaning wait for any child process.
WNOHANG 则是非阻塞模式标志。

3.直接使用 sigaction 结构体,当中的选项设置 SA_NOCLDWAIT,这样就不需要任何处理,告诉内核直接终结子进程,不要进入僵尸状态。

    struct sigaction act;
    act.sa_flags = SA_NOCLDWAIT;
    sigaction(SIGCHLD, &act, NULL);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值