第三章 进程管理

本文详细介绍了Linux系统中的进程管理,包括进程的定义、状态、创建(fork()、vfork())、线程创建、内核线程、进程终止以及资源的分配和释放。重点阐述了fork()如何通过clone()实现,以及线程在Linux中被视为特殊进程的方式。此外,还讨论了进程描述符、进程状态设置、孤儿进程处理等核心概念。
摘要由CSDN通过智能技术生成

第三章 进程管理

进程

进程是一个动态的概念,是处于执行期的程序。也包含许多资源,可执行程序代码(代码段,text section),打开的文件,挂起的信号,内核内部数据,处理器状态,一个或多个具有内存映射的内存地址空间,一个或多个执行线程(thread of execution),存放全局变量的数据段等。
执行线程,简称线程(thread),是在进程中活动的对象。每个线程都具有一个独立的程序计数器、进程栈和一组进程寄存器。内核调度的对象是线程而不是进程。
LInux系统的线程实现非常特别,对线程和进程并不进行特别区分,线程是特殊的进程。

Linux进程创建到结束的大致流程如下:

  1. fork()系统调用通过复制一个现有进程来创建一个全新的进程,从内核返回两次,一次父进程,一次新产生的子进程。fork实际上是由clone()系统调用实现的。
  2. exec()一组函数可以创建新的地址空间,并把新的程序载入其中。因此新的进程就可以执行新的、不同的程序。
  3. exit()系统调用退出执行,终结进程并释放所占用的资源。父进程可以使用wait4()查询子进程是否终结,其实使得进程拥有了等待特定进程执行完毕的能力。
  4. 进程退出执行后被设置为僵死状态,只到它的父进程调用wait()或waitpis()为止。

进程描述符及任务结构

进程在linux也被叫做任务(task)。
进程存放在任务队列(task_list)的双向循环链表中。链表中的每一项都是进程描述符(process descriptor,tast_struct)的结构,包含了一个具体进程的所有信息。在<linux/sched.h>头文件中定义。
tast_struct的结构相对较大,包含了内核管理一个进程所需的所有信息。进程描述符中包含的数据能完整的描述一个正在执行的程序(进程):它打开的文件、进程的地址空间,挂起的信号,进程的状态,还有其它更多信息。

分配进程描述符

Linux使用slab分配器分配task_struct结构,只需在栈底(对于向下生长的栈)或栈顶(对于向上生长的栈)创建一个struct thread_info结构。
在x86上,struct thread_info定义在<asm/thread_info.h>。
每个任务(进程)的thread_info结构在它的内核栈的尾端分配,结构中task域中存放的是指向该任务实际的task_struct的指针。

进程描述符的存放

内核通过进程标识值(process identification value)或PID来标识每个进程。PID是一个数,表示为pid_t隐含类型,实际上就是一个int类型。PID的最大值可以由管理员修改/proc/sys/kernel/pid_max。
current宏查找当前正在运行进程的进程描述符,使用非常频繁,速度很重要。硬件体系结构不同,该宏的实现也不同,必须针对专门的硬件体系结构做处理。有的硬件体系结构(PowerPC,PPC)可以拿出一个专门的寄存器用来存放当前进程的task_struct的指针。而有些像x86这样的体系架构(其寄存器并不富裕),就只能在内核栈的尾端创建thread_info结构,通过计算偏移量间接的查找task_struct结构。

进程状态

进程描述符中的state域描述了进程的当前状态。

状态标志状态含义
TASK_RUNNING运行进程是可执行的。正在执行,或者等待执行
TASK_INTERRUPTIBLE可中断进程正在睡眠(阻塞),等待某些条件的达成。一旦达成,内核把进程状态设置为运行
TASK_UNINTERRUPTIBLE不可中断除了接收到信号也不会唤醒或准备投入运行,其它与可中断相同。在进程必须在等待时不收干扰或等待事件很快发生时使用
__TASK_TRACED被跟踪被其它进程跟踪的进程,如ptrace对调试程序进行跟踪
__TASK_STOP停止进程停止执行,进程没有投入运行也不能投入运行

设置当前进程状态

调整某个进程的状态,最好使用下面的这个函数。

set_task_state(task,state);

set_current_state()和set_task_state(current,state)的含义是等同的。

进程上下文

用户空间程序执行系统调用或触发某个异常,会陷入了内核空间。此时,内核执行进程并处于进程上下文中,,current宏是有效的。
系统调用和异常处理程序是对内核明确定义的接口。进程只有通过这些接口才能陷入内核执行——对内核的所有访问都要通过这些接口。

进程家族树

Linux的进程之间存在一个明显的继承关系,所有的进程都是PID为1的init进程的后代。内核在系统启动的最后阶段启动init进程。该进程读取系统的初始化脚本(initscript)并执行其它的相关程序,最终完成系统启动的整个过程。进程间的关系放在进程描述符中。每个task_struct都包含一个执行其父进程task_struct、叫做parent的指针,还包含一个称为children的子进程链表。

/*父进程的获取*/
struct task_struct *my_parent=current->parent;
/*访问子进程*/
struct task_struct *task;
struct list_head *list;

list_for_each(list,&current->children){
	task=list_entry(list,struct task_struct,sibling);
	/*task现在指向当前的某个子进程*/
}

进程创建

许多其它的操作系统都提供了产生(spawn)进程的机制,首先在新的地址空间里创建进程,读入可执行文件,最后开始执行。
Linux将上述过程分解到fork()和exec()两个函数中去。

写时拷贝copy-on-write

只有在需要写入的时候数据才会被复制,从而使各个进程拥有各自的拷贝。在此之前,只是以只读方式共享。
frok()的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符。

fork()

Linux通过clone()实现fork(),使用一系列的参数指定父、子进程之间需要共享的资源。fork()、vfork()、_clone()都根据各自的参数标志去调用clone(),clone()再去调用do_fork()。
do_fork()完成创建中的大部分工作,定义在kernel/fork.c文件中,该函数调用copy_process()函数,然后让进程开始运行。
copy_process()的过程:

  1. 调用dup_task_struct()为新进程创建一个内存栈、thread_info结构和task_struct,这些值与当前进程的值相同。此时,子进程和父进程的描述符是完全相同的。
  2. 检查并确保创建这个子进程后,当前用户的进程数目没有超出分配资源的限制。
  3. 子进程使自己与父进程区分开来。进程描述符内的许多成员要被清0或设为初始值。
  4. 子进程状态设置为TASK_UNINTERRUPTIBLE,保证不会投入运行。
  5. 调用copy_flags()更新task_struct的flags的成员。表明是否拥有超级用户权限的PF_SUPERPRIV标志清零。表明进程还没有调用exec()函数的PF_FORKNOEXEC标志被设置。
  6. 调用alloc_pid()为子进程分配一个有效的PID。
  7. 根据传递给clone()的参数标志,copy_process()拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间。
  8. 最后做扫尾工作并返回一个指向子进程的指针。

回到do_fork()函数,如果copy_process()函数成功返回,新建的子进程被唤醒并投入运行。
内核有意选择子进程首先执行。

vfork()

除了不拷贝父进程的页表项外,vfork()和fork()的功能相同。不推荐使用。

线程

线程机制是现代编程技术中常用的一种抽象概念。该机制体提供了在同一程序内共享内存地址空间运行的一组线程。这些线程还可以共享打开的文件和其他资源。线程机制支持并发程序设计技术,在多处理系统上,能保证真正的并行处理。
在LInux中,线程仅仅被视为一个与其他进程共享某些资源的进程,是一个特殊的进程。

创建线程

与普通进程创建类似,在调用clone()是要传递一些参数标志来指明共享的资源。

clone(CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND,0);

一个普通的fork()的实现是:

clone(SIGCHLD,0)

vfork()的实现是:

clone(CLONE_VFORK|VLONE_VM|SIGCHLD,0)

传递给clone()的参数决定了新创建进程的行为方式和父子进程之间共享的资源种类。

内核线程

内核线程与普通的进程间的区别在于内核线程没有独立的地址空间。只在内核空间运行,从来不切换到用户空间。内核进程和普通进程一样,可以被调度,也可以被抢占。
内核线程只能由其他内核线程创建。内核是通过从kthreadd内核进程中衍生出所有新的内核进程来自动处理者一点的。<linux/kthread.h>声明接口。
从现有内核线程创建一个新的内核线程的方法:

struct task_struct *kthread_create(
	int (*threadfn)(void *data),
	void *data,
	const char namefmt[],
	...
)

新的任务是有kthread内核进程通过clone()创建的。新的进程将运行threadfn函数,给其传递data参数。进程被命名为namefmt,namefmt接受类似printf()的格式化参数的的可变参数列表。新创建的进程处于不可运行的状态,需要调用wake_up_process()唤醒。

可以使用kthread_run()来新建并运行一个进程。这个例程是以宏实现的,实际只是简单的调用了kthread_create()和wake_up_process()。

struct task_struct *kthread_run(
	int (*threadfn)(void *data),
	void *data,
	const char namefmt[],
	...
)

内核线程启动后一直运行直到调用do_exit()退出,或者内核的其他部分调用kthread_stop()退出。

int kthread_stop(struct task_struct *k)

终结

一个进程终结时,必须释放它所占有的资源并通知父进程。
发生在调用exit()系统调用时,大部分工作靠do_exit()完成(kernel/exit.c),其工作如下。

  1. 将task_struct的标志成员设置为PF_EXITING。
  2. 调用del_timer_sync()删除内核定时器。
  3. 如果BSD的记账功能开启,调用acct_update_integrals()输出信息。
  4. 调用exit_mm()释放进程占用的mm_struct,如果没有其他进程使用则彻底释放。
  5. 调用sem__exit()函数。如果排队等待IPC信号,离开队列。
  6. 调用exit_files()和exit_fs(),分别递减文件描述符、文件系统数据的引用计数。如果为零,可以释放。
  7. 把存放在task_struct的exit_code置为由exit()提供的退出代码,或者完成一些特定的退出动作。退出代码供父进程检索。
  8. 调用exit_notify()向父进程发送信号,给子进程重新找养父。养父线程为线程组中的其他线程或者init进程,把进程状态(task_exit_stateexit_state)设置为EXIT_ZOMBIE。
  9. 调用schedule()切换到新的进程。
    此时,进程占用的内存就是内存栈、thread_info和task_struct结构,其他资源已被释放,存在的唯一目的就是像父进程提供信息。

删除进程描述符

当父进程获得已终结子进程的信息后,或者通知内核它并不关心那些信息后,子进程的task_struct将会被释放。
wait()这一族的函数都是通过一个唯一且复杂的wait4() 实现的。标准动作是挂起调用它的进程,直到一个子进程退出,返回该子进程的PID。
释放进程描述符时,调用release_task():

  1. 调用__exit_signal(),该函数调用_unhash_process(),后者由调用detach_pid()从pidhash中删除该进程,同时也从任务列表中删除该进程。
  2. _exit_signal()释放目前僵死进程所使用的所有剩余资源 ,并进行最终统计和记录。
  3. 如果这个进程是线程组的最后一个进程并且领头进程以死掉,通知僵死的领头进程的父进程。
  4. 调用put_task_struct()释放进程内存栈和thread_info所占的页,并释放task_struct所占的slab高速缓存。

孤儿进程

如果父进程在子进程之前退出,必须给子进程找到一个新的父亲,在当前线程组中找一个线程或者让init()做为父进程。
相关函数为:

do_exit()

exit_notify()

forgret_original_parent()
find_new_reaper()
patrace_exit_finisf()

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值