Linux 操作系统的广袤世界里,进程管理宛如一座大厦的基石,支撑着整个系统的稳定运行与高效运转 。而task_struct结构体,无疑是进程管理这座大厦的核心支柱,它承载着进程的关键信息,贯穿于进程从诞生到消亡的整个生命周期,在进程创建、调度、通信以及资源管理等各个关键环节都发挥着不可替代的作用。
深入探究task_struct结构体,就如同开启一场探秘 Linux 内核底层运作奥秘的奇妙之旅,能够帮助我们更好地理解操作系统的运行机制,优化系统性能,解决实际应用中遇到的各种问题。无论是系统开发者、运维工程师,还是对操作系统原理充满好奇的技术爱好者,掌握task_struct结构体的相关知识,都将为我们在 Linux 领域的探索与实践提供有力的支持和坚实的保障。
下面是 task_struct 结构中一些重要字段的简要说明:
-
state: 进程状态,如运行、等待或睡眠。
-
pid: 进程ID,用于唯一标识一个进程。
-
parent: 父进程的指针。
-
children: 子进程链表的头指针。
-
sibling: 兄弟进程链表的指针。
-
mm: 内存描述符,包含了进程所拥有的地址空间信息。
-
files: 文件描述符表,保存了进程打开文件的相关信息。
-
sched_entity: 调度实体,记录了与调度相关的信息,如优先级、时间片等。
-
cred: 进程凭证,包含了与权限相关的信息,如用户ID、组ID等。
一、概述
在 Linux 内核中,无论是进程还是线程,到了内核里面,都叫做任务(Task),由统一的数据结构 task_struct 进行管理。task_struct 是 Linux 中的进程描述符,是感知进程存在的唯一实体。Linux 内核中通过一个双向循环链表将所有的 task_struct 串了起来,不同的操作系统中,PCB 所包含的内容也会不同。
2.1 任务ID
任务ID是任务的唯一标识,在tast_struct中,主要涉及以下几个ID
pid_t pid;
pid_t tgid;
struct task_struct *group_leader;
之所以有pid(process id),tgid(thread group ID)以及group_leader,是因为线程和进程在内核中是统一管理,视为相同的任务(task)。
任何一个进程,如果只有主线程,那 pid 和tgid相同,group_leader 指向自己。但是,如果一个进程创建了其他线程,那就会有所变化了。线程有自己的pid,tgid 就是进程的主线程的 pid,group_leader 指向的进程的主线程。因此根据pid和tgid是否相等我们可以判断该任务是进程还是线程。
2.2 亲缘关系
除了0号进程以外,其他进程都是有父进程的。全部进程其实就是一颗进程树,相关成员变量如下所示:
struct task_struct __rcu *real_parent; /* real parent process */
struct task_struct __rcu *parent; /* recipient of SIGCHLD, wait4() reports */
struct list_head children; /* list of my children */
struct list_head sibling; /* linkage in my parent's children list */
-
parent 指向其父进程。当它终止时,必须向它的父进程发送信号。
-
children 指向子进程链表的头部。链表中的所有元素都是它的子进程。
-
sibling 用于把当前进程插入到兄弟链表中。
通常情况下,real_parent 和 parent 是一样的,但是也会有另外的情况存在。例如,bash 创建一个进程,那进程的 parent 和 real_parent 就都是 bash。如果在 bash 上使用 GDB 来 debug 一个进程,这个时候 GDB 是 parent,bash 是这个进程的 real_parent。
2.3 任务状态
任务状态部分主要涉及以下变量
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
int exit_state;
unsigned int flags;
其中状态state通过设置比特位的方式来赋值,具体值在include/linux/sched.h中定义:
/* Used in tsk->state: */
#define TASK_RUNNING 0
#define TASK_INTERRUPTIBLE 1
#define TASK_UNINTERRUPTIBLE 2
#define __TASK_STOPPED 4
#define __TASK_TRACED 8
/* Used in tsk->exit_state: */
#define EXIT_DEAD 16
#define EXIT_ZOMBIE 32
#define EXIT_TRACE (EXIT_ZOMBIE | EXIT_DEAD)
/* Used in tsk->state again: */
#define TASK_DEAD 64
#define TASK_WAKEKILL 128
#define TASK_WAKING 256
#define TASK_PARKED 512
#define TASK_NOLOAD 1024
#define TASK_NEW 2048
#define TASK_STATE_MAX 4096#define TASK_KILLABLE (TASK_WAKEKILL | TASK_UNINTERRUPTIBLE)
TASK_RUNNING并不是说进程正在运行,而是表示进程在时刻准备运行的状态。当处于这个状态的进程获得时间片的时候,就是在运行中;如果没有获得时间片,就说明它被其他进程抢占了,在等待再次分配时间片。在运行中的进程,一旦要进行一些 I/O 操作,需要等待 I/O 完毕,这个时候会释放 CPU,进入睡眠状态。
在Linux中有两种睡眠状态:
-
一种是 TASK_INTERRUPTIBLE,可中断的睡眠状态。这是一种浅睡眠的状态,也就是说,虽然在睡眠,等待 I/O 完成,但是这个时候一个信号来的时候,进程还是要被唤醒。只不过唤醒后,不是继续刚才的操作,而是进行信号处理。当然程序员可以根据自己的意愿,来写信号处理函数,例如收到某些信号,就放弃等待这个 I/O 操作完成,直接退出;或者收到某些信息,继续等待。
-
另一种睡眠是 TASK_UNINTERRUPTIBLE,不可中断的睡眠状态。这是一种深度睡眠状态,不可被信号唤醒,只能死等 I/O 操作完成。一旦 I/O 操作因为特殊原因不能完成,这个时候,谁也叫不醒这个进程了。你可能会说,我 kill 它呢?别忘了,kill 本身也是一个信号,既然这个状态不可被信号唤醒,kill 信号也被忽略了。除非重启电脑,没有其他办法。因此,这其实是一个比较危险的事情,除非程序员极其有把握,不然还是不要设置成 TASK_UNINTERRUPTIBLE。
-
于是,我们就有了一种新的进程睡眠状态,TASK_KILLABLE,可以终止的新睡眠状态。进程处于这种状态中,它的运行原理类似 TASK_UNINTERRUPTIBLE,只不过可以响应致命信号。由于TASK_WAKEKILL 用于在接收到致命信号时唤醒进程,因此TASK_KILLABLE即在TASK_UNINTERUPTIBLE的基础上增加一个TASK_WAKEKILL标记位即可。
TASK_STOPPED是在进程接收到 SIGSTOP、SIGTTIN、SIGTSTP或者 SIGTTOU 信号之后进入该状态。
TASK_TRACED 表示进程被 debugger 等进程监视,进程执行被调试程序所停止。当一个进程被另外的进程所监视,每一个信号都会让进程进入该状态。
一旦一个进程要结束,先进入的是 EXIT_ZOMBIE 状态,但是这个时候它的父进程还没有使用wait() 等系统调用来获知它的终止信息,此时进程就成了僵尸进程。EXIT_DEAD 是进程的最终状态。EXIT_ZOMBIE 和 EXIT_DEAD 也可以用于 exit_state。
上面的进程状态和进程的运行、调度有关系,还有其他的一些状态,我们称为标志。放在 flags字段中,这些字段都被定义成为宏,以 PF 开头。
#define PF_EXITING 0x00000004
#define PF_VCPU 0x00000010
#define PF_FORKNOEXEC 0x00000040
PF_EXITING 表示正在退出。当有这个 flag 的时候,在函数 find_alive_thread() 中,找活着的线程,遇到有这个 flag 的,就直接跳过。
PF_VCPU 表示进程运行在虚拟 CPU 上。在函数 account_system_time中,统计进程的系统运行时间,如果有这个 flag,就调用 account_guest_time,按照客户机的时间进行统计。
PF_FORKNOEXEC 表示 fork 完了,还没有 exec。在 _do_fork ()函数里面调用 copy_process(),这个时候把 flag 设置为 PF_FORKNOEXEC()。当 exec 中调用了 load_elf_binary() 的时候,又把这个 flag 去掉。
2.4 任务权限
任务权限主要包括以下两个变量,real_cred是指可以操作本任务的对象,而red是指本任务可以操作的对象。
/* Objective and real subjective task credentials (COW): */
const struct cred __rcu *real_cred;
/* Effective (overridable) subjective task credentials (COW): */
const struct cred __rcu *cred;
cred定义如下所示:
struct cred {
......kuid_t uid; /* real UID of the task */kgid_t gid; /* real GID of the task */kuid_t suid; /* saved UID of the task */kgid_t sgid; /* saved GID of the task */kuid_t euid; /* effective UID of the task */kgid_t egid; /* effective GID of the task */kuid_t fsuid; /* UID for VFS ops */kgid_t fsgid; /* GID for VFS ops */
......kernel_cap_t cap_inheritable; /* caps our children can inherit */kernel_cap_t cap_permitted; /* caps we're permitted */kernel_cap_t cap_effective; /* caps we can actually use */kernel_cap_t cap_bset; /* capability bounding set */kernel_cap_t cap_ambient; /* Ambient capability set */
......
} __randomize_layout;
从这里的定义可以看出,大部分是关于用户和用户所属的用户组信息。
-
uid和 gid,注释是 real user/group id。一般情况下,谁启动的进程,就是谁的 ID。但是权限审核的时候,往往不比较这两个,也就是说不大起作用。
-
euid 和 egid,注释是 effective user/group id。一看这个名字,就知道这个是起“作用”的。当这个进程要操作消息队列、共享内存、信号量等对象的时候,其实就是在比较这个用户和组是否有权限。
-
fsuid 和fsgid,也就是 filesystem user/group id。这个是对文件操作会审核的权限。
在Linux中,我们可以通过chmod u+s program命令更改更改euid和fsuid来获取权限。
除了以用户和用户组控制权限,Linux 还有另一个机制就是 capabilities。
原来控制进程的权限,要么是高权限的 root 用户,要么是一般权限的普通用户,这时候的问题是,root 用户权限太大,而普通用户权限太小。有时候一个普通用户想做一点高权限的事情,必须给他整个 root 的权限。这个太不安全了。于是,我们引入新的机制 capabilities,用位图表示权限,在capability.h可以找到定义的权限。我这里列举几个。
#define CAP_CHOWN 0
#define CAP_KILL 5
#define CAP_NET_BIND_SERVICE 10
#define CAP_NET_RAW 13
#define CAP_SYS_MODULE 16
#define CAP_SYS_RAWIO 17
#define CAP_SYS_BOOT 22
#define CAP_SYS_TIME 25
#define CAP_AUDIT_READ 37
#define CAP_LAST_CAP CAP_AUDIT_READ
对于普通用户运行的进程,当有这个权限的时候,就能做这些操作;没有的时候,就不能做,这样粒度要小很多。
2.5 运行统计
运行统计从宏观来说也是一种状态变量,但是和任务状态不同,其存储的主要是运行时间相关的成员变量,具体如下所示
u64 utime;//用户态消耗的CPU时间
u64 stime;//内核态消耗的CPU时间
unsigned long nvcsw;//自愿(voluntary)上下文切换计数
unsigned long nivcsw;//非自愿(involuntary)上下文切换计数
u64 start_time;//进程启动时间,不包含睡眠时间
u64 real_start_time;//进程启动时间,包含睡眠时间
2.6 进程调度
进程调度部分较为复杂,会单独拆分讲解,这里先简单罗列成员变量。
//是否在运行队列上
int on_rq;
//优先级
int prio;
int static_prio;
int normal_prio;
unsigned int rt_priority;
//调度器类
const struct sched_class *sched_class;
//调度实体
struct sched_entity se;
struct sched_rt_entity rt;
struct sched_dl_entity dl;
//调度策略
unsigned int policy;
//可以使用哪些CPU
int nr_cpus_allowed;
cpumask_t cpus_allowed;
struct sched_info sched_info;
2.7 信号处理
信号处理相关的数据结构如下所示
/* Signal handlers: */
struct signal_struct *signal;
struct sighand_struct *sighand;
sigset_t blocked;
sigset_t real_blocked;
sigset_t saved_sigmask;
struct sigpending pending;
unsigned long sas_ss_sp;
size_t sas_ss_size;
unsigned int sas_ss_flags;
这里将信号分为三类:
-
阻塞暂不处理的信号(blocked)
-
等待处理的信号(pending)
-
正在通过信号处理函数处理的信号(sighand)
信号处理函数默认使用用户态的函数栈,当然也可以开辟新的栈专门用于信号处理,这就是 sas_ss_xxx 这三个变量的作用。
2.8 内存管理
内存管理部分成员变量如下所示
struct mm_struct *mm;
struct mm_struct *active_mm;
由于内存部分较为复杂,会放在后面单独介绍,这里了先不做详细说明。
2.9 文件与文件系统
文件系统部分也会在后面详细说明,这里先简单列举成员变量
/* Filesystem information: */
struct fs_struct *fs;
/* Open file information: */
struct files_struct *files;
2.10 内核栈
内核栈相关的成员变量如下所示。为了介绍清楚其作用,我们需要从为什么需要内核栈开始逐步讨论。
struct thread_info thread_info;
void *stack;
当进程产生系统调用时,会利用中断陷入内核态。而内核态中也存在着各种函数的调用,因此我们需要有内核态函数栈。Linux 给每个 task 都分配了内核栈。在 32 位系统上 arch/x86/include/asm/page_32_types.h,是这样定义的:一个 PAGE_SIZE是 4K,左移一位就是乘以 2,也就是 8K。
#define THREAD_SIZE_ORDER 1
#define THREAD_SIZE (PAGE_SIZE << THREAD_SIZE_ORDER)
内核栈在 64 位系统上 arch/x86/include/asm/page_64_types.h,是这样定义的:在 PAGE_SIZE 的基础上左移两位,也即 16K,并且要求起始地址必须是 8192 的整数倍。
#ifdef CONFIG_KASAN
#define KASAN_STACK_ORDER 1
#else
#define KASAN_STACK_ORDER 0
#endif#define THREAD_SIZE_ORDER (2 + KASAN_STACK_ORDER)
#define THREAD_SIZE (PAGE_SIZE << THREAD_SIZE_ORDER)
内核栈的结构如下所示,首先是预留的8个字节,然后是存储寄存器,最后存储thread_info结构体。
这个结构是对 task_struct 结构的补充。因为 task_struct 结构庞大但是通用,不同的体系结构就需要保存不同的东西,所以往往与体系结构有关的,都放在 thread_info 里面。在内核代码里面采用一个 union将thread_info和stack 放在一起,在 include/linux/sched.h 中定义用以表示内核栈。由代码可见,这里根据架构不同可能采用旧版的task_struct直接放在内核栈,而新版的均采用thread_info,以节约空间。
union thread_union {
#ifndef CONFIG_ARCH_TASK_STRUCT_ON_STACKstruct task_struct task;
#endif
#ifndef CONFIG_THREAD_INFO_IN_TASKstruct thread_info thread_info;
#endifunsigned long stack[THREAD_SIZE/sizeof(long)];
};
另一个结构 pt_regs,定义如下。其中,32 位和 64 位的定义不一样。
#ifdef __i386__
struct pt_regs {unsigned long bx;unsigned long cx;unsigned long dx;unsigned long si;unsigned long di;unsigned long bp;unsigned long ax;unsigned long ds;unsigned long es;unsigned long fs;unsigned long gs;unsigned long orig_ax;unsigned long ip;unsigned long cs;unsigned long flags;unsigned long sp;unsigned long ss;
};
#else
struct pt_regs {unsigned long r15;unsigned long r14;unsigned long r13;unsigned long r12;unsigned long bp;unsigned long bx;unsigned long r11;unsigned long r10;unsigned long r9;unsigned long r8;unsigned long ax;unsigned long cx;unsigned long dx;unsigned long si;unsigned long di;unsigned long orig_ax;unsigned long ip;unsigned long cs;unsigned long flags;unsigned long sp;unsigned long ss;
/* top of stack page */
};
#endif
内核栈和task_struct是可以互相查找的,而这里就需要用到task_struct中的两个内核栈相关成员变量了。
⑴通过task_struct查找内核栈
如果有一个 task_struct 的 stack 指针在手,即可通过下面的函数找到这个线程内核栈:
static inline void *task_stack_page(const struct task_struct *task)
{return task->stack;
}
从 task_struct 如何得到相应的 pt_regs 呢?我们可以通过下面的函数,先从 task_struct找到内核栈的开始位置。然后这个位置加上 THREAD_SIZE 就到了最后的位置,然后转换为 struct pt_regs,再减一,就相当于减少了一个 pt_regs 的位置,就到了这个结构的首地址。
/** TOP_OF_KERNEL_STACK_PADDING reserves 8 bytes on top of the ring0 stack.* This is necessary to guarantee that the entire "struct pt_regs"* is accessible even if the CPU haven't stored the SS/ESP registers* on the stack (interrupt gate does not save these registers* when switching to the same priv ring).* Therefore beware: accessing the ss/esp fields of the* "struct pt_regs" is possible, but they may contain the* completely wrong values.*/
#define task_pt_regs(task) \
({ \unsigned long __ptr = (unsigned long)task_stack_page(task); \__ptr += THREAD_SIZE - TOP_OF_KERNEL_STACK_PADDING; \((struct pt_regs *)__ptr) - 1; \
})
这里面有一个TOP_OF_KERNEL_STACK_PADDING,这个的定义如下:
#ifdef CONFIG_X86_32
# ifdef CONFIG_VM86
# define TOP_OF_KERNEL_STACK_PADDING 16
# else
# define TOP_OF_KERNEL_STACK_PADDING 8
# endif
#else
# define TOP_OF_KERNEL_STACK_PADDING 0
#endif
也就是说,32 位机器上是 8,其他是 0。这是为什么呢?因为压栈 pt_regs 有两种情况。我们知道,CPU 用 ring 来区分权限,从而 Linux 可以区分内核态和用户态。因此,第一种情况,我们拿涉及从用户态到内核态的变化的系统调用来说。因为涉及权限的改变,会压栈保存 SS、ESP 寄存器的,这两个寄存器共占用 8 个 byte。另一种情况是,不涉及权限的变化,就不会压栈这 8 个 byte。这样就会使得两种情况不兼容。如果没有压栈还访问,就会报错,所以还不如预留在这里,保证安全。在 64 位上,修改了这个问题,变成了定长的。
⑵通过内核栈找task_struct
首先来看看thread_info的定义吧。下面所示为早期版本的thread_info和新版本thread_info的源码
struct thread_info {struct task_struct *task; /* main task structure */__u32 flags; /* low level flags */__u32 status; /* thread synchronous flags */__u32 cpu; /* current CPU */mm_segment_t addr_limit;unsigned int sig_on_uaccess_error:1;unsigned int uaccess_err:1; /* uaccess failed */
};
struct thread_info {unsigned long flags; /* low level flags */unsigned long status; /* thread synchronous flags */
};
老版中采取current_thread_info()->task 来获取task_struct。thread_info 的位置就是内核栈的最高位置,减去 THREAD_SIZE,就到了 thread_info 的起始地址。
static inline struct thread_info *current_thread_info(void)
{return (struct thread_info *)(current_top_of_stack() - THREAD_SIZE);
}而新版本则采用了另一种current_thread_info#include <asm/current.h>
#define current_thread_info() ((struct thread_info *)current)
#endif
那 current 又是什么呢?在 arch/x86/include/asm/current.h 中定义了。
struct task_struct;DECLARE_PER_CPU(struct task_struct *, current_task);static __always_inline struct task_struct *get_current(void)
{return this_cpu_read_stable(current_task);
}#define current get_current
新的机制里面,每个 CPU 运行的 task_struct 不通过thread_info 获取了,而是直接放在 Per CPU 变量里面了。多核情况下,CPU 是同时运行的,但是它们共同使用其他的硬件资源的时候,我们需要解决多个 CPU 之间的同步问题。Per CPU 变量是内核中一种重要的同步机制。顾名思义,Per CPU 变量就是为每个 CPU 构造一个变量的副本,这样多个 CPU 各自操作自己的副本,互不干涉。比如,当前进程的变量 current_task 就被声明为 Per CPU 变量。要使用 Per CPU 变量,首先要声明这个变量,在 arch/x86/include/asm/current.h 中有:
DECLARE_PER_CPU(struct task_struct *, current_task);
然后是定义这个变量,在 arch/x86/kernel/cpu/common.c 中有:
DEFINE_PER_CPU(struct task_struct *, current_task) = &init_task;
也就是说,系统刚刚初始化的时候,current_task 都指向init_task。当某个 CPU 上的进程进行切换的时候,current_task 被修改为将要切换到的目标进程。例如,进程切换函数__switch_to 就会改变 current_task。
__visible __notrace_funcgraph struct task_struct *
__switch_to(struct task_struct *prev_p, struct task_struct *next_p)
{
......this_cpu_write(current_task, next_p);
......return prev_p;
}
当要获取当前的运行中的 task_struct 的时候,就需要调用 this_cpu_read_stable 进行读取。
#define this_cpu_read_stable(var) percpu_stable_op("mov", var)
通过这种方式,即可轻松地获得task_struct的地址。
二、task_struct:进程的“灵魂档案”
2.1 定义与地位
在 Linux 内核的代码世界里,task_struct被精心定义为一个结构体,其内部成员众多,宛如一个庞大而有序的信息仓库 。在<linux/sched.h>头文件中,我们能一窥它的定义全貌:
struct task_struct {volatile long state; /* 进程状态 */void *stack; /* 指向内核栈的指针 */pid_t pid; /* 进程ID */pid_t tgid; /* 线程组ID */struct task_struct *real_parent; /* 指向真正的父进程 */struct task_struct *parent; /* 指向接收SIGCHLD信号的父进程 */struct list_head children; /* 子进程链表 */struct list_head sibling; /* 兄弟进程链表 */struct mm_struct *mm; /* 指向内存描述符 */struct mm_struct *active_mm; /* 指向活跃的内存描述符 */// 此处省略大量其他成员
};
task_struct就如同进程的 “灵魂档案”,从进程诞生的那一刻起,它便如影随形,详细记录着进程的各种属性和状态信息。无论是进程的唯一标识 —— 进程 ID(PID),还是进程所处的运行状态(如运行、就绪、睡眠等),亦或是进程与其他进程之间的亲属关系(父进程、子进程、兄弟进程等),以及进程所占用的内存资源、打开的文件描述符等关键信息,都被一一存储在这个结构体中。可以毫不夸张地说,task_struct是进程在 Linux 内核中的 “代言人”,内核正是通过对task_struct结构体的管理和操作,实现了对进程的创建、调度、终止等一系列生命周期的有效管控 。
2.2 内存布局奥秘
在 Linux 内核中,task_struct的内存分配与内核栈有着紧密的联系 。通常情况下,内核会为每个进程分配一个大小固定的内存区域,这个区域同时包含了task_struct结构体和进程的内核栈。以常见的 x86 架构为例,内核会一次性分配两个连续的物理页面(每个页面大小通常为 4KB,共 8KB)来存储这两部分内容 。其中,task_struct结构体大约占用底部的 1KB 空间,而剩余的 7KB 空间则用于存放进程的内核栈 。
这种内存布局方式并非随意为之,而是有着深刻的设计考量 。一方面,将task_struct与内核栈放在一起,能够减少内存碎片的产生,提高内存的使用效率 。当进程创建时,一次性分配连续的内存空间,避免了多次分配内存可能导致的内存碎片化问题 。另一方面,这种布局方式也方便了内核在进行进程上下文切换时对相关信息的快速访问 。在进程上下文切换过程中,内核需要保存和恢复进程的各种状态信息,包括 CPU 寄存器的值、堆栈指针等 。将task_struct与内核栈相邻放置,使得内核能够通过简单的指针运算,快速找到并操作这些关键信息,从而大大提高了进程上下文切换的效率 。
例如,当进程从用户态切换到内核态时,CPU 需要将当前的堆栈指针切换到内核栈的地址 。由于task_struct与内核栈在内存中是连续的,内核可以根据task_struct中保存的栈指针信息,迅速定位到内核栈的起始地址,完成堆栈指针的切换 。同样,在进程返回用户态时,内核也能够轻松地恢复用户栈的地址和相关状态信息 。这种紧密的内存布局关系,就像是一场精心编排的舞蹈,task_struct与内核栈相互配合,共同保障了进程在 Linux 内核中的高效运行 。
2.3task_struct 与系统调用
(1)以 fork 系统调用为例看 task_struct 的复制与初始化
在 Linux 中,fork 系统调用是创建新进程的重要方式。当执行 fork 调用时,内核会为新创建的子进程分配一个新的 task_struct 结构体,这是整个创建子进程流程中极为关键的起始步骤。
起初,内核会尽可能多地将父进程 task_struct 里的内容复制到子进程的 task_struct 中。不过,这里要注意并非所有内容都会马上被原样复制,像内存页相关的部分,就会采用写时拷贝(COW)机制来优化性能,避免不必要的资源浪费以及保证数据在后续使用时的独立性和安全性。
例如,在一个文本编辑进程执行 fork 操作创建子进程时,父进程 task_struct 中记录的该文本编辑程序代码段、数据段等相关内存区域信息会先尝试复制给子进程,而那些具体的内存页面数据可能暂不实际复制,只是设置好写时拷贝相关的机制,等到子进程要对相应内存数据进行修改操作时,才会真正去复制一份属于子进程自己的数据副本,以保证父子进程后续数据操作的互不干扰。
完成基本的复制后,内核还需要进行一系列必要的修改来确保子进程能够独立于父进程运行。其中会设置子进程的 PID(进程 ID),使其拥有一个新的唯一的进程 ID,而父进程的 PID 保持不变;同时,子进程的 PPID(父进程 ID)会被设置为调用 fork 的进程的 PID。除此之外,像进程组、会话等关系也会相应地更新或初始化,信号处理器、文件描述符表等同样要进行适当的调整,以符合子进程后续独立运行的需求。
(2)对进程管理的影响与意义
task_struct 在 fork 系统调用创建子进程时的这种复制与初始化机制,对进程管理有着多方面重要的影响和意义。
从资源分配角度来看,通过写时拷贝机制,在子进程创建初期可以避免大量不必要的内存数据复制开销,多个子进程可以在初期共享父进程的内存资源,只有当真正需要修改数据时才各自分配独立的内存空间,这样能更高效地利用系统内存资源,尤其是在创建多个相似子进程的场景下,能显著节省内存开销,使得系统整体资源分配更加合理且灵活。
在进程调度方面,新创建并初始化好的子进程会被加入到内核的调度队列中(通常是就绪队列),等待 CPU 的调度执行。每个子进程拥有独立的 task_struct 结构体,意味着调度器可以依据各个进程(包括父子进程以及不同的子进程之间) task_struct 里记录的不同状态、优先级等调度相关信息,来公平且合理地分配 CPU 时间片,确保系统中各个进程都能有序地获得执行机会,保障系统的并发处理能力和整体运行效率。
从进程独立性和安全性来讲,尽管子进程是以父进程为模板进行 task_struct 的复制和初始化,但经过修改关键标识信息以及后续内存数据写时拷贝等操作后,子进程能够独立运行,不会因为自身的操作(比如修改内存数据、接收信号等)而影响到父进程或者其他子进程的正常运行,保障了每个进程在系统中的独立性,同时也避免了因进程间不合理的相互干扰而可能引发的安全问题,增强了整个系统进程管理的稳定性和安全性。
三、结构成员深度剖析
3.1 进程状态标识
在task_struct结构体中,state和exit_state成员肩负着标识进程状态的重要使命 。state成员通过一系列预定义的常量值,细致地描述了进程当前的运行状态 。其中,TASK_RUNNING状态犹如赛道上蓄势待发的选手,表示进程要么正在 CPU 上全力奔跑(执行),要么已经站在起跑线上,时刻准备着获取 CPU 的青睐(就绪) 。当我们在系统中运行一个简单的计算程序时,在程序执行的过程中,该进程就处于TASK_RUNNING状态 。如果系统中同时存在多个处于TASK_RUNNING状态的进程,它们就会像一群渴望上场比赛的选手,等待着调度器按照一定的规则安排它们轮流在 CPU 这个赛道上奔跑 。
TASK_INTERRUPTIBLE状态则像是一位暂时休息的选手,进程处于可中断的睡眠状态,它正在耐心等待某个特定事件的发生,比如等待读取文件的数据、等待网络请求的响应等 。当一个进程发起文件读取操作时,由于磁盘 I/O 速度相对较慢,在数据读取完成之前,进程会进入TASK_INTERRUPTIBLE状态,暂时让出 CPU 资源,进入睡眠状态 。此时,如果有信号传来,就如同有人呼喊这位休息的选手,它会被唤醒,从睡眠状态中苏醒过来,加入到可运行状态的队伍中,等待再次获得 CPU 资源,继续执行后续的操作 。
与TASK_INTERRUPTIBLE状态类似,TASK_UNINTERRUPTIBLE状态下的进程也处于睡眠状态,但它是深度睡眠,如同一位陷入沉睡的选手,不会被信号轻易唤醒,只有当它所等待的特定事件完成时,才会被唤醒 。在某些情况下,进程可能会等待特定的硬件资源,比如等待磁盘设备完成初始化,此时进程会进入TASK_UNINTERRUPTIBLE状态,以确保在硬件资源准备好之前,不会被其他因素干扰 。
__TASK_STOPPED状态表示进程被暂时喊停,处于停止执行的状态,就像比赛中的选手因为某些特殊原因被裁判要求暂停比赛 。通常,当进程接收到SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU等信号时,就会进入这种状态 。例如,当我们在调试程序时,使用调试工具向进程发送SIGSTOP信号,进程就会停止执行,方便我们进行调试操作 。
__TASK_TRACED状态则表示进程正在被像调试器这样的 “裁判助手” 密切监视着,进程的一举一动都在监控之下 。在调试程序时,调试器会将进程设置为__TASK_TRACED状态,以便实时获取进程的运行信息,帮助开发者找出程序中的问题 。
而exit_state成员主要用于记录进程在终止阶段的相关状态 。EXIT_ZOMBIE状态意味着进程已经完成了它的使命,执行被终止,但它的 “后事” 还未处理完毕,父进程还没有使用wait()等系统调用来获取它的终止信息 。就像一位选手完成了比赛,但还没有和教练(父进程)进行最后的交接 。EXIT_DEAD状态则表示进程已经彻底结束,所有的资源都已被释放,如同选手已经离开赛场,一切都已尘埃落定 。
进程状态的转换就像一场精心编排的舞蹈,随着系统中各种事件的发生而有序进行 。当一个处于TASK_INTERRUPTIBLE状态的进程等待的事件完成时,它会像被唤醒的选手一样,从睡眠状态转换为TASK_RUNNING状态,重新获得执行的机会 。当进程接收到终止信号时,它会从当前状态转换为EXIT_ZOMBIE状态,等待父进程的处理 。父进程调用wait()系统调用后,进程才会最终进入EXIT_DEAD状态,完成它的整个生命周期 。
3.2 身份标识
pid和tgid作为task_struct结构体中的身份标识成员,在进程和线程的识别与管理中扮演着至关重要的角色 。pid,即进程 ID,是系统为每个进程分配的独一无二的 “身份证号码”,它就像班级里每个学生的学号,用于唯一标识一个进程 。在 Linux 系统中,pid是一个整型数值,从 1 开始依次递增,每个新创建的进程都会被赋予一个比之前进程pid更大的唯一值 。当我们在系统中运行多个程序时,通过pid可以准确地区分不同的进程,操作系统也能够根据pid对进程进行各种操作,如发送信号、终止进程等 。例如,使用kill命令时,就需要指定进程的pid来向该进程发送终止信号 。
tgid,即线程组 ID,与线程组的概念紧密相连 。在 Linux 系统中,线程被视为轻量级进程,它们共享同一进程的资源,如内存空间、文件描述符等 。线程组是由一个或多个线程组成的集合,这些线程共同协作完成特定的任务 。tgid用于标识线程组,同一线程组中的所有线程都拥有相同的tgid,它就像一个团队的标志,将同一组的线程紧密联系在一起 。对于只有主线程的进程来说,pid和tgid的值是相等的,因为此时进程就是一个单一的线程组,主线程既是进程的代表,也是线程组的唯一成员 。但当一个进程创建了多个线程时,情况就有所不同了 。每个线程都有自己独立的pid,就像团队中的每个成员都有自己的个性标识,但它们都共享同一个tgid,以表明它们属于同一个线程组 。
这种进程和线程的标识机制,为操作系统的高效管理提供了有力支持 。在多线程编程中,通过pid和tgid,操作系统能够清晰地识别每个线程的身份,合理地分配 CPU 资源,确保线程组内的线程能够协同工作,同时也能对不同线程组的进程进行有效的调度和管理 。例如,在一个多线程的网络服务器程序中,主线程负责监听网络连接,而多个工作线程负责处理接收到的请求 。通过pid和tgid,操作系统可以准确地调度这些线程,保证服务器能够高效地处理大量并发请求 。
3.3 优先级与调度
在task_struct结构体中,prio、static_prio、normal_prio和rt_priority等成员在进程调度的舞台上扮演着关键角色,它们共同决定了进程在 CPU 资源竞争中的优先级顺序 。
static_prio是进程的静态优先级,它就像一个学生的基础成绩,在进程创建时就被确定下来,并且在进程的生命周期中相对稳定 。静态优先级的值越小,代表进程的优先级越高 。对于实时进程,其静态优先级范围通常是 0 - 99,而普通进程的静态优先级范围是 100 - 139 。例如,一个用于实时视频播放的进程,为了保证视频播放的流畅性,可能会被赋予较低的静态优先级,以确保它能够优先获得 CPU 资源 。静态优先级可以通过nice()或者setpriority等系统调用在用户空间进行修改,新创建的进程会继承父进程的静态优先级 。
rt_priority表示进程的实时优先级,主要用于实时进程 。实时进程对时间的要求非常严格,需要在规定的时间内完成任务 。实时优先级的范围是 1 - 99,值越大表示优先级越高 。在一个工业控制系统中,用于控制生产设备的实时进程,其rt_priority可能会被设置得较高,以确保能够及时响应设备的各种信号,保证生产的正常进行 。普通进程的rt_priority通常为 0 。
normal_prio是归一化优先级,它是根据静态优先级、实时优先级和调度策略综合计算得出的 。调度器在进行调度决策时,会参考归一化优先级来确定进程的执行顺序 。对于普通进程,其归一化优先级通常就是静态优先级;而对于实时进程,归一化优先级则与实时优先级相关 。
prio是进程的动态优先级,它是调度器实际用于调度的优先级 。动态优先级在运行时可以根据进程的运行情况进行调整 。例如,当一个进程长时间占用 CPU 资源时,调度器可能会降低它的动态优先级,以便让其他进程也有机会获得 CPU 资源 。相反,当一个进程处于等待状态,等待某个资源的时间较长时,调度器可能会适当提高它的动态优先级,以提高系统的整体性能 。
不同的调度策略对这些优先级成员有着不同的运用方式 。在完全公平调度(CFS)策略下,调度器会根据进程的虚拟运行时间(vruntime)和归一化优先级来分配 CPU 时间,确保每个进程都能公平地获得 CPU 资源 。而在实时调度策略中,如SCHED_FIFO(先进先出调度)和SCHED_RR(时间片轮转调度),实时优先级rt_priority起着关键作用,高优先级的实时进程会优先获得 CPU 资源,并且在没有更高优先级实时进程的情况下,会一直占用 CPU,直到完成任务或者主动让出 CPU 。
3.4 内存管理指针
在task_struct结构体中,mm和active_mm成员犹如进程内存管理世界的 “导航仪”,在进程用户空间内存管理的复杂旅程中发挥着关键作用 。
mm指针指向一个mm_struct结构体,这个结构体就像是进程内存世界的 “大管家”,详细记录了进程用户空间的内存布局和管理信息 。它包含了进程的代码段、数据段、堆、栈等内存区域的映射信息,以及页表、虚拟内存区域(VMA)列表等重要数据 。当一个进程运行时,它所需要的代码和数据都存储在这些内存区域中 。例如,一个 C 语言程序在运行时,程序的可执行代码会存储在代码段,全局变量和静态变量会存储在数据段,动态分配的内存会在堆中进行管理,而函数调用时的局部变量和参数则会存储在栈中 。mm_struct结构体通过对这些内存区域的有效管理,确保进程能够正确地访问和使用内存资源 。
active_mm主要用于处理内核线程的内存管理问题 。对于普通用户进程来说,active_mm通常指向与该进程关联的mm_struct,就像一个专属的内存管理助手,时刻为进程提供内存管理服务 。然而,内核线程的情况有所不同 。内核线程只在内核空间中运行,不需要访问用户空间内存,因此它们通常没有自己独立的mm_struct 。在这种情况下,active_mm会指向最后一个运行在该 CPU 上的用户进程的mm_struct 。这就好比内核线程在内存管理方面没有自己的 “家”,但它可以借用最后一个在该 CPU 上运行的用户进程的 “家” 来进行一些必要的内存操作 。例如,当内核线程执行某些需要访问内存的操作时,它可以通过active_mm找到合适的内存上下文,从而确保内存操作的正确执行 。
这种内存管理机制,既保证了普通进程能够高效地管理和使用自己的用户空间内存,又巧妙地解决了内核线程在内存管理方面的特殊需求,使得整个系统的内存管理更加灵活和高效 。在系统中同时运行多个进程和内核线程的情况下,通过mm和active_mm的协同工作,能够有条不紊地进行内存分配、回收和访问控制,为系统的稳定运行提供了坚实的保障 。
3.5 亲属关系指针
在task_struct结构体中,real_parent、parent、children和sibling等成员如同一张无形的关系网,清晰地描绘了进程间的亲属关系,在进程管理和信号传递的舞台上发挥着不可或缺的作用 。
real_parent指针指向进程真正的父进程,就像孩子指向自己的亲生父母 。在正常情况下,当一个进程被创建时,它的real_parent会指向创建它的父进程 。例如,当我们在终端中通过命令行启动一个新的进程时,这个新进程的real_parent就是终端进程 。然而,在某些特殊情况下,如使用调试工具(如 GDB)调试进程时,情况会有所不同 。假设在 bash 中使用 GDB 来调试一个进程,此时进程的parent是 GDB,因为 GDB 负责监控和控制进程的执行;而real_parent仍然是 bash,因为 bash 是最初创建该进程的父进程 。这种区分在进程管理和信号传递中非常重要,它确保了进程能够正确地继承父进程的资源和属性,并且在需要时能够向正确的父进程发送信号 。
parent指针同样指向父进程,但它主要用于接收SIGCHLD信号和wait4()系统调用的报告 。当一个进程终止时,它会向自己的parent发送SIGCHLD信号,通知父进程自己的状态发生了变化 。父进程可以通过wait4()系统调用来获取子进程的终止信息,如子进程的退出状态、资源使用情况等 。这就像孩子完成任务后向家长汇报情况,家长可以根据这些信息进行相应的处理 。
children是一个链表头,它将所有属于该进程的子进程串联在一起,形成了一个家族树 。链表中的每个元素都是该进程的子进程,通过children链表,父进程可以方便地管理和访问自己的子进程 。例如,父进程可以遍历children链表,对每个子进程进行资源分配、状态查询等操作 。
sibling指针则用于将当前进程插入到兄弟进程链表中,它就像连接兄弟姐妹之间的纽带 。拥有同一父进程的所有进程互为兄弟进程,它们通过sibling指针相互关联 。通过这个链表,进程可以快速找到自己的兄弟进程,实现进程间的协作和通信 。例如,在一个多进程的应用程序中,兄弟进程之间可能需要共享某些资源或者传递数据,通过sibling链表,它们可以方便地找到彼此并进行交互 。
在进程管理和信号传递中,这些亲属关系指针起着至关重要的作用 。当一个进程接收到信号时,它会根据自己的亲属关系将信号传递给合适的进程 。例如,当父进程接收到SIGCHLD信号时,它可以通过children链表找到对应的子进程,并进行相应的处理 。这种基于亲属关系的信号传递机制,确保了信号能够准确地到达目标进程,提高了系统的响应速度和稳定性 。
3.6 时间与统计信息
在task_struct结构体中,utime、stime、start_time等时间相关成员,以及nvcsw、nivcsw等统计信息成员,就像一个个精准的记录员,详细地记录着进程的时间开销和运行统计信息,为系统的性能分析和进程管理提供了重要的数据支持 。
utime表示进程在用户态下消耗的时间,就像运动员在比赛中实际奔跑的时间 。它记录了进程执行用户代码所花费的时间,这个时间不包括进程在系统调用和内核态下的时间 。例如,一个计算密集型的进程在进行复杂的数学运算时,utime会随着运算的进行而不断增加 。通过统计utime,我们可以了解进程在用户态下的执行效率,判断进程是否存在性能瓶颈 。
stime则记录了进程在内核态下消耗的时间,如同运动员在比赛中准备和调整的时间 。当进程进行系统调用,如读取文件、分配内存等操作时,会进入内核态,stime会统计这部分时间 。系统调用通常涉及到内核资源的访问和管理,stime的统计可以帮助我们了解进程对内核资源的使用情况,评估系统调用的开销 。
start_time记录了进程的启动时间,就像比赛的开始时间 。它是一个时间戳,表示进程从创建到开始执行的时间点 。通过start_time,我们可以计算进程的运行时长,了解进程在系统中的存活时间 。在系统性能分析中,运行时长是一个重要的指标,它可以帮助我们判断进程是否长时间占用系统资源,是否需要进行优化 。
nvcsw和nivcsw属于统计信息成员,分别表示自愿上下文切换次数和非自愿上下文切换次数 。自愿上下文切换是指进程主动放弃 CPU,例如进程在等待 I/O 操作完成时,会主动让出 CPU,此时nvcsw会增加 。这就像运动员在比赛中主动休息,调整状态 。非自愿上下文切换则是指进程被调度器强制剥夺 CPU,例如当有更高优先级的进程需要运行时,当前进程会被切换出去,nivcsw会增加 。这就像运动员在比赛中被裁判要求暂停,让其他选手上场 。通过统计这两个值,我们可以了解进程在 CPU 竞争中的表现,评估调度器的性能 。如果一个进程的nivcsw过高,可能意味着系统中存在竞争激烈的情况,需要进一步优化调度策略 。
四、task_struct与进程管理的“化学反应”
4.1 进程创建
在 Linux 系统中,进程的创建如同生命的诞生,充满了奇妙的过程,而fork()系统调用则是这个过程的关键 “催化剂” 。当fork()系统调用被触发时,一场精心编排的 “复制” 大戏便拉开了帷幕 。内核首先会在内存中为新的子进程精心分配一个全新的task_struct结构体,就像为新生命准备了一个独特的 “生命档案” 。这个新的task_struct结构体就像是一张白纸,等待着被赋予各种关键信息 。
接下来,子进程开始从父进程那里继承一系列重要的信息 。子进程会继承父进程的进程状态,就像孩子继承了父母的某些特质 。如果父进程处于运行状态,子进程在创建初期也会继承这个状态,等待着被调度执行 。在进程的亲属关系方面,子进程的real_parent和parent指针都会指向父进程,就像孩子与父母之间建立了紧密的联系 。这种亲属关系的继承确保了子进程能够正确地融入进程家族树,在需要时能够向父进程寻求支持和资源 。
在内存管理方面,子进程会与父进程共享内存资源 。它们共享同一内存描述符mm_struct,这意味着它们在用户空间中看到的内存布局是相同的 。这就好比两个孩子住在同一所房子里,共享着房子里的各种设施 。不过,这种共享是基于写时拷贝(Copy - On - Write,COW)机制的 。在初始阶段,子进程和父进程共享内存页面,但当其中任何一方试图对共享页面进行写操作时,系统会为写操作的一方分配新的物理页面,将共享页面的内容复制到新页面中,然后进行写操作 。这样,既保证了内存资源的高效利用,又确保了子进程和父进程在内存操作上的独立性 。
文件描述符表也会被子进程继承 。这意味着父进程打开的文件,子进程同样可以访问 。如果父进程打开了一个日志文件用于记录信息,子进程也能够读取和写入这个文件 。这为父子进程之间的协作提供了便利,它们可以通过共享的文件进行数据传递和同步 。
例如,在一个多进程的服务器程序中,父进程负责监听网络端口,接受客户端的连接请求 。当有新的连接到来时,父进程通过fork()创建子进程,子进程继承了父进程的网络连接文件描述符,从而可以独立地处理与客户端的通信 。在这个过程中,子进程的task_struct结构体从父进程那里继承了必要的信息,使得子进程能够顺利地开始它的 “生命旅程”,与父进程协同工作,共同完成服务器的任务 。
4.2 进程调度
进程调度在 Linux 系统中就像是一场激烈的资源争夺赛,而task_struct结构体则是这场比赛中的关键 “情报站”,为调度器提供了丰富的信息,帮助调度器做出合理的决策 。调度器在选择下一个要执行的进程时,会仔细参考task_struct中的优先级信息 。进程的优先级就像运动员的比赛排名,优先级高的进程会优先获得 CPU 资源 。实时进程的优先级通常高于普通进程,这是因为实时进程对时间的要求非常严格,需要在规定的时间内完成任务 。在一个实时视频监控系统中,用于处理视频流的进程会被赋予较高的优先级,以确保视频的实时性和流畅性 。
进程的状态也是调度器关注的重要信息 。处于TASK_RUNNING状态的进程,如同已经站在起跑线上的运动员,时刻准备着获取 CPU 资源,进入执行状态 。而处于TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE状态的进程,则像是在休息或等待特定资源的运动员,调度器会暂时跳过它们,将 CPU 资源分配给更有执行条件的进程 。
当进行上下文切换时,task_struct的作用更是不可或缺 。上下文切换就像是运动员在比赛中途进行换人,新上场的运动员需要迅速适应比赛环境 。在这个过程中,task_struct保存了进程的各种上下文信息,包括 CPU 寄存器的值、堆栈指针等 。当一个进程被切换出去时,内核会将该进程的 CPU 寄存器的值等上下文信息保存到它的task_struct中 。当这个进程再次被调度执行时,内核会从它的task_struct中读取这些上下文信息,恢复 CPU 寄存器的值和堆栈指针,使得进程能够继续从上次中断的地方执行 。这就像运动员在比赛中换人后,新上场的运动员能够迅速了解比赛的进展情况,继续完成比赛 。
在多核心 CPU 的系统中,调度器还会根据task_struct中的cpus_allowed等信息,决定将进程分配到哪个 CPU 核心上执行 。这个信息就像是运动员的参赛场地选择,调度器会根据进程的需求和 CPU 核心的负载情况,合理地安排进程在不同的 CPU 核心上运行,以提高系统的整体性能 。
4.3 进程终止
进程终止的过程就像是一场演出的落幕,虽然看似简单,但背后却涉及到一系列复杂而有序的操作,而task_struct在这个过程中扮演着关键的角色 。当一个进程完成了它的使命,准备终止时,内核会首先对task_struct中的相关信息进行处理 。进程会释放它所占用的各种资源,这就像是演员在演出结束后归还借用的道具 。例如,进程会关闭它打开的文件描述符,释放文件锁,将文件资源归还给系统 。在内存管理方面,进程会释放它所占用的内存空间,包括堆内存、栈内存等 。对于使用了动态内存分配的进程,如通过malloc()函数分配的内存,在进程终止时,这些内存会被回收,以避免内存泄漏 。
进程的状态也会被更新 。它会从当前的运行状态转换为EXIT_ZOMBIE状态,就像演员从舞台上退下,进入了一种等待善后处理的状态 。在EXIT_ZOMBIE状态下,进程虽然已经停止执行,但它的task_struct结构体仍然存在于系统中,因为它还需要向父进程传递一些重要的信息,如进程的退出状态、资源使用情况等 。父进程可以通过wait()或waitpid()等系统调用来获取这些信息 。这就像演出结束后,演员需要向导演汇报演出的情况 。
当父进程调用wait()或waitpid()时,内核会处理子进程的task_struct 。内核会读取子进程的退出状态,这个状态信息就像是演员的演出评价,父进程可以根据这个状态了解子进程的执行结果 。内核会回收子进程的task_struct结构体以及其他相关资源,将它们从系统中彻底移除 。这就像是演出结束后,清理舞台和道具,为下一场演出做好准备 。
如果父进程没有及时调用wait()来获取子进程的终止信息,子进程就会一直处于EXIT_ZOMBIE状态,成为一个 “僵尸进程” 。僵尸进程虽然不占用CPU等执行资源,但它会占用系统的内存等资源,就像废弃的道具占用着仓库的空间 。长时间存在大量僵尸进程可能会导致系统资源的浪费,影响系统的性能 。因此,在编写多进程程序时,父进程需要及时处理子进程的终止信息,避免僵尸进程的产生 。
五、案例实战分析
为了更直观地感受task_struct在实际中的应用,我们通过一个简单的内核模块代码示例来一探究竟 。下面是一段用于读取所有进程的task_struct结构信息的内核模块代码:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/sched/signal.h>
#include <linux/init.h>static int __init hello_init(void) {struct task_struct *pp;printk("for_each_process begin\n");for_each_process(pp) {printk(KERN_INFO "process_info pid:%i comm:%s flags:%i", pp->pid, pp->comm, pp->flags);}return 0;
}static void __exit hello_exit(void) {printk("for_each_process end!\n");
}module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
在这段代码中,我们首先包含了必要的头文件,这些头文件为我们提供了与内核交互所需的各种定义和函数声明 。hello_init函数是内核模块的初始化函数,当模块被加载到内核中时,这个函数会被执行 。在函数内部,我们使用for_each_process宏来遍历系统中的所有进程 。这个宏就像是一个向导,带领我们逐一访问系统中的每个进程 。对于每个遍历到的进程,我们通过printk函数打印出其pid(进程 ID)、comm(进程名称)和flags(进程标志)等信息 。pid就像进程的身份证号码,独一无二地标识着每个进程;comm则是进程的名字,让我们能够直观地了解进程的用途;flags包含了进程的各种状态标志为我们提供了关于进程状态的重要线索 。
hello_exit函数是内核模块的退出函数,当模块从内核中卸载时,这个函数会被执行 。在函数中,我们使用printk函数打印出模块卸载的提示信息 。
通过这个内核模块,我们可以清晰地看到系统中每个进程的一些关键信息,这些信息都来自于task_struct结构体 。这就好比我们打开了一个进程信息的宝藏库,通过task_struct结构体,我们能够获取到进程的各种详细信息,从而更好地了解系统中进程的运行状态和行为 。