进程和轻量级进程#

image-20211112165309944

在Linux内核中,进程/线程对应的数据结构是task_struct,定义在include/linux/sched.h中。

线程在Linux中的实现是 Naive POSIX Thread Library 。在内核眼中,Linux的线程实际上也是一个进程(task_struct),区别是线程的“进程”共享了地址空间、文件描述符等,称作==轻量级进程==。因此,Linux的线程也是独立的调度单元,是可以分别在不同的CPU上同时运行的。原生的Linux 线程(2.6版本内核之前)因为只实现在了用户态,所以,即使是一个多线程程序,对于内核来说只能看到一个进程,这些线程就只能在一个CPU上运行,对于多核多线程来说是很致命的。

从实现的角度,Linux的线程(LWP)是通过pthread库创建/使用的。而进程和线程的创建都调用了clone()系统调用(kernel/fork.c )。区别是两者使用了不同的flags。

进程管理#

进程状态#

  • TASK_RUNNING: The process is either executing on a CPU or waiting to be executed
  • TASK_INTERRUPTIBLE: The process is suspended (sleeping) until some condition becomes true.
  • TASK_UNINTERRUPTIBLE: Like TASK_INTERRUPTIBLE, except that delivering a signal to the sleeping process leaves its state unchanged.

PID#

PID用来区分不同的进程结构体。Linux中最大PID数目可以在/proc/sys/kernel/pid_max中查看。

每个进程/轻量级进程都分配有一个唯一的PID。但是对于同一进程中的线程来说,我们拿到的是进程ID确是相同的,这是怎么实现的呢?

Linux为了兼容POSIX标准,利用了线程组(thread group)这一概念。所有的线程都会把线程组里面第一个线程的PID存在tgid字段内。==getpid()系统调用返回的实际上是tgid的值。==

/**
 * sys_getpid - return the thread group id of the current process
 *
 * Note, despite the name, this returns the tgid not the pid.  The tgid and
 * the pid are identical unless CLONE_THREAD was specified on clone() in
 * which case the tgid is the same in all threads of the same group.
 *
 * This is SMP safe as current->tgid does not change.
 */
SYSCALL_DEFINE0(getpid)
{
    return task_tgid_vnr(current);
}

bitmap管理PID#

IDR管理PID#

  • PID: replace pid bitmap implementation with IDR API commit, commit

进程切换#

==TLDR:进程在调用schedule()方法时,将当前进程运行的寄存器信息保存在task_struct->thread_info内,同时从进程B中的task_struct->thread_info中加载B运行时的寄存器信息。==

关键函数调用链:schedule() -> context_switch(rq, prev, next) -> switch_to(prev, next, prev) -> __switch_to()

  • schedule(): kernel/sched.c
  • context_switch(): kernel/sched.c
  • switch_to(): include/asm-x86_64/system.h
  • __switch_to(): arch/x86_64/kernel/process.c
  • include/asm-x86_64/processor.h

关键结构体#

struct tss_struct#

存储了IO Permission Bitmap,进程是否允许访问某个地址或者IO端口。

TSS是per cpu的,因此,进程的hardware context不能存储在TSS内。实际上,==除了通用目的寄存器以外的大部分的寄存器信息都存储在Kernel Mode Stack内。==

进程切换流程#

进程切换分为两步:

  1. 切换Page Global Directory来使用新的地址空间;
  2. 切换Kernel Mode Stackhardware conext,包含了一个进程运行所需的所有信息。

进程创建#

内核线程#

所谓的内核线程,就是只运行在内核态的线程。相比之下,正常的线程既可以运行在用户态,也可以运行在内核态。==因为内核线程只运行在内核态,它只会使用大于PAGE_OFFSET的线性地址。正常线程会在用户态或者内核态使用全部的4GB线性地址(32位系统)==

// include/asm-x86_64/page.h

#define __PAGE_OFFSET           0xffff810000000000UL
#define PAGE_OFFSET		((unsigned long)__PAGE_OFFSET)

创建内核线程的函数create_thread()是一段汇编语言,它基本上实现了下面的调用:

do_fork(flags|CLONE_VM|CLONE_UNTRACED, 0, pregs, 0, NULL, NULL);

CLONE_VM避免拷贝调用进程的page tables,因为内核线程不使用用户态的内存空间,所以拷贝page tables纯属浪费时间和空间。CLONE_UNTRACED保证内核线程不被其它进程追踪,即使调用进程已经被追踪了。

pregs参数用来给新的内核线程找到

0号进程#

多核系统中,每个CPU都有一个0号进程。