【读书笔记-《Linux内核设计与实现》-1 进程管理】

这本书并不厚,惭愧的是结合其他教程,读了好几遍才算基本弄懂。个人觉得适合作为学习Linux操作系统的一本进阶一些的教材。
本章介绍进程相关的知识,思维导图如图所示。
在这里插入图片描述

1.1 进程的基本概念

进程不仅仅局限于可执行程序代码,通常还包含其他资源,如打开的文件,挂起的信号,一个或多个具有内存映射的内存地址空间,一个或多个执行线程,存放全局变量的数据段等。进程是处于执行期的程序以及相关资源的总称。

执行线程是进程中活动的对象,每个线程都拥有一个独立的程序计数器,进程栈和一组进程寄存器。内核调度的对象是线程。对Linux而言,线程与进程并无特别区分,线程相当于一种特殊的进程。

1.2 进程描述符及任务结构

1.2.1 进程描述符

内核将进程列表存放在称为任务队列的双向循环链表中。链表中的项类型为task_struct,称为进程描述符,进程描述符包含一个具体进程的所有信息,可以完整描述一个正在执行的程序。

对于x86硬件体系,进程在进程栈的栈底(向下增长的栈)创建结构struct thread_info。struct thread_info在文件<asm/thread_info.h>中定义如下,其中的task域包含了指向该进程的task_struct指针。如下图所示:
在这里插入图片描述

struct thread_info
{
	struct task_struct *task;
	struct exec_domain 			*exec_domain;
	__u32						flags;
	__u32						status;
	__u32						cpu;
	int							preempt_count;
	mm_segment_t				addr_limit;
	struct reastart_block		restart_block;
	void						*sysenter_return;
	int							uaccess_err;
	
};

因此在x86系统中,可以通过current_thread_info函数在栈底找到thread_info结构,并从中找到task_struct地址:

current_thread_info()->task;

内核通过PID来标识每个进程,每个进程的PID存放在进程各自的描述符中。

1.2.2 进程状态

进程共有以下几种状态:

  • TASK_RUNNING(运行) 进程是可执行的,或正在执行,或在运行队列中等待执行。进程在用户空间中执行的唯一可能状态
  • TASK_INTERRUPTIBLE(可中断) 进程正在睡眠,等待条件达成。条件达成后,内核会将进程状态设置为运行;或接收到信号被唤醒。
  • TASK_UNINTERRUPTIBLE(不可中断) 接收到信号也不会被唤醒,使用较少
  • __TASK_TRACED 被其他进程跟踪的进程
  • __TASK_STOPPED(停止) 进程没有投入运行也不能投入运行,发生在接收到SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU等信号的时候。

进程状态的转化如图所示:

在这里插入图片描述
内核可以通过set_task_state(task, state)函数设置进程状态:

set_task_state(task, state);		//将任务task状态设置为state

1.2.3 进程上下文

一般程序在用户空间执行,当一个程序执行了系统调用或触发了某个异常,就会陷入内核空间。

进程只有通过系统调用和异常处理程序才能陷入内核。

1.2.4 进程家族树

所有进程都是PID为1的init进程的后代。

系统中每个进程都有一个父进程,有零个或多个子进程。父进程相同的子进程称为兄弟进程。每个task_struct包含一个指向其父进程task_struct称为parent的指针,以及一个称为children的子进程链表。

可以通过如下代码获取当前进程的父进程的进程描述符:

struct task_struct *my_parent = current->parent;

init进程的进程描述符是作为init_task静态分配的。可以通过如下代码找到init进程指针:

struct task_struct *task;
for(task=current; task!=&init_task; task=task->parent)
//经过for循环后task指向init

1.3 进程创建

Linux的进程不是直接产生的,而是通过复制已有进程产生的,进程的创建主要用到fork()与exec()两个函数。

首先fork()通过复制当前进程创建一个子进程,随后exec()读取可执行文件并将其载入地址空间开始运行。

1.3.1 fork()

fork()函数调用clone(),clone()调用do_fork(),do_fork()函数完成创建中的主要工作。do_fork()调用copy_process()函数完成以下工作:

  • 调用dup_task_struct()为新进程创建内核栈、thread_info结构与task_struct,此时这些值与当前进程相同
  • 子进程开始于父进程区别开来,初始化进程描述符中的许多成员
  • 子进程状态设置为TASK_UNINTERRUPTIBLE,确保不会投入运行
  • 调用copy_flags()函数更新task_struct的flags成员
  • 调用alloc_pid()为新进程分配一个有效的PID
  • 根据传递给clone()的参数标识,copy_process()复制或共享打开的文件,文件系统信息,信号处理函数,进程地址空间和命名空间等。
  • copy_process()返回一个指向子进程的指针

返回到do_fork()函数,如copy_process()函数成功返回,新创建的子进程被唤醒并投入运行。

1.3.2 线程创建

在Linux中,线程被视为一个与其他进程共享某些资源的进程。

线程的创建于普通进程创建类似,只是在调用clone()时需传递一些参数标志来指明需共享的资源:

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

对比一个普通fork()的实现:

clone(SIGCHLD, 0);

内核需要在后台完成的操作可以通过内核线程来完成。内核线程只能在内核空间中运行。可以通过kthread_run()来创建并运行一个内核进程:

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

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

1.4 进程终结

一个进程终结时,内核必须释放它所占有的资源并通知其父进程。

进程终结发生在调用exit()系统调用时,既可能是显式调用,也可能是隐式地从某个主函数返回。当进程接受到不能处理也不能忽略地信号或异常时,也可能被动地终结。无论如何终结,大部分工作需要由do_exit()完成。

该函数完成资源释放,调用exit_notify向其父进程发送信号,并给其子进程重新寻找养父。养父为线程组中的其他线程或init进程。

此时进程处于EXIT_ZOMBIE状态,用于向父进程提供信息。父进程确认为无关信息后,该进程占用的内存被释放,归还给系统,进程描述符也被删除。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值