大家好,我是程栩,一个专注于性能的大厂程序员,分享包括但不限于计算机体系结构、性能优化、云原生的知识。
引
在阅读perf
源码的过程中,发现有诸多地方不太了解。思索再三,还是考虑先阅读一些Linux相关的资料。本文是《Linux内核设计与实现》系列阅读的第一篇文章。在阅读的过程中,笔者尽量的在最新的源码中寻找对应,从而帮助理解。笔者基于v6.3
内核进行相关的代码阅读,在引用源码的地方会通过注释标出代码位置,例如L737
是指737行。
程序、进程与线程
程序员们每天都在写程序,程序就是平常我们写的代码。但是这些代码不会写完就自己跑起来,我们需要将它们运行。为了能够帮助管理这些运行的程序们,我们抽象出了进程的概念。进程自然需要知道运行的程序是什么,还需要记录当前进程运行的状态、打开的文件、当前运行的地址等内容。简单的说,进程就是正在执行的程序代码的实时结果。
在一个高速计算机中,我们需要经常进行进程的调度,那么一个进程不运行的时候,我们就需要保存一下当前运行到哪了,这样在下次调度到CPU上执行的时候才能接着运行。可是有些资源并不是动态资源,而是静态资源,例如我们的程序代码。如果每一次我们都对进程做切换的话,那么我们就需要带着静态资源一起,相对而言开销会大一些。所以我们不妨将这些动态资源抽取出来,作为一个新的实体,这样我们调度的时候就只需要调度这些新的实体,相对而言开销会小一些。这就是我们所说的线程。每一个线程都拥有独立的程序计数器、进程栈和一组进程寄存器。「通常来说,进程是资源分配的最小单位,线程是调度的最小单位。」
现代操作系统提供了两种虚拟机制:虚拟处理器和虚拟内存。虚拟处理器让进程觉得自己是独占了当前的处理器,尽管当它被调度走的时候别的进程会被调度到该处理器上运行。
进程描述符
那么,我们如何在操作系统中表示一个进程呢?一个进程又会记录哪些信息呢?
在Linux中,内核把进程列表放在一个双向循环链表中,该链表的每一项都是一个task_struct
结构的数据,我们将其称之为进程描述符。该结构定义在include/linux/sched.h
文件中,而且这个结构体有非常多的属性,在《Linux内核设计与实现》一书中提到,在当时的task_struct
结构体就有1.7KB大小,现在的内核只增不减。除了链表,我们也可以用一个静态数组来存放这些数据,例如在xv6
中就以这种方式实现进程数据的管理。
我们在这里挑选一些有代表性的属性进行说明:
代码语言:javascript复制//include/linux/sched.h L737
struct task_struct {
unsigned int __state;
struct thread_info thread_info;
/* Real parent process: */
struct task_struct __rcu *real_parent;
/* Recipient of SIGCHLD, wait4() reports: */
struct task_struct __rcu *parent;
/*
* Children/sibling form the list of natural children:
*/
struct list_head children;
struct list_head sibling;
#ifdef CONFIG_PERF_EVENTS
struct perf_event_context *perf_event_ctxp;
struct mutex perf_event_mutex;
struct list_head perf_event_list;
#endif
pid_t pid;
pid_t tgid;
}
__state
属性表示着该进程的运行状态,比如我们在操作系统中经常能见到的运行态、就绪态等等。这里在属性前面加__
是为了表示这是一个内部变量。在include/linux/sched.h
中,也定义了非常多进程的状态:
//include/linux/sched.h L84
/* Used in tsk->state: */
#define TASK_RUNNING 0x00000000
#define TASK_INTERRUPTIBLE 0x00000001
#define TASK_UNINTERRUPTIBLE 0x00000002
#define __TASK_STOPPED 0x00000004
#define __TASK_TRACED 0x00000008
/* Used in tsk->exit_state: */
#define EXIT_DEAD 0x00000010
#define EXIT_ZOMBIE 0x00000020
#define EXIT_TRACE (EXIT_ZOMBIE | EXIT_DEAD)
/* Used in tsk->state again: */
#define TASK_PARKED 0x00000040
#define TASK_DEAD 0x00000080
#define TASK_WAKEKILL 0x00000100
#define TASK_WAKING 0x00000200
#define TASK_NOLOAD 0x00000400
#define TASK_NEW 0x00000800
#define TASK_RTLOCK_WAIT 0x00001000
#define TASK_FREEZABLE 0x00002000
#define __TASK_FREEZABLE_UNSAFE (0x00004000 * IS_ENABLED(CONFIG_LOCKDEP))
#define TASK_FROZEN 0x00008000
#define TASK_STATE_MAX 0x00010000
在注释中提到,这些通过掩码的方式来定义了进程的状态,例如运行态、可中断状态、不可中断状态等。在这里我们不对具体的状态细节进行深究。
此外,还通过这些状态的组合,定义了更多新的状态:
代码语言:javascript复制//include/linux/sched.h L 109
/*
* DO NOT ADD ANY NEW USERS !
*/
#define TASK_FREEZABLE_UNSAFE (TASK_FREEZABLE | __TASK_FREEZABLE_UNSAFE)
/* Convenience macros for the sake of set_current_state: */
#define TASK_KILLABLE (TASK_WAKEKILL | TASK_UNINTERRUPTIBLE)
#define TASK_STOPPED (TASK_WAKEKILL | __TASK_STOPPED)
我们可以通过set_task_state
或set_current_state
来调整进程的状态:
//include/linux/sched.h L 175
/*
* set_current_state() includes a barrier so that the write of current->state
* is correctly serialised wrt the caller's subsequent test of whether to
* actually sleep:
*
* for (;;) {
* set_current_state(TASK_UNINTERRUPTIBLE);
* if (CONDITION)
* break;
*
* schedule();
* }
* __set_current_state(TASK_RUNNING);
*
* If the caller does not need such serialisation (because, for instance, the
* CONDITION test and condition change and wakeup are under the same lock) then
* use __set_current_state().
*
* The above is typically ordered against the wakeup, which does:
*
* CONDITION = 1;
* wake_up_state(p, TASK_UNINTERRUPTIBLE);
*
* where wake_up_state()/try_to_wake_up() executes a full memory barrier before
* accessing p->state.
*
* Wakeup will do: if (@state & p->state) p->state = TASK_RUNNING, that is,
* once it observes the TASK_UNINTERRUPTIBLE store the waking CPU can issue a
* TASK_RUNNING store which can collide with __set_current_state(TASK_RUNNING).
*
* However, with slightly different timing the wakeup TASK_RUNNING store can
* also collide with the TASK_UNINTERRUPTIBLE store. Losing that store is not
* a problem either because that will result in one extra go around the loop
* and our @cond test will save the day.
*
* Also see the comments of try_to_wake_up().
*/
#define __set_current_state(state_value)
do {
debug_normal_state_change((state_value));
WRITE_ONCE(current->__state, (state_value));
} while (0)
#define set_current_state(state_value)
do {
debug_normal_state_change((state_value));
smp_store_mb(current->__state, (state_value));
} while (0)
正如注释中提到的那样,必要的时候该函数会设置内存屏障来强制其他处理器进行重排从而设置进程成为指定状态。
thread_info
是用来记录一些进程的低级别信息的,也会用来记录task_struct
的地址。在2。6以前的内核中,进程的task_struct
结构体会存放在内核栈的尾端,这样对于寄存器较少的硬件体系结构只需要有栈指针就可以快速找到它的位置。但是在后续的版本中,通常会将thread_info
存储在栈底(向下增长栈)或栈顶(向上增长栈),并由它指向task_struct
。以mips
体系结构的实现为例:
// arch/mips/include/asm/thread_info.h L18
/*
* low level task data that entry.S needs immediate access to
* - this struct should fit entirely inside of one cache line
* - this struct shares the supervisor stack pages
* - if the contents of this structure are changed, the assembly constants
* must also be changed
*/
struct thread_info {
struct task_struct *task; /* main task structure */
unsigned long flags; /* low level flags */
unsigned long tp_value; /* thread pointer */
__u32 cpu; /* current CPU */
int preempt_count; /* 0 => preemptable, <0 => BUG */
struct pt_regs *regs;
long syscall; /* syscall number */
};
可以看到该结构体存储了一个指向task_struct
的指针,我们可以由它来快速的找到task_struct
。
值得注意的是,笔者在arch/x86/include/asm/thread_info.h
中未找到类似的结构体,只有:
//arch/x86/include/asm/thread_info.h L56
struct thread_info {
unsigned long flags; /* low level flags */
unsigned long syscall_work; /* SYSCALL_WORK_ flags */
u32 status; /* thread synchronous flags */
#ifdef CONFIG_SMP
u32 cpu; /* current CPU */
#endif
};
在书中提到,由于体系结构的不同,通过thread_info
去获取task_struct
的方式也各不相同,例如在mips
中:
// arch/mips/include/asm/thread_info.h L67
static inline struct thread_info *current_thread_info(void)
{
return __current_thread_info;
}
直接将thread_info
的结构体返回即可,这是因为其有专门的寄存器进行存储;但是在x86
中就没有相关的寄存器,所以其只能通过计算偏移间接的获取task_struct
地址。thread_info
和current_thread_info
都是基于特定的体系结构的,所以会有各种不同的实现。
所谓不忘初心方得始终,我们在task_struct
中看到了有关perf
的结构体成员:
// include/linux/sched.h L1239
#ifdef CONFIG_PERF_EVENTS
struct perf_event_context *perf_event_ctxp;
struct mutex perf_event_mutex;
struct list_head perf_event_list;
#endif
在这里我们可以看到一个perf_event_list
,笔者在这里猜测就是由这个属性来保存该进程相关的事件信息的,具体的实现我们后续再研究。
real_parent
、parent
、children
和sibling
则是用来表示一个进程相关的其他进程的信息。real_parent
就是其父进程的指针,因为我们的操作系统的全部进程都可以看成是由init
进程不断的fork
的结果,所以整体上来说进程是一个树的结构,也就自然有了树的相关概念。real_parent
和parent
的类型是task_struct __rcu
,这是一个指向task_struct
的指针,__rcu
表示这个指针适用Read-Copy Update
机制,也即写者进行操作的时候不直接修改原始数据,而是修改一个副本,只有当所有的读者都完成读以后,才将数据写入,这种机制适用于多读和少写的情况。children
和sibling
则是list_head
结构的,这是一个Linux中重要的结构体:
// include/linux/types.h L178
struct list_head {
struct list_head *next, *prev;
};
可以看到这是一个记录了前一个指针和下一个指针的结构体。而从定义中我们会发现,这个链表并不包含具体的数据,而只有前后指针的相关信息,那内核是如何获取到整个结构体的数据的呢?我们明天在进行相关的介绍。
小结
今天我们基于《Linux内核设计与实现》与内核源码了解了一些进程管理的皮毛,后面会接着进行相关系列的更新,从而加深对Linux的理解。今日小结:
小结