[Linux][内核学习笔记]--进程管理

进程是什么?

 进程是Linux系统抽象出来的,表示正在执行程序的实时结果。进程不等于程序,程序是静态地存储在磁盘上,而进程活动于操作系统中。多个进程有可能在执行同一份程序代码。进程除了包括程序代码外,还包括其他资源,像打开的文件、挂起的信号、内核数据、处理器状态、一个或多个具有内存映射的内存地址空间、一个或多个执行线程等。

 线程是进程中的活动对象,一个进程对应多个线程。线程共享进程的部分资源,像代码段、数据段、堆、当前目录、用户ID、组ID、打开的文件描述符等,也独有资源,像栈空间、程序计数器、程序运行时用到的寄存器等。

 Linux内核对进程和线程没有特别区分,在实现上没有特别的调度算法或定义数据结构来标识线程,进程和线程都是用相同的进程PCB数据结构。内核里使用clone()方法来创建线程,其工作方式与fork()类似。但是会区分哪些资源与进程共享,哪些资源是线程独有。

 Linux系统通过fork()系统调用复制父进程来创建子进程,接着调用exec()函数来创建新的地址空间,并把新的程序载入其中。最终,程序通过exit()系统调用退出执行,进入僵死状态,等待父进程调用wait()或waitpid()函数回收其资源。

 进程上下文 : 当用户程序call系统调用或发生某种异常时,会陷入到内核空间。此时称内核“代表进程执行”并处于进程上下文。

进程描述符及任务结构

 进程的所有信息被task_struct数据结构所描述,通常称它为进程PCB。每一个进程对应一个PCB,系统中的所有PCB存放在一个双向循环链表中,称这个链表为任务队列。

 在早期寄存器较弱的处理器时代,为了避免消耗额外的寄存器来保存PCB的位置,在2.6以前的内核版本中,将PCB存放在内核栈的底部位置,这样可以方便地通过栈指针来获得PCB的位置。后来,为避免动态分配和释放PCB结构所带来的系统消耗,采用slab机制预先分配和重复使用PCB的方法,进而为了在汇编代码中能够更加容易计算偏移量,内核栈的底部位置用thread_info结构替换PCB结构,PCB结构被包含在thread_info结构中。

 内核中current全局变量利用内核栈的特性可以获取当前进程的PCB。首先,从SP寄存器中得到当前内核栈的地址,然后获取到 thread_info数据结构的指针。进而 thread_info->task的到PCB结构。

static inline struct thread_info *current_thread_info(void) __attribute_const__;

static inline struct thread_info *current_thread_info(void)
{
	return (struct thread_info *)
		(current_stack_pointer & ~(THREAD_SIZE - 1));//current_stack_pointer 通过汇编获得内核栈的栈顶指针并屏蔽掉低13位,得到存放thread_info的地址
}

#define get_current() (current_thread_info()->task)
#define current get_current()

进程创建

在这里插入图片描述

 用户程序通过fork()、vfork()、clone()系统调用创建进程,进入到内核空间,最后都调用到do_fork(),区别只是调用的参数不一样。

  1. do_fork()函数原型:
long do_fork(unsigned long clone_flags, 
	      unsigned long stack_start,
	      unsigned long stack_size,
	      int __user *parent_tidptr,
	      int __user *child_tidptr)
  • clone_flags:标志位
  • stack_start:用户态栈的起始地址
  • stack_size:用户态的大小,通常为0
  • parent_tidptr:指向父进程ID
  • child_tidptr:指向子进程ID

fork、vfork、clone调用do_fork()的传参有什么不同:

fork():

 do_fork(SIGCHILD,0,0,NULL,NULL)

 SIGCHILD表示在子进程终止后发送信号通知父进程。fork()采用写时复制机制,是一种可以推迟甚至免除拷贝数据的技术。此时内核并不复制整个进程地址空间,而是父子进程共享地址空间,仅复制了父进程的页表,不会复制页面的内容,只有当子进程需要写数据时才会真正地复制一个副本。

vfork():

 do_fork(SIGCHILD|CLONE_VFORK|CLONE_VM,0,0,NULL,NULL)

 vfork() 比 fork() 多了两个标志位。CLONE_VFORK表示父进程会被挂起,直到子进程释放虚拟内存资源。CLONE_VM表示父子进程共享虚拟内存空间。

clone():

 do_fork(clone_flags,newsp,0,parent_tidptr,child_tidptr)

 用于创建线程,参数通过寄存器从用户空间传下来,通常会指定新的栈地址,所以说线程拥有独立的栈空间。

kernel_thread():

 do_fork(flags|CLONE_VM|CLONE_UNTRACED,(unsigned long)fn,(unsigned long)arg,NULL,NULL)

 用于创建内核线程。

在include/uapi/linux/sched.h中定义了很多clone_flags标志:

#define CSIGNAL		0x000000ff	/* signal mask to be sent at exit */
#define CLONE_VM	0x00000100	/* set if VM shared between processes */
#define CLONE_FS	0x00000200	/* set if fs info shared between processes */
#define CLONE_FILES	0x00000400	/* set if open files shared between processes */
#define CLONE_SIGHAND	0x00000800	/* set if signal handlers and blocked signals shared */
#define CLONE_PIDFD	0x00001000	/* set if a pidfd should be placed in parent */
#define CLONE_PTRACE	0x00002000	/* set if we want to let tracing continue on the child too */
#define CLONE_VFORK	0x00004000	/* set if the parent wants the child to wake it up on mm_release */
#define CLONE_PARENT	0x00008000	/* set if we want to have the same parent as the cloner */
#define CLONE_THREAD	0x00010000	/* Same thread group? */
#define CLONE_NEWNS	0x00020000	/* New mount namespace group */
#define CLONE_SYSVSEM	0x00040000	/* share system V SEM_UNDO semantics */
#define CLONE_SETTLS	0x00080000	/* create a new TLS for the child */
#define CLONE_PARENT_SETTID	0x00100000	/* set the TID in the parent */
#define CLONE_CHILD_CLEARTID	0x00200000	/* clear the TID in the child */
#define CLONE_DETACHED		0x00400000	/* Unused, ignored */
#define CLONE_UNTRACED		0x00800000	/* set if the tracing process can't force CLONE_PTRACE on this clone */
#define CLONE_CHILD_SETTID	0x01000000	/* set the TID in the child */
#define CLONE_NEWCGROUP		0x02000000	/* New cgroup namespace */
#define CLONE_NEWUTS		0x04000000	/* New utsname namespace */
#define CLONE_NEWIPC		0x08000000	/* New ipc namespace */
#define CLONE_NEWUSER		0x10000000	/* New user namespace */
#define CLONE_NEWPID		0x20000000	/* New pid namespace */
#define CLONE_NEWNET		0x40000000	/* New network namespace */
#define CLONE_IO		0x80000000	/* Clone io context */
  • CLONE_VM:父子进程共享虚拟内存空间。
  • CLONE_FS:父子进程共享同一个文件系统,像根目录、当前工作目录等。
  • CLONE_FILES:父子进程共享文件描述表。文件描述表保存文件描述符信息,因此一个进程打开的文件描述,另一个文件描述符也可以使用。
  • CLONE_SIGHAND:父子进程共享信号处理函数表。
  • CLONE_PTRACE:父进程被跟踪,子进程也会被跟踪。
  • CLONE_VFORK:父进程被挂起,直到子进程释放了虚拟内存资源。
  • CLONE_PARENT:拥有同一个父进程。init进程或容器中的init进程不允许创建兄弟进程,否则该进程无法被init进程回收,变成僵死进程。
  • CLONE_THREAD:父子进程共享相同的线程群。
  • CLONE_NEWNS:表示父子进程不共享mount namespace。
  • CLONE_NEWUSER:表示子进程要创建新的User Namespace , User Namespace 用于管理User ID和Group ID的映射,起到隔离User ID的作用。
  • CLONE_NEWPID:表示创建一个新的PID namespace。在没有PID namespace之前,进程唯一的标识是PID,在引入PID namespace之后,标识一个进程需要PID namespace和PID 双重认证。CLONE_NEWUSER、CLONE_NEWPID和CLONE_SIGHAND共享信号会冲突。
  • CLONE_IDLETASK:将PID设置为0(只供idel进程使用)。
  1. copy_process()函数原型:
static __latent_entropy struct task_struct *copy_process(
					struct pid *pid,
					int trace,
					int node,
					struct kernel_clone_args *args)
  • copy_process函数首先会对clone_flags标志进行合法性检查,当出现以下三种情况时,返回出错代号:

    (1) CLONE_NEWNS和CLONE_FS同时被设置。前者标志表示父子进程不共享mount namespace(文件系统挂载点),子进程创建新的mount namespace ,而后者标志则表示父子进程共享文件系统信息,两者不可兼容。

    (2) CLONE_NEWUSER和CLONE_FS同时被设置。前者主要用来隔离安全相关的标识符(idebtifier)和属性(attribute),包括用户ID、用户组ID、root目录、key(密钥)以及特殊权限。与后者会产生矛盾。

    (3) CLONE_THREAD被设置,但CLONE_SIGHAND未被设置。如果子进程和父进程属于同一个线程组(CLONE_THREAD被设置),那么子进程必须共享父进程的信号(CLONE_SIGHAND被设置)。

    (4) CLONE_SIGHAND被设置,但CLONE_VM未被设置。如果子进程共享父进程的信号,那么必须同时共享父进程的内存描述符和所有的页表(CLONE_VM被设置)。

    (5) CLONE_PARENT被设置,并且当前进程的信号 SIGNAL_UNKILLABLE被设置。SIGNAL_UNKILLABLE 只有init进程才会被设置,如果init进程创建了兄弟进程,那么子进程的资源将永远不会被回收,成为僵死进程。

    (6) CLONE_NEWPID表示创建一个新的PID namespace。在没有PID namespace之前,进程唯一的标识是PID,在引入PID namespace之后,标识一个进程需要PID namespace和PID 双重认证。CLONE_NEWUSER、CLONE_NEWPID和CLONE_SIGHAND共享信号会有冲突。

  • 复制进程描述:dup_task_struct()首先分配struct task_struct 和 struct thread_info数据结构实例,接着复制文件描述符和thread_info结构数据,只是将子进程的进程描述符和thread_info结构指向父进程的进程描述符和thread_info,然后设置进程不被立即调度,因为子进程还没有完全诞生。

  • 复制父进程证书。

  • 判断系统当前的进程数是否超过了最大进程数。

  • 到此,子进程与父进程的进程描述完全一样,后面开始初始化进程描述符的部分成员,父子进程逐渐区分开

  • 初始化进程调度相关的数据结构,抽象每个进程或线程作为一个调度实体。

  • 设置进程状态为TASK_RUNNING,此时进程还不能开始运行,因为它还没有加入就绪队列中,外部事件或信号不能唤醒它。

  • 设置默认的调度策略和调度优先级。进程优先级分为普通进程优先级(100~139)、实时进程优先级(0-99)、deadline 进程优先级(-1)。

  • 初始化thread_info部分成员,获取当前CPU ID。初始化preempt_count计数(为了支持内核抢占而引入该字段)。当preempt_count为0时,表示内核可以被抢占。

  • 复制父进程的文件系统信息、信号系统、IO系统、内存空间、处理器相关寄存器的值等。

  • 分配新的pid结构,获取新进程真正的pid,并设置线程组。增加系统当前进程计数,返回。

Q&A

  1. 在内核中如何获取当前进程的 task_struct数据结构?
     答:current
  2. 下面程序会打印出几个"_" ?
    在这里插入图片描述
     答:6
  3. 用户空间进程的页表是什么时候分配的,其中一级页表什么时候分配?二级页表?
     答:copy_process()–>copy_mm()–>dup_mm()–>mm_init()–>pgd_alloc
  4. 请简述fork、vfork、clone之间的区别?
     答:fork创建的子进程有独立的内存空间。vfork创建的子进程没有独立的内存,父子进程共享地址空间。clone功能强大,函数实现复杂,可以选择性的继承父进程的资源,也可以选择像vfork一样与父进程共享一个进程地址空间,从而使创造的是线程,也可以不和父进程共享,甚至可以选择创造出来的进程和父进程不再是父子关系,而是兄弟关系。

内核线程

 内核经常需要在后台执行一些任务,像flush和ksoftirqd这些任务通过内核线程实现,内核线程是独立运行在内核空间的标准进程,它跟普通进程的区别在于内核线程没有独立的地址空间,mm指针被设定为NULL。

 下 ps -ef 指令可以查看系统中的内核线程,实际上内核线程也是由其他线程创建,所有内核线程的鼻祖是kthreadd。新的内核线程可以通过调用kthread_create函数创建,然后再调用wake_up_process()唤醒它,或者直接调用kthread_run创建并运行新内核线程。kthread_stop函数停止一个内核线程。

进程终结(清理工作)

 当进程调用exit()或者从main()函数返回时,代表进程的生命周期即将结束。进程最终会调do_exit()函数去释放所持有的所有资源:

  • 将task_struct中的标志成员flags设定为PF_EXITING。
  • 调用del_timer_sync()删除任一内核定时器。根据返回结果,确保没有内核定时器在排队,也没有内核定时器处理程序在运行。
  • 如果进程的记账信息(记账记录所需的信息,比如命令名的少量二进制数据、使用 的CPU时间量、用户ID和组ID、开始时间等,记账信息被内核保存在进程表里,并当任何一个新进程创建时被初始化。每个记账信息当一个进程终止时都被写入)是开启的,do_exit()调用acct_update_integrals()来输出记账信息。
  • 调用exit_mm()函数释放进程占用的mm_struct,如果没有进程共享它的进程地址空间,则彻底释放mm_struct。
  • 调用sem_exit()函数。如果进程排队等候IPC信号,它则离开队列。
  • 调用exit_files()和exit_fs(),分别递减文件描述符、文件系统数据的引用计数,如果引用计数减到0,说明没有其他进程在使用相关资源,则可以释放。
  • 接着把存放在task_struct的exit_code成员中的任务退出代码设定为由exit()提供的退出代码,或者去完成其他由内核机制规定的退出动作。退出代码存放在这里供父进程随时检索。
  • 调用exit_notify()向父进程发送信号,给子进程重新找养父,养父为线程组中的其他线程或者为init进程,并把进程状态exit_state设为EXIT_ZOMBIE。
  • do_exit()调用schedule切换到新的进程。EXIT_ZOMBIE状态的进程不会再调度,所以这是进程所执行的最后一段代码。do_exit()永不返回(__noreturn )。

void __noreturn do_exit(long code)

到这里,与进程相关资源已全部被释放。但进程仍然还存在,并没有消失,还占有内核栈、thread_info结构和task_struct结构,它还需要向父进程提供信息,当父进程检索到信息,确认是无关信息后,进程的剩余内存才会被释放,归还给系统。

删除进程描述符

 清理工作和删除文件描述符是分开执行的。进程描述符是为了让子进程终结后父进程有办法获得子进程的信息。在父进程确认子进程信息是无关信息后,task_struct结构才会被释放。

 父进程中调用的wait一族函数就是在等待子进程退出,然后回收资源的。在最终需要释放进程描述符时,release_task()才会被调用,release_task会去释放进程的内核栈、thread_info结构和task_struct 结构。进程描述符以及进程独享资源才能完全被释放。

孤儿进程

 当父进程在子进程之前退出,子进程成为孤儿进程,系统无法回收子进程占用的资源,会白白浪费掉资源。解决方法是在当前线程组内找一个线程做父亲,或直接让init进程当父亲。do_exit()函数中会调用exit_notify()函数,该函数会调用forget_original_parent(),forget_original_parent()再调find_new_reaper来执行寻父过程。

参考资料
[1] 《Linux 内核设计与实现 第三版》
[2] 《奔跑吧Linux内核》
[3] Linux kernel 5.4.1

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值