文章目录
- 前言
- 一、任务 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操作系统