Linux 进程(查漏补缺版)

Linux 进程(查漏补缺版),第1张

Linux 进程(查漏补缺版)

文章目录

一、概述二、进程管理

2.1 进程描述符2.2 进程状态2.3 进程创建2.4 线程实现2.5 进程终结 三、进程调度

3.1 上下文切换 四、总结

一、概述

进程就是处于执行期的程序,通常包括内容:正文段、数据段、打开的文件、挂起的信号、内核内部数据、处理器状态,内存映射的地址空间、执行线程等。

线程是进程中活动的对象,每个线程都拥有一个独立的程序计数器、进程栈和一组进程寄存器。对于 Linux 来说,线程就是一种特殊的进程。

二、进程管理 2.1 进程描述符

内核把进程放在任务队列(一个双向循环链表中),链表的每一项,都是一个 task_struct 成为进程描述符号,也就是进程控制块。包含一个具体进程的所有信息。大约有 1.7 KB 的大小。

在寄存器较少的体系结构中,一般将当前的 task_struct 放在栈尾部,保存在一个 thread_info 中,通过栈顶指针和偏移量快速获取。

struct thread_info {
	struct task_struct	*task;              //当前进程描述符
	void			*dump_exec_domain;      //备份可执行领域
	unsigned long		flags;              //标志
	int			preempt_count;              //可抢占计数
	unsigned long		tp_value;
	mm_segment_t		addr_limit;         //地址的界限
	struct restart_block	restart_block;  //信号的实现
	struct pt_regs		*regs;
	unsigned int		cpu;                //标识当前cpu
};

另外,Unix 进程都存在一个明显的继承关系,所有的进程都是 PID 为 1 的 init 进程的后代。

进程在系统启动的最后阶段启动 init 进程,该进程读取系统的初始化脚本,并执行其他的相关程序,最终完成整个系统的启动。

进程描述符也维护了进程的继承关系,保存了父进程的 ID 以及子进程的 list

	struct task_struct __rcu	*parent;    //父进程引用
	struct list_head		children;       //子进程的链表
	struct list_head		sibling;        //兄弟进程,也就是相同父进程的进程
	struct task_struct		*group_leader;  //进程组的组长
2.2 进程状态

进程中的 _state 描述了进程状态,Linux 的进程状态和传统 *** 作系统五状态、三状态模型有一点区别。

系统中的每个进程都处于这五个状态之一,可以使用 ps -ef 或者 ps aux 查看进程状态。

TASK_RUNNING:运行态或者就绪态,唯一在用户空间或者内核空间执行的状态,对应 stat 的 R 状态。
TASK_INTERRUPTIBLE:可中断状态,浅度睡眠态,一般是主动进入睡眠等待某些条件达成,可以响应信号,而唤醒进入运行,对应查询的状态 S。
TASK_UNINTERRUPTIBLE:不可中断态,深度睡眠的状态,不会响应信号,也就是说,你发送一个 kill 的信号也不会将进程杀死。对应状态 D。(TASK_STOPPED 或 TASK_TRACED):表示进程被停止(往往是因为受到信号)或者因为被其他进程追踪而暂停,等待被追踪进程的 *** 作,对应状态 T。TASK_ZOMBIE:僵尸态,子进程运行结束,父进程没有感知以及没有通过 wait 函数回收子进程资源,子进程游离在系统成为僵尸进程,对应状态 Z。

内核通过通过 set_current_state 方法设置进程的状态。

#define set_current_state(state_value)					
	do {								
		debug_normal_state_change((state_value));		
		smp_store_mb(current->__state, (state_value));		
	} while (0)
2.3 进程创建

很多系统对于进程的创建提供了 spawn 的机制,也就是产生进程,包括,分配地址空间, 载入可运行文件,然后执行。而 Unix 又采用了不同的方式,将上述步骤分成 fork() 和 exec() 两部分,fork 通过拷贝父进程来创建一个子进程,而 exec 则读取可执行文件,并载入地址空间开始运行。

另外,为了 fork 之后快速的 exec,Unix 还提供了写时复制的机制,就是子进程fork父进程的时候,不会拷贝一个完全一样的副本,而是拷贝以及修改一些必要信息,共享其余的地址空间,并把空间的权限设置为只读,一旦有进程要进行修改 *** 作,就会引发异常,此时才会拷贝一份需要修改的地址空间,通常以页为单位,这样就可以节省很多拷贝的空间以及时间。Redis 的 RDB 持久化就是使用这种机制的很好案例。

fork()

fork 在父进程调用一次,父子进程各返回一次,父进程返回子进程 ID,子进程返回 0,内核通过返回值的不同来区分父子进程从而执行不同的任务。

Linux 使用系统调用 clone() 来实现 fork() ,拷贝一个子进程大部分内容都由 copy_process() 完成:

1)首先会通过 dup_task_struct 备份当前进程,为新进程创建一个内核栈、thread info 以及 task struct,此时和父进程是完全相同的。2)然后就通过一些数据校验,将子进程和父进程区分,为子进程的数据清零或者初始化。3)根据 clone_flag 的参数标志,拷贝或共享打开的文件描述符、信号处理函数、进程地址空间、命名空间等。4)最后,返回一个指向子进程的指针。

fork 的子进程继承父进程的如下参数:

子进程不继承父进程的两者区别如下:

fork 主要用于以下两种情况:

    类似于 Reactor 模型,父进程复制一个子进程执行不同的代码段,例如网络服务进程,父进程接受连接之后,交给子进程继续执行具体业务,而父进程继续监听连接。父进程要执行一个不同的程序,例如 shell 终端需要执行别的程序从而复制一个子进程,立即调用 exec。

vfork()

vfork 相对于 fork 的区别是,是否对父进程的页表进行复制,vfork 不能向地址空间中写入,并且保证父进程会在子进程 exec 或者 exit 后才会运行。为的是优化运行的效率,但是父进程依赖子进程的运行,可能导致死锁,以及现在写时复制的引入,所以最好不调用 vfork。

2.4 线程实现

从 Linux 内核的角度来说,它没有线程的概念,它也有唯一隶属于它的 task_struct ,所以仅仅被视为一个和其他进程共享某些资源的特殊进程。

对比于其他对于线程有特殊实现的 *** 作系统,需要维护一个进程空间,再维护指向该进程包含的线程,还要维护线程自己的结构。显然,Linux 只需要维护几个进程,并且制定他们共享哪些资源,实现的更高雅。

线程的创建也使用 clone() 系统调用来复制线程,只不过传入的参数有所不同,通过传入的参数来指明需要共享的资源。

创建线程使用:

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

fork() 使用:

clone(SIGCHLD, 0);

vfork() 使用:

clone(CLONE_VFORK | CLONE_VM | SIGCHLD, 0);

每个参数表达的意思如下:



内核线程

内核需要在后台执行一些 *** 作,这种任务就可以通过内核线程完成。内核线程也有一个存储信息的 task_struct。

内核线程和普通线程的区别在于,内核线程没有独立的地址空间,只在内核空间运行。

主要通过 kthread.h 中的 kthread_create、kthread_run 和 kthread_stop 来控制内核线程的创建,运行以及停止。

2.5 进程终结

进程的终结一般使用 kernel/exit.c 下的 do_exit 函数来实现,源码大致如下:

void __noreturn do_exit(long code)
{
	struct task_struct *tsk = current;
	int group_dead;

	// 省略一些校验和特殊处理

    // 将 task_struct 中的标志信号设置为 PF_EXITING
    exit_signals(tsk);  

    // 一些需要特殊退出的情况
    if (tsk->mm)
        sync_mm_rss(tsk->mm);
    // 输出记账信息
    acct_update_integrals(tsk);
    group_dead = atomic_dec_and_test(&tsk->signal->live);
    // 进程组消亡的情况做一些特殊处理
    if (group_dead) {
        if (unlikely(is_global_init(tsk)))
            panic("Attempted to kill init! exitcode=0x%08xn",
                  tsk->signal->group_exit_code ?: (int)code);

#ifdef CONFIG_POSIX_TIMERS
        // 删除内核定时器,以及取消一些定时处理程序
		hrtimer_cancel(&tsk->signal->real_timer);
		exit_itimers(tsk->signal);
#endif
		if (tsk->mm)
			setmax_mm_hiwater_rss(&tsk->signal->maxrss, tsk->mm);
	}
	acct_collect(code, group_dead);
	if (group_dead)
		tty_audit_exit();
	audit_free(tsk);
    // 把 exit_code 置为参数传入的 code ,表示该进程结束
	tsk->exit_code = code;
	taskstats_exit(tsk, group_dead);
    // 释放占用的 mm_struct (mm_struct 指的是进程的虚拟地址空间)
	exit_mm();

	if (group_dead)
		acct_process();
	trace_sched_process_exit(tsk);

	// 退出IPC信号等待
	exit_sem(tsk);
	// 释放 shm(shm 是基于内存的临时文件系统) 存储段
	exit_shm(tsk);
	// 释放递减文件描述符
	exit_files(tsk);
	// 递减文件系统数据的引用计数,变成 0 的话可以释放,上同
	exit_fs(tsk);
	if (group_dead)
		disassociate_ctty(1);
	// 释放任务的命名空间
	exit_task_namespaces(tsk);
	// 释放任务
	exit_task_work(tsk);
	// 释放该进程的线程
	exit_thread(tsk);
	
	// 做一些通知
	perf_event_exit_task(tsk);
	sched_autogroup_exit_task(tsk);
	cgroup_exit(tsk);
	flush_ptrace_hw_breakpoint(tsk);
	exit_tasks_rcu_start();
	
	// 向父进程发送信号,给子进程重新找养父,
	// 并设置进程状态为 EXIT_ZOMBIE 也就是僵尸进程
	exit_notify(tsk, group_dead);
	proc_exit_connector(tsk);
	mpol_put_task_policy(tsk);

	// 省略最后的一些校验和回收
	
	// 最后会调用 schedule() 切换其他进程运行
	
	
}

子进程结束退出之后,父进程就可以通过 wait 或者 waitpid 来等待回收子进程资源,wait 会阻塞等待一直到第一个退出的进程,而 waitpid 会等待制定 pid 进程,不会阻塞。

wait 函数还是使用系统调用 wait4 来实现,最终需要释放文件描述符时,release_task 函数会被调用。

void release_task(struct task_struct *p)
{
	struct task_struct *leader;
	struct pid *thread_pid;
	int zap_leader;
repeat:
	rcu_read_lock();
	dec_rlimit_ucounts(task_ucounts(p), UCOUNT_RLIMIT_NPROC, 1);
	rcu_read_unlock();

	cgroup_release(p);
    // 加锁
	write_lock_irq(&tasklist_lock);
	ptrace_release_task(p);
	// 获取进程的id
	thread_pid = get_pid(p->thread_pid);

	// 从 pidhash 以及任务列表中删除该进程
	// 释放所有剩余资源,并进行记录
	__exit_signal(p);

	zap_leader = 0;
	leader = p->group_leader;
	// 如果该进程是进程组的最后一个进程,则通知进程组 leader 进程的父进程回收资源
	if (leader != p && thread_group_empty(leader)
			&& leader->exit_state == EXIT_ZOMBIE) {
		zap_leader = do_notify_parent(leader, leader->exit_signal);
		if (zap_leader)
			leader->exit_state = EXIT_DEAD;
	}

	write_unlock_irq(&tasklist_lock);
	seccomp_filter_release(p);
	proc_flush_pid(thread_pid);
	put_pid(thread_pid);
	// 释放线程
	release_thread(p);
	// 释放内核栈、thread_info 所占的页以及 task_struct 所占的 slab 高速缓存
	// 至此,子进程的所有资源都被释放
	put_task_struct_rcu_user(p);

	p = leader;
	if (unlikely(zap_leader))
		goto repeat;
}

总结过程如下:

另外,如果父进程在子进程之前退出,就会通过 exit_notify 找寻别的父进程,为同进程组的进程或者 init 进程,来保证僵尸进程不会一直游离在系统内浪费资源,找到父进程对资源进行回收。

三、进程调度

进程调度算法就不多赘述了。

进程调度

3.1 上下文切换

Linux 进程的上下文切换就是从一个可执行进程切换到另一个可执行进程,在 kernel/sched.c 中的 context_switch 函数实现,当内核调用 schedule 运行进程的时候,就会调用该函数。

主要就做了两步 *** 作:

通过 switch_mm 函数切换虚拟内存;通过 switch_to 切换处理器状态;

static inline task_t * context_switch(runqueue_t *rq, task_t *prev, task_t *next)
{
	struct mm_struct *mm = next->mm;
	struct mm_struct *oldmm = prev->active_mm;

	if (unlikely(!mm)) {
		next->active_mm = oldmm;
		atomic_inc(&oldmm->mm_count);
		enter_lazy_tlb(oldmm, next);
	} else
	    // 切换新的 mm_struct 也就是虚拟内存
		switch_mm(oldmm, mm, next);

	if (unlikely(!prev->mm)) {
		prev->active_mm = NULL;
		WARN_ON(rq->prev_mm);
		rq->prev_mm = oldmm;
	}

	// 切换处理器状态,包括栈,寄存器等信息的保存和恢复
	switch_to(prev, next, prev);

	return prev;
}

另外,内核需要知道什么时候应该调用 schedule,所以内核提供了一个 need_resched 的标志来表明是否需要重新执行一次调度,当高优先级进程进入可执行状态,或者进程中断返回的时候,都会设置该标志。

四、总结

进程是一个正在运行程序的实例,是 *** 作系统分配资源的基本单位,进程包括正文段,数据段,堆栈,打开的文件,挂起的信号,处理器状态等内容;在 linux 中,进程包含一个唯一的 task_struct 结构体来修饰,一般通过 fork() 和 exit() 来创建和销毁。

线程是运行在进程中的一段逻辑流,是 *** 作系统调度的基本单位,共享进程的资源。在 Linux 中,并没有线程的单独概念,它也包含一个 task_struct 的结构,被看作是一个与多个进程共享资源的特殊进程,它的创建也和 fork 类似,使用 clone() 的系统调用创建,不过会多加入一些参数来标识需要共享哪些资源。

内核线程没有独立的地址空间,只在内核空间运行。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/5720686.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-18
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存