Linux 进程管理之struct task_struct

server/2024/12/4 3:29:49/

文章目录

  • 前言
  • 一、任务 ID
  • 二、信号处理
  • 三、任务状态
  • 四、进程调度
  • 五、运行统计信息
  • 六、进程亲缘关系
  • 七、进程权限
  • 八、内存管理
  • 九、文件与文件系统
  • 十、进程栈
    • 10.1 用户态函数栈
    • 10.2 内核态函数栈
  • 参考资料

前言

进程是一个应用程序运行时刻的实例(从进程的结构看);进程是应用程序运行时所需资源的容器(从进程的功能看);甚至进程是一堆数据结构(从操作系统对进程实现的角度来说)。
进程是一个应用程序运行时刻的实例,它的目的就是操作系统用于管理和运行多个应用程序的;其次,操作系统是给应用程序提供服务的。因此进程必须要有一个地址空间,这个地址空间至少包括两部分内容:一部分是内核,一部分是用户的应用程序。

在 Linux 里面,无论是进程,还是线程,到了内核里面,我们统一都叫任务(Task),由一个统一的结构 task_struct 进行管理。如下图所示:
在这里插入图片描述
进程是管理资源的基本单位,线程是调度的基本单位。

// linux-5.15/include/linux/sched.hstruct task_struct {......
}

struct task_struct是用于描述进程运行状况以及控制进程运行所需要的全部信息,是操作系统用来感知进程存在的一个最重要的数据结构。

进程在其生命周期内,进程和内核的很多模块进行交互,如内存管理模块、进程调度模块以及文件系统模块。

struct task_struct数据结构包含的内容可以归纳为如下,如下图所示:
在这里插入图片描述
图片来自于:极客时间趣谈Linux操作系统

(1)Linux 内核使用一个双向链表,将所有的 task_struct 串起来。

// linux-5.15/include/linux/sched.hstruct task_struct {......struct list_head		tasks;.....
}
#define for_each_process(p) \for (p = &init_task ; (p = next_task(p)) != &init_task ; )

for_each_process宏获取所有进程。

#define next_task(p) \list_entry_rcu((p)->tasks.next, struct task_struct, tasks)

next_task宏获取该进程的下一个进程。

(2)此外Linux内核还用哈希表将所有的 task_struct 串起来。

enum pid_type
{PIDTYPE_PID,PIDTYPE_TGID,PIDTYPE_PGID,PIDTYPE_SID,PIDTYPE_MAX,
};
	/* PID/PID hash table linkage. */struct pid			*thread_pid;struct hlist_node		pid_links[PIDTYPE_MAX];

根据进程pid获取struct task_struct API:

// linux-5.15/kernel/pid.cstruct pid *find_get_pid(pid_t nr)
{struct pid *pid;rcu_read_lock();pid = get_pid(find_vpid(nr));rcu_read_unlock();return pid;
}
EXPORT_SYMBOL_GPL(find_get_pid);struct task_struct *pid_task(struct pid *pid, enum pid_type type)
{struct task_struct *result = NULL;if (pid) {struct hlist_node *first;first = rcu_dereference_check(hlist_first_rcu(&pid->tasks[type]),lockdep_tasklist_lock_is_held());if (first)result = hlist_entry(first, struct task_struct, pid_links[(type)]);}return result;
}
EXPORT_SYMBOL(pid_task);

一、任务 ID

struct task_struct {pid_t				pid;pid_t				tgid;struct task_struct		*group_leader;	
}

进程和线程到了内核这里,统一变成了任务。在内核中,它们虽然都是任务,但是应该加以区分。其中,pid 是 process id,tgid 是 thread group ID。

任何一个进程,如果只有主线程,那 pid 是自己,tgid 是自己,group_leader 指向的还是自己。

但是,如果一个进程创建了其他线程,那就会有所变化了。线程有自己的 pid,tgid 就是进程的主线程的 pid,group_leader 指向的就是进程的主线程。

有了 tgid,我们就知道 tast_struct 代表的是一个进程还是代表一个线程了。

bool is_process(struct task_struct *p)
{if(p->pid == p->tgid)return true;   //进程elsereturn false;  //线程
}

二、信号处理

struct task_struct {/* Signal handlers: */struct signal_struct		*signal;struct sighand_struct __rcu		*sighand;sigset_t			blocked;sigset_t			real_blocked;/* Restored if set_restore_sigmask() was used: */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 这三个变量的作用。

task_struct 里面有一个 struct sigpending pending。如果我们进入 struct signal_struct *signal 去看的话,还有一个 struct sigpending shared_pending。它们一个是本任务的,一个是线程组共享的。

三、任务状态

struct task_struct {unsigned int			__state;int				exit_state;/* Per task flags (PF_*), defined further below: */unsigned int			flags;
}

state(状态)可以取的值:

/* Used in tsk->state: */
#define TASK_RUNNING			0x0000
#define TASK_INTERRUPTIBLE		0x0001
#define TASK_UNINTERRUPTIBLE		0x0002
#define __TASK_STOPPED			0x0004
#define __TASK_TRACED			0x0008
/* Used in tsk->exit_state: */
#define EXIT_DEAD			0x0010
#define EXIT_ZOMBIE			0x0020
#define EXIT_TRACE			(EXIT_ZOMBIE | EXIT_DEAD)
/* Used in tsk->state again: */
#define TASK_PARKED			0x0040
#define TASK_DEAD			0x0080
#define TASK_WAKEKILL			0x0100
#define TASK_WAKING			0x0200
#define TASK_NOLOAD			0x0400
#define TASK_NEW			0x0800
/* RT specific auxilliary flag to mark RT lock waiters */
#define TASK_RTLOCK_WAIT		0x1000
#define TASK_STATE_MAX			0x2000

从定义的数值很容易看出来,state 是通过 bitset 的方式设置的,也就是说,当前是什么状态,哪一位就置一。
在这里插入图片描述
TASK_RUNNING 并不是说进程正在运行,而是表示进程在时刻准备运行的状态。当处于这个状态的进程获得时间片的时候,就是在运行中;如果没有获得时间片,就说明它被其他进程抢占了,在等待再次分配时间片。

在运行中的进程,一旦要进行一些 I/O 操作,需要等待 I/O 完毕,这个时候会释放 CPU,进入睡眠状态。

一种是 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 相当于这两位都设置了。

#define TASK_KILLABLE			(TASK_WAKEKILL | TASK_UNINTERRUPTIBLE)

TASK_STOPPED 是在进程接收到 SIGSTOP、SIGTTIN、SIGTSTP 或者 SIGTTOU 信号之后进入该状态。

#define TASK_STOPPED			(TASK_WAKEKILL | __TASK_STOPPED)

TASK_TRACED 表示进程被 debugger 等进程监视,进程执行被调试程序所停止。当一个进程被另外的进程所监视,每一个信号都会让进程进入该状态。

#define TASK_TRACED			(TASK_WAKEKILL | __TASK_TRACED)

一旦一个进程要结束,先进入的是 EXIT_ZOMBIE 状态,但是这个时候它的父进程还没有使用 wait() 等系统调用来获知它的终止信息,此时进程就成了僵尸进程。

EXIT_DEAD 是进程的最终状态。

EXIT_ZOMBIE 和 EXIT_DEAD 也可以用于 exit_state。

上面的进程状态和进程的运行、调度有关系,还有其他的一些状态,我们称为标志。放在 flags 字段中,这些字段都被定义成为宏,以 PF 开头。如下:

/** Per process flags*/
#define PF_VCPU			0x00000001	/* I'm a virtual CPU */
#define PF_IDLE			0x00000002	/* I am an IDLE thread */
#define PF_EXITING		0x00000004	/* Getting shut down */
#define PF_IO_WORKER		0x00000010	/* Task is an IO worker */
#define PF_WQ_WORKER		0x00000020	/* I'm a workqueue worker */
#define PF_FORKNOEXEC		0x00000040	/* Forked but didn't exec */
#define PF_MCE_PROCESS		0x00000080      /* Process policy on mce errors */
#define PF_SUPERPRIV		0x00000100	/* Used super-user privileges */
#define PF_DUMPCORE		0x00000200	/* Dumped core */
#define PF_SIGNALED		0x00000400	/* Killed by a signal */
#define PF_MEMALLOC		0x00000800	/* Allocating memory */
#define PF_NPROC_EXCEEDED	0x00001000	/* set_user() noticed that RLIMIT_NPROC was exceeded */
#define PF_USED_MATH		0x00002000	/* If unset the fpu must be initialized before use */
#define PF_USED_ASYNC		0x00004000	/* Used async_schedule*(), used by module init */
#define PF_NOFREEZE		0x00008000	/* This thread should not be frozen */
#define PF_FROZEN		0x00010000	/* Frozen for system suspend */
#define PF_KSWAPD		0x00020000	/* I am kswapd */
#define PF_MEMALLOC_NOFS	0x00040000	/* All allocation requests will inherit GFP_NOFS */
#define PF_MEMALLOC_NOIO	0x00080000	/* All allocation requests will inherit GFP_NOIO */
#define PF_LOCAL_THROTTLE	0x00100000	/* Throttle writes only against the bdi I write to,* I am cleaning dirty pages from some other bdi. */
#define PF_KTHREAD		0x00200000	/* I am a kernel thread */
#define PF_RANDOMIZE		0x00400000	/* Randomize virtual address space */
#define PF_SWAPWRITE		0x00800000	/* Allowed to write to swap */
#define PF_NO_SETAFFINITY	0x04000000	/* Userland is not allowed to meddle with cpus_mask */
#define PF_MCE_EARLY		0x08000000      /* Early kill for mce process policy */
#define PF_MEMALLOC_PIN		0x10000000	/* Allocation context constrained to zones which allow long term pinning. */
#define PF_FREEZER_SKIP		0x40000000	/* Freezer should not count it as freezable */
#define PF_SUSPEND_TASK		0x80000000      /* This thread called freeze_processes() and should not be frozen */

比如:
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 去掉。

四、进程调度

struct task_struct {//是否在运行队列上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;//可以使用哪些CPUint        nr_cpus_allowed;cpumask_t      cpus_allowed;struct sched_info    sched_info;
}

关于调度,请参考调度专栏:Linux 进程调度

五、运行统计信息

在进程的运行过程中,会有一些统计量,比如有进程在用户态和内核态消耗的时间、上下文切换的次数等等。

struct task_struct {
u64        utime;//用户态消耗的CPU时间
u64        stime;//内核态消耗的CPU时间
unsigned long      nvcsw;//自愿(voluntary)上下文切换计数
unsigned long      nivcsw;//非自愿(involuntary)上下文切换计数
u64        start_time;//进程启动时间,不包含睡眠时间
u64        real_start_time;//进程启动时间,包含睡眠时间
}

六、进程亲缘关系

任何一个进程都有父进程。所以,整个进程其实就是一棵进程树。而拥有同一父进程的所有进程都具有兄弟关系。

struct task_struct {/** Pointers to the (original) parent process, youngest child, younger sibling,* older sibling, respectively.  (p->father can be replaced with* p->real_parent->pid)*//* 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;
}

parent 指向其父进程。当它终止时,必须向它的父进程发送信号。
children 表示链表的头部。链表中的所有元素都是它的子进程。
sibling 用于把当前进程插入到兄弟链表中。
如下图所示:
在这里插入图片描述
通常情况下,real_parent 和 parent 是一样的,但是也会有另外的情况存在。例如,bash 创建一个进程,那进程的 parent 和 real_parent 就都是 bash。如果在 bash 上使用 GDB 来 debug 一个进程,这个时候 GDB 是 parent,bash 是这个进程的 real_parent。

七、进程权限

进程组权限控制:进程能否访问某个文件,能否访问其他的进程组,以及这个进程组能否被其他进程组访问等等,这都是进程组权限的控制范畴。

在 Linux 里面,对于进程权限的定义如下:

struct task_struct {/* Process credentials: *//* Tracer's credentials at attach: */const struct cred __rcu		*ptracer_cred;/* Objective and real subjective task credentials (COW): */const struct cred __rcu		*real_cred;/* Effective (overridable) subjective task credentials (COW): */const struct cred __rcu		*cred;
}

请参考:Linux 安全 - Credentials

八、内存管理

每个进程都有自己独立的虚拟内存空间,这需要有一个数据结构来表示,就是 mm_struct。

struct task_struct {struct mm_struct		*mm;struct mm_struct		*active_mm;
}

请参考:Linux 内存管理

九、文件与文件系统

每个进程有一个文件系统的数据结构,还有一个打开文件的数据结构。

struct task_struct {/* Filesystem information: */struct fs_struct		*fs;/* Open file information: */struct files_struct		*files;}

请参考:Linux 文件系统

十、进程栈

10.1 用户态函数栈

在进程的内存空间里面,栈是一个从高地址到低地址,往下增长的结构,也就是上面是栈底,下面是栈顶,入栈和出栈的操作都是从下面的栈顶开始的。
在这里插入图片描述
我们先来看 32 位操作系统的情况。在 CPU 里,ESP(Extended Stack Pointer)是栈顶指针寄存器,入栈操作 Push 和出栈操作 Pop 指令,会自动调整 ESP 的值。另外有一个寄存器 EBP(Extended Base Pointer),是栈基地址指针寄存器,指向当前栈帧的最底部。

例如,A 调用 B,A 的栈里面包含 A 函数的局部变量,然后是调用 B 的时候要传给它的参数,然后返回 A 的地址,这个地址也应该入栈,这就形成了 A 的栈帧。接下来就是 B 的栈帧部分了,先保存的是 A 栈帧的栈底位置,也就是 EBP。因为在 B 函数里面获取 A 传进来的参数,就是通过这个指针获取的,接下来保存的是 B 的局部变量等等。

当 B 返回的时候,返回值会保存在 EAX 寄存器中,从栈中弹出返回地址,将指令跳转回去,参数也从栈中弹出,然后继续执行 A。

对于 64 位操作系统,模式多少有些不一样。因为 64 位操作系统的寄存器数目比较多。rax 用于保存函数调用的返回结果。栈顶指针寄存器变成了 rsp,指向栈顶位置。堆栈的 Pop 和 Push 操作会自动调整 rsp,栈基指针寄存器变成了 rbp,指向当前栈帧的起始位置。

改变比较多的是参数传递。rdi、rsi、rdx、rcx、r8、r9 这 6 个寄存器,用于传递存储函数调用时的 6 个参数。如果超过 6 的时候,还是需要放到栈里面。

然而,前 6 个参数有时候需要进行寻址,但是如果在寄存器里面,是没有地址的,因而还是会放到栈里面,只不过放到栈里面的操作是被调用函数做的。

在这里插入图片描述

10.2 内核态函数栈

struct task_struct {void				*stack;
}

请参考:Linux 进程管理之内核栈和struct pt_regs

进程栈如下图所示:
在这里插入图片描述

参考资料

Linux 5.15.0

极客时间:趣谈Linux操作系统


http://www.ppmy.cn/server/147179.html

相关文章

淘宝商品数据获取:Python爬虫技术的应用与实践

引言 随着电子商务的蓬勃发展,淘宝作为中国最大的电商平台之一,拥有海量的商品数据。这些数据对于市场分析、消费者行为研究、商品推荐系统等领域具有极高的价值。然而,如何高效、合法地从淘宝平台获取这些数据,成为了一个技术挑…

Linux vi/vim

Linux vi/vim 所有的 Unix Like 系统都会内建 vi 文书编辑器,其他的文书编辑器则不一定会存在。 但是目前我们使用比较多的是 vim 编辑器。 vim 具有程序编辑的能力,可以主动的以字体颜色辨别语法的正确性,方便程序设计。 什么是 vim&…

PostgreSQL17官网下载详细教程

PostgreSQL17官网下载详细教程 文章目录 PostgreSQL17官网下载详细教程1. 官网下载地址2. 下载1. 进入下载页2. Download the installer3. Download zip archive 3. 下载后的两种安装文件 1. 官网下载地址 https://www.postgresql.org/download/ 2. 下载 下面以下载Windows下…

【Redis初阶】Zset 有序集合

Hi~!这里是奋斗的明志,很荣幸您能阅读我的文章,诚请评论指点,欢迎欢迎 ~~ 🌱🌱个人主页:奋斗的明志 🌱🌱所属专栏:Redis 📚本系列文章为个人学习笔…

C#热更原理与HybridCLR

一、Mono的诞生 在Mono之前,C#虽然很好,但是只在windows家族平台上使用,就这点C#与Java就无法比。于是微软公司向ECMA申请将C#作为一种标准。在2001年12月,ECMA发布了ECMA-334 C#语言规范。C#在2003年成为一个ISO标准(ISO/IEC 23270)。意味着只要你遵守CLI(Common Lang…

为什么在PyTorch中需要添加批次维度

为什么在PyTorch中需要添加批次维度 在PyTorch中添加批次维度至图像或其他数据的实践是出于几个重要的考虑,这些考虑直接关系到如何设计和实现深度学习模型,以及如何利用现代计算资源进行高效计算。以下是详细解释为何在PyTorch中处理数据时需要添加批次…

docker-compose部署kafka

docker-compose.yaml文件 version: 3 services:zookeeper:image: bitnami/zookeeper:latestcontainer_name: zookeeperenvironment:- ALLOW_ANONYMOUS_LOGINyesnetwork_mode: hostkafka:image: bitnami/kafka:latestcontainer_name: kafkaenvironment:- KAFKA_BROKER_ID1- KAF…

Unity-Particle System属性介绍(一)基本属性

什么是ParticleSystem 粒子系统是Unity中用于模拟大量粒子的行为的组件。每个粒子都有一个生命周期,包括出生、运动、颜色变化、大小变化和死亡等。粒子系统可以用来创建烟雾、火焰、水、雨、雪、尘埃、闪电和其他各种视觉效果。 开始 在项目文件下创建一个Vfx文件…