IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 系统运维 -> linux内核设计和实现第三章节-----进程管理 -> 正文阅读

[系统运维]linux内核设计和实现第三章节-----进程管理

前言:

为什么要学习linux内核,随着ebpf的深入,对linux内核的熟悉程度,将会直接决定我们的kprobe的使用情况,尽管linux内核已经帮我们通过map定义了钩子

参考文献:

<<linux内核设计和实现>>

参考网址:

linux内核线程_huangweiqing80的博客-CSDN博客_内核线程

find_new_reaper - it610.com

3.1 进程

进程一般包括:可执行程序的代码,像打开的文件、挂起的信号,内核内部处理数据,处理器状态,一个或者多个具有内存映射的内存地址空间以及一个或者多个执行线程组成????????

执行线程简称线程,每个线程都有一个独立的程序计数器、进程栈、进程寄存器。内核调度的对象是线程而不是进程,现在多线程程序司空见惯,对linux内核而言进程和线程其实并不是区别特别明显。

现在操作系统中有两种虚拟机制,包括虚拟内存和虚拟处理器,可能多个进程共享一个处理器,虚拟处理器会给这些进程一个虚假现象,让他们认为自己是单独使用一个cpu,线程之间一般共享虚拟内存,都有各自的虚拟处理器。

程序本身并不是进程,进程是出于执行期间的程序以及相关的资源总称。完全可能存在两个或者两个进程执行同一个程序。并且两个或者两个以上并存的程序还可以共享打开的文件,虚拟内存之类的资源。

在linux系统忠,操作系统通过调用fork去创建一个进程,调用fork的是父进程,产生的新进程是子进程,fork之后一般调用exec开辟新的地址空间,在现代linux内核中,fork实际上是由clone系统调用实现的。通过调用wait4系统调用查询子进程是否终结,父进程调用wait或者waitpid来回收子进程。

3.2 进程描述符及任务结构

内核把进程的列表存放在叫做任务列表的双向链表中。链表中每一项类型都是task_struct、称为进程描述符(processer descriptor)的结构,该结构定义在<linux/sched.h> 文件中。进程描述符包含一个具体进程的所有信息。

进程描述符(process description)包括了打开的文件、进程的空间地址、挂起的信号以及进程的状态。

3.2.1 分配进程描述符

linux通过slab分配器分配task_struct 结构,这样能达到复用对象和缓存的着色的目的。各个进程的task_struct存放在它的内核栈尾端。这样做是为了让x86寄存器只要通过栈栈指针就能找到他的位置。

在x86上,struct thread_info 在文件 <asm/thread_info.h>中定义如下:

struct thread_info {
    struct task_struct *task;
    struct exec_domain *exec_domain;
    _u32    flag;
    _u32    status;
    _u32    cpu;
    int     preempt_count;
    mm_segment_t addr_limit;
    struct restart_block restart_block;
    void    *sysenter_return;
    int    uaccess_err;
};

?每个任务的thread_info结构在它的内核栈的尾端分配。结构中task域中存放的是指向该任务实际task_struct 的指针。

3.2.2 进程描述符的存放

内核通过一个唯一的进程标识值或pid来标识每个进程。pid默认的最大值是32768,尽管这个值也可以增加到400万。

如果确实需要修改的话我们可以通过修改/proc/sys/kernel/pid_max值来修改。

zhanglei@ubuntu:~$ cat /proc/sys/kernel/pid_max
4194304

内核大部分处理程序是通过task_struct进行的。因此通过current这个宏去访问描述符的速度就至关重要,current再从thread_info的task域中提取并且返回task_struct的地址。

current_thread_info()->task

3.2.3进程状态

TASK_RUNNING(运行)

TASK_INTERRUPTIBLE(可中断)

TASK_UNINTERRUPTIBLE(不可中断)

__TASK_TRACED(被其他进程跟踪的进程)

__TASK_STOPPED(停止)

3.2.4设置当前进程状态

内核进程需要调整某个进程状态。这时最好使用set_task_state(task, state)函数

/*将任务task的状态为state*/
set_task_state(task, state)

必要的时候设置内存屏障

task->state = state;

3.2.5 进程上下文

可执行程序代码是进程的重要组成部分。这些代码从一个可执行文件载入到进程地址空间执行。当一个程序执行系统调用就进入内核态了。我们称内核“代表进程执行”并处在进程上下文中,此时current宏依旧是有效的

3.2.6 进程家族树

所有的进程都是pid为1的init进程的后代。每个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);
}

init 进程的进程描述符是 作为init_task 静态分配的。下面的代码可以很好的显示所有进程的关系:

struct task_struct *task;

for(task = current; task != &init_task; task = task->parent)

/*task指向init*/

任务队列本来是一个双向列表,对于给定的进程,获取链表中的下一个进程

list_entry(task->tasks.next, struct task_struct, tasks);

获取上一个进程:

list_entry(task->tasks.prev, struct task_struct, tasks);

这两个分别通过 next_task(task)宏和prev_task(task)宏实现。

3.3 进程创建

一般通过fork 去调用,fork后很多进程会执行exec去替换可执行环境。

fork创建的进程区别仅仅在于pid、ppid和某些资源的统计量。

3.3.1 写时拷贝

这是我一个很久之前去面试过的问题了,早在unix是全量复制父进程内存,但是linux中做了改善,写时复制,只有在数据写入的时候,数据才会被复制。

3.3.2 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初始化值。那些不是继承而来的描述符成员,主要是信息。task_struct 许多成员依然未被修改

4)子进程的状态被设置为TASK_UNITERRUPTIBLE,以保证它不会投入运行。

5)copy_process 调用copy_flags 已更新task_struct 的flags成员。表明进程是否拥有超级用户权限的PF_SUPERPRIV 标志被清0.表明进程还没有调用exec函数的PF_FORKNOEXEDC标志被设置。

6) alloc_pid 为新进程分配一个pid

7)根据传递给clone参数标志,copy_process 拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。这些资源在进程间的所有线程之间共享;否则这些资源对每个进程是不同的,因此被拷贝到这里。

8)最后copy_process做扫尾工作并且返回一个指向子进程的指针。

fork 之后立马调用exec可以避免写时复制的开销。

3.3.3 vfork

除了不拷贝父进程的页表项外,vfork 和 fork调用基本相同。

vfork 是通过在clone中加入一个标志位来调用实现的。

vfork 是如何运行的?在linux2.2以前是这么运行的:

1)在调用copy_process时候,task_struct 的vfor_done成员被设置位NULL。

2)执行do_fork后,如果给定特别标志,vfork_done会指向一个特殊地址

3)子进程线开始执行后,父进程不是马上恢复执行,而是一直等待,知道子进程调用vfork_done指针向父进程发送信号

4)在调用mm_release时候这个函数用于进程退出内存地址空间,并且检查vfork_done是否是空,如果不是空,向父进程发送信号

5)回到do_fork,父进程醒来并且返回。

这样子进程在新的空间地址运行,父进程也恢复在原空间地址运行,这样降低了开销,但不是最优性能。

3.4线程在linux中实现

从linux内核角度看,内核看所有线程都当作进程来实现。内核并没有准备特别算法和数据结构,每个线程都有自己的task_struct ,所以它看起来像是一个普通进程。

3.4.1 创建线程

fork的clone实现

clone(SIGCHILD, 0);

vfork的clone实现:

clone(CLONE_VFORK | CLONE_VM | SIGCHILD, 0)

在<linux/sched.h>中定义的clone标志位:

/*
 * cloning 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 */

/* Flags for the clone3() syscall. */
#define CLONE_CLEAR_SIGHAND 0x100000000ULL /* Clear any signal handler and reset to SIG_DFL. */
#define CLONE_INTO_CGROUP 0x200000000ULL /* Clone into a specific cgroup given the right permissions. */

/*
 * cloning flags intersect with CSIGNAL so can be used with unshare and clone3
 * syscalls only:
 */
#define CLONE_NEWTIME   0x00000080      /* New time namespace */

3.4.2内核线程

内核线程和普通的线程区别在于没有地址空间,mm位NULL,内核线程只会在内核空间内运行,从来不会切换到用户空间。内核线程和普通线程一样,可以被调度,也可以被抢占。

在这里插入图片描述

pid1 是init 进程,pid 2 是内核线程?

zhanglei@ubuntu:~$ ps -ef
UID          PID    PPID  C STIME TTY          TIME CMD
root           1       0  0 18:35 ?        00:00:12 /sbin/init auto noprompt
root           2       0  0 18:35 ?        00:00:00 [kthreadd]

kthread底层也是调用clone 产生的,具体api:

task_struct  *kthread_create(int  (*threadfn)(void  *data),void  *data,const  char  *namefmt,  ...);

线程创建后,不会马上运行,而是需要将kthread_create()  返回的task_struct指针传给wake_up_process(),然后通过此函数运行线程。

kthread_run  
:创建并启动线程的函数:
struct  task_struct  *kthread_run(int  (*threadfn)(void  *data),void  *data,const  char  *namefmt,  ...);

kthread_stop:通过发送信号给线程,使之退出。
int  kthread_stop(struct  task_struct  *thread);

3.5 进程终结

一般来说进程析构是自己引起的 比如说调用exit,当然也可能是外部,比如说,发送信号

大部分调用do_exit内核都会进行下面的操作:

1)task_struct 结构体的标志位设置位PF_EXTING

2)调用del_timer_sync 删除内核的定时器。

3)如果由进程会计的记账信息,具体看unix高级环境编程,会输出进程会计信息

4) 调用 exit_mm() 释放占用的mm_struct

5) 调用sem__exit 函数。如果进程排队等待IPC信号,则离开队列。

6) 调用exit_files() 和 exit_fs(),分别递减文件描述符、文件系统数据的引用计数。如果其中某个引用技术的值降为0,那么久嗲表没有进程使用资源,则可以释放。

7) 接着把存放在task_struct 的exit 成员中的任务退出代码设置为exit提供的退出代码,或者去完成任何由其他内核机制规定的退出动作。退出代码放在这里供父进程检索使用。

8)调用exit_notify 向 父进程发送信号,给子进程重新找养父,养父位线程组中的其他线程或者位init进程,并把进程状态设置位EXIT_ZOMBILE

9) do_exit()调用schedule()切换到新的进程。因为处于EXIT_ZOMBILE状态的进程不会再次调度,这是进程执行的最后一段代码,do_exit 永远不返回。

到了这里进程变成了EXIT_ZOMBILE状态,但是进程内部的 task_struct 、thread_info等架构提荣然不会被释放,只有在父进程检索到信息后,通知内核是无关信息,由进程持有者去释放,归还资源给内核。

3.5.1 释放进程描述符

尽管调用do_exit()后,线程已经进入僵死状态,但是进程描述符task_struct 依然没有被释放,只有在父进程收到SIGCHILD通知后调用wait一族的函数才会被真正释放,退出步骤依次是:

1)调用__exit_signal(),该函数调用_unhash_process(),后者调用detach_pid()从pidhash上删除这个线程,同时也从任务列表中删除这个进程。

2)__exit_signal() 释放目前已经将死进程所使用的所有资源,并进行最后的统计和记录

3)如果这个进程是线程组最后一个进程,并且领头进程已经死了,那么release_task()就要通知将死进程的领头进程的父进程。

4)release_task() 调用put_task_struct() 释放进程内核栈和thread_info结构所占领的页,并且释放task_struct 所占领的slab高速缓冲区

至此进程描述符的资源被全部释放掉。

3.5.2 孤儿进程的进退维谷

如果父进程在子进程之前退出,必须有一个进制确保子进程能找到一个父进程,否则孤儿进程会永远僵死,白白占用内存。在do_Exit()中会调用exit_notify(),这个函数会调用forget_original_parent(),后者会调用find_new_reaper()来执行寻父过程

find_new_reaper()函数先在当前线程组中找一个线程作为父亲,如果找不到,就让init做父进程。(init进程是在linux启动时就一直存在的)

/*
 * When we die, we re-parent all our children.
 * Try to give them to another thread in our thread
 * group, and if no such member exists, give it to
 * the child reaper process (ie "init") in our pid
 * space.
 */
static struct task_struct *find_new_reaper(struct task_struct *father)
{
	struct pid_namespace *pid_ns = task_active_pid_ns(father);
	struct task_struct *thread;

	thread = father;
	while_each_thread(father, thread) {
		//遍历该结束的进程所在线程组的下一个进程
		if (thread->flags & PF_EXITING)//如果得到的下一个进程被标记了 PF_EXITING ,就不符合要求,需要继续遍历
			continue;
		if (unlikely(pid_ns->child_reaper == father))
			/*
			child_reaper 表示进程结束后,需要这个child_reaper指向的进程对这个结束的进程进行托管,
			其中的一个目的是对孤儿进程进行回收。
			若该托管进程是该结束进程本身,就需要重新设置托管进程,
			设置为该结束进程所在线程组的下一个符合要求的进程即可。
			*/
			pid_ns->child_reaper = thread;
		return thread;//在该结束进程所在的线程组中找到符合要求的进程,返回即可
	}

	/*
	如果该结束进程所在的线程组中没有其他的进程,
	函数就返回该结束进程所在命名空间的 child_reaper 指向的托管进程
	(前提是该托管进程不是该结束进程本身)
	*/
	if (unlikely(pid_ns->child_reaper == father)) {
		/*
		如果该结束进程所在命名空间的 child_reaper 指向的托管进程就是该结束进程本身,
		而程序运行至此,说明在该线程组中已经找不到符合要求的进程,
		此时,需要将托管进程设置为 init 进程,供函数返回
		*/
		write_unlock_irq(&tasklist_lock);
		if (unlikely(pid_ns == &init_pid_ns))
			panic("Attempted to kill init!");

		zap_pid_ns_processes(pid_ns);
		write_lock_irq(&tasklist_lock);
		/*
		 * We can not clear ->child_reaper or leave it alone.
		 * There may by stealth EXIT_DEAD tasks on ->children,
		 * forget_original_parent() must move them somewhere.
		 */
		pid_ns->child_reaper = init_pid_ns.child_reaper;
	}

	return pid_ns->child_reaper;
}

  系统运维 最新文章
配置小型公司网络WLAN基本业务(AC通过三层
如何在交付运维过程中建立风险底线意识,提
快速传输大文件,怎么通过网络传大文件给对
从游戏服务端角度分析移动同步(状态同步)
MySQL使用MyCat实现分库分表
如何用DWDM射频光纤技术实现200公里外的站点
国内顺畅下载k8s.gcr.io的镜像
自动化测试appium
ctfshow ssrf
Linux操作系统学习之实用指令(Centos7/8均
上一篇文章      下一篇文章      查看所有文章
加:2021-11-15 16:13:57  更:2021-11-15 16:15:03 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年12日历 -2024/12/24 11:27:40-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码
数据统计