关于线程:Linux内核-进程管理

32次阅读

共计 5065 个字符,预计需要花费 13 分钟才能阅读完成。

1. 过程和线程

1.1 定义

过程是处于运行状态的程序和相干资源的总称,是资源分配的最小单位。

线程是过程的外部的一个执行序列,是 CPU 调度的最小单位。

  • 有一段 可执行程序代码
  • 有一段过程专用的 零碎堆栈空间 零碎空间堆栈
  • 过程描述符,用于形容过程的相干信息。
  • 独立的存储空间 ,也就是专有的用户空间,相应的又会有 用户空间堆栈

Linux 零碎对于线程实现十分非凡,他并不辨别线程和过程,线程只是一种非凡的过程罢了。从下面四点因素来看,领有前三点而缺第四点因素的就是线程,如果齐全没有第四点的用户空间,那就是零碎线程,如果是共享用户空间,那就是用户线程。

1.2 次要区别

过程作为分配资源的根本单位,而把线程作为独立运行和独立调度的根本单位,因为线程比过程更小,基本上不领有系统资源,故对它的调度所付出的开销就会小得多,能更高效的进步零碎多个程序间并发执行的水平。

过程和线程的次要差异在于它们是不同的操作系统资源管理形式。过程有独立的地址空间,一个过程解体后,在保护模式下不会对其它过程产生影响,而线程只是一个过程中的不同执行门路。线程有本人的堆栈和局部变量,但线程之间没有独自的地址空间,一个线程死掉就等于整个过程死掉,所以多过程的程序要比多线程的程序强壮,但在过程切换时,消耗资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用过程。

总结:linux 中,过程和线程惟一区别是有没有独立的地址空间。

2. 过程描述符及工作构造

32 位机器上,大概有 1.7KB,过程描述符残缺形容一个正在执行的过程的所有信息。

工作队列(双向循环链表)

过程描述符 struct task_struct(源代码 | linnux/sched.h | v5.4)

struct task_struct {
    volatile long state;    // - 1 为不可运行, 0 为可运行, >0 为已中断
    int lock_depth;        // 锁的深度
    unsigned int policy; // 调度策略:个别有 FIFO,RR,CFS
    pid_t pid;   // 过程标识符, 用来代表一个过程
    struct task_struct *parent;    // 父过程
    struct list_head children;    // 子过程
    struct list_head sibling;   // 兄弟过程
}

2.1 调配过程描述符

2.1.1 slab 分配器

linux 采纳 slab 分配器调配 task_struct 构造

目标:对象复用和缓存着色。

slab 分配器动静生成 task_struct,只需在栈底(绝对于向下增长的栈)或栈顶(绝对于向上增长的栈)创立一个新构造 struct thread_info。

2.1.2 过程描述符寄存

PID 最大值默认为 32768(short int 短整形的最大值 <linux/threads.h>)可通过批改 /proc/sys/kernel/pid_max 进步下限。

current 宏查找以后正在运行过程的过程描述符。

x86 零碎中,current 把栈指针后 13 个无效位屏蔽掉,用来计算出 thread_info 的偏移。

current_thread_info 函数

movl $-8192,%eax
andl %esp,%eax

2.1.3 过程状态

  • TASK_RUNNING:1. 正在执行 2. 在运行队列中期待执行
  • TASK_INTERRUPTIBLE:阻塞(可中断)
  • TASK_UNINTERRUPTIBLE:阻塞(不可中断)
  • \_\_TASK_TRACED:被其余过程跟踪的过程
  • \_\_TASK_STOPPED:过程进行

陷入内核执行

  1. 零碎调用
  2. 异样处理程序

2.1.4 过程家族树

init 过程

  • 所有过程都是 PID 为 1 的 init 过程的后辈
  • 内核在系统启动的最初阶段启动 init 过程。

init 过程目标:读取零碎的初始化脚本,并执行其余的相干程序,最终实现系统启动的整个过程。

task_struct 中记录父子过程

  1. parent 指针(指向父过程)
  2. children 子过程链表

3. 过程创立

其余操作系统提供产生(spawn)过程机制,首先在新地址空间里创立过程,读入可执行文件,最初开始执行。

UNIX 将上述机制流程分成两步 fork()和 exec()

  • fork()拷贝以后过程创立一个子过程
  • exec()负责读取可执行文件,并将其入地址空间

3.1 写时拷贝(copy-on-write)

使地址空间上的页的拷贝推延到理论产生写入的时候才进行。

原理:如果有过程试图批改一个页,就会产生一个缺页中断。内核解决缺页中断的形式就是对该页进行一次通明复制。这时会革除页面的 COW 属性,示意着它不再被共享。

3.2 fork()函数

fork()的理论开销就是复制父过程的页表以及给子过程创立惟一的过程描述符。

在当初 linux 内核中,fork()实际上是由 clone()零碎调用实现的

3.2.1 copy_process()函数

  1. dup_task_struct()为新过程创立一个内核栈,thread_info 构造和 task_struct 与以后过程雷同。父子过程描述符是完全相同的。(调配空间)
  2. 查看并确保新创建这个过程后,以后用户所领有的过程数目没有超出给它调配的资源的限度。(查看边界)
  3. 子过程与父过程区别开。过程描述符的许多成员都要被清 0 或设初始值,那些不是继承来的过程描述符的成员,次要是统计信息。task_struct 中的大多数数据都仍然未被批改。(子过程初始化)
  4. 子过程的状态被设置为 TASK_UNINTERRUPTIBLE(不可中断,阻塞状态),以保障它不会投入运行。(设置子过程状态)
  5. copy_process()调用 copy_flags()以更新 task_struct 的 flags 成员。(设置标记位)

    • 表明过程是否领有超级用户权限的 PF_SUPERPRIV 标记被清 0
    • 表明过程还没有调用 exec()函数的 PF_FORKNOEXEC 标记被设置
  6. 调用 alloc_pid()为新过程调配一个无效的 PID。(为子过程调配 pid)
  7. 依据传递给 clone()的参数,copy_process()拷贝或共享关上的文件、文件系统信息、信号处理函数、过程地址空间和命名空间等。个别状况下,这些资源会被给定的过程的所有线程共享;否则,这些资源对每个过程是不同的,因而被拷贝到这里。(将资源参数标记赋值给构造体)
  8. copy_process()做开头工作并返回一个指向子过程的指针,再回到 do_fork()函数,如果 copy_process()函数胜利返回,新创建的子过程被唤醒并让其投入运行。(返回子过程指针,并唤醒子过程执行)

注:内核无意让子过程先执行,并非总能如此,因为个别子过程都会马上调用 exec()函数,这样能够防止写时拷贝的额定开销。因为父过程先执行,可能往地址空间写入。

3.3 vfork 函数

vfork()和 fork()区别:vfork()不拷贝父过程的页表项。

vfork():子过程作为父过程的一个独自线程在它的地址空间里运行,父过程被阻塞,直到子过程退出或执行 exec(),子过程不能向地址空间写入。

4. 线程创立

线程创立和过程创立基本一致,通过调用 clone()函数传递的参数标记,指明须要共享的资源。

创立线程

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

// CLONE_VM : 地址空间
// CLONE_FS : 文件系统
// CLONE_FILES : 文件描述符
// CLONE_SIGHAND : 信号处理程序及被阻断的信号

创立过程(等同 fork()函数)

clone(SIGCHLD,0);

创立过程(等同 vfork()函数)

clone(CLONE_VFORK | CLONE_VM | SIGCHLD, 0)

4.1 内核线程

内核线程只在内核空间执行,从不切换到用户空间。

内核线程和一般过程的区别:内核线程没有独立的地址空间。(task_struct 的 mm 指针被设置为 NULL)

内核线程只能由其余内核线程创立,通过 kthreadd 内核线程衍生出所有新的内核线程。(kthreadd 是所有内核线程的祖宗)

4.1.1 kthreadd 内核线程

kthreadd 内核线程是在内核初始化时被创立,循环执行 kthreadd 函数,它的作用是治理调度其它的内核线程。

kthreadd 函数的作用是运行 kthread_create_list 全局链表中保护的 kthread。能够调用 kthread_create 函数创立一个kthread,它会被退出到kthread_create_list 链表中,同时 kthread_create 函数会唤醒 kthreadd_task。kthreadd 在执行 kthread 会调用老的接口,kthreadd 内核线程在运行 kthread 时,会调用老接口 kernel_thread,它会运行一个名为“kthread”的内核线程,去运行创立 kthread,被执行的 kthread 会从 kthread_create_list 链表中删除,并且 kthreadd 会一直地调用 scheduler 让出 CPU,这个线程不能敞开。

创立内核线程,不运行

kthread_create 函数(源代码 | linux/kthread.h | v5.4)是通过 clone()零碎调用,创立一个内核线程,但新创建的线程处于不可运行状态。

kthread_create(threadfn, data, namefmt, arg...)

创立内核线程,并运行

kthread_run 函数(源代码 | linux/kthread.h | v5.4),通过调用 kthread_create 函数创立内核线程,而后调用 wake_up_process()进行唤醒。

#define kthread_run(threadfn, data, namefmt, ...)               \
({                                       \
    struct task_struct *__k                           \
        = kthread_create(threadfn, data, namefmt, ## __VA_ARGS__); \
    if (!IS_ERR(__k))                           \
        wake_up_process(__k);                       \
    __k;                                   \
})

内核线程进行

int kthread_stop(struct task_struct *k);

5. 过程终结

开释所占用的资源,并告知父过程。

一般来说,过程的析构是本身引起的,它产生在过程调用 exit()零碎调用的时候。

既能够显式地调用 exit()这个零碎调用,也能够隐性地从某个程序的主函数返回。(C 语言编辑器会在 main()函数的返回点前面搁置调用 exit 代码)

终结的工作大部分都靠 do_exit()(<kernel/exit.c>)

5.1 do_exit()函数

  1. 将 task_struct 中标记成员设置成 PF_EXITING
  2. 调用 del_timer_sync()删除任一内核定时器。确保没有定时器在排队,也没有定时器处理程序在运行。
  3. 如果 BSD 的记账性能是开启的,do_exit()调用 acct_update_integrals()来输入记账信息。
  4. 调用 exit_mm()函数开释过程占用的 mm_struct,如果没有别的过程同时应用它们(也就是说,这个地址空间没有被共享),就彻底开释它们。
  5. 调用 sem__exit()函数,如果过程排队期待 IPC 信号,它则来到队列。
  6. 调用 exit_files()和 exit_fs()别离递加文件描述符,文件系统数据援用计数,如果其中某个援用计数的数值降为零,那就不必代表没有过程在应用相应的资源,此时能够开释。
  7. 把寄存在 task_struct 的 exit_code()成员中的工作退出代码置为由 exit()提供的退出代码,或者去实现任何其余有内核机制规定的退出动作。退出代码寄存在这里供父过程随时检索。
  8. 调用 exit_notify 向父过程发送信号,给子过程从新找养父(其余线程或 init 过程),并将寄存在 task_struct 构造中的 exit_state 设置为 EXIT_ZOMBIE。
  9. do_exit 调用 schedule()切换到新的过程,因为处于 EXIT_ZOMBIE 状态的过程不会被调度,所以这是过程所执行的最初一段代码,do_exit()永不返回。

5.2 wait 族函数

wait 族函数都是通过惟一但很简单的一个零碎调用 wait4()来实现的,挂起调用它的过程,直到其中的一个子过程退出,此时函数会返回子过程的 PID。此外,调用此函数时提供的指针会蕴含子函数的退出代码。

作者:世至其美

更多博客文章:https://hqber.com

正文完
 0