Linux-进程的管理与调度14(基于6.1内核)

Linux-进程的管理与调度14(基于6.1内核)---Linux进程退出

一、Linux进程的退出

1.1 linux下进程退出的方式

出方式说明退出状态码是否清理资源示例代码
正常退出程序调用 exit()return 或正常结束返回值 0 或非 0清理资源exit(0)return 0
异常退出程序因信号(如 SIGSEGVSIGFPE 等)或调用 abort() 强制退出非零状态码未必完全清理资源abort()、访问非法内存(如 SIGSEGV

1.2 _exit, exit和_Exit的区别和联系

特性exit_exit_Exit
定义位置<stdlib.h><unistd.h><stdlib.h>
执行操作执行缓冲区刷新,调用 atexit() 注册的函数,执行文件清理等不执行缓冲区刷新,不调用 atexit() 注册的函数,直接退出不执行缓冲区刷新,不调用 atexit() 注册的函数,直接退出
返回值返回退出状态码,通常由 exit() 提供给操作系统返回退出状态码,直接返回给操作系统返回退出状态码,直接返回给操作系统
使用场景需要清理资源和执行退出处理的情况fork() 后的子进程中直接退出,或者在多线程程序中避免资源清理用于现代 C11 代码,表达清晰的终止,替代 _exit

二、进程退出的系统调用

2.1 _exit和exit_group系统调用

_exit系统调用

进程退出由_exit系统调用来完成,这使得内核有机会将该进程所使用的资源释放回系统

进程终止时,一般是调用exit库函数释放进程所拥有的资源

_exit系统调用的入口点是sys_exit()函数,需要一个错误码作为参数,以便退出进程。

定义是体系结构无关的, 见kernel/exit.c

用户空间多线程应用程序(用户空间线程!!!),对应内核中就有多个进程, 这些进程共享虚拟地址空间和资源,有各自的进程id(pid),但是组进程id(tpid)是相同, 都等于组长(领头进程)的pid

为什么还需要exit_group

我们如果了解linux的线程实现机制的话,会知道所有的线程是属于一个线程组的,同时即使不是线程,linux也允许多个进程组成进程组,多个进程组组成一个会话,因此我们本质上了解到不管是多线程,还是进程组起本质都是多个进程组成的一个集合,那么我们的应用程序在退出的时候,自然希望一次性的退出组内所有的进程

因此exit_group就诞生了

exit_group函数会杀死属于当前进程所在线程组所有进程。它接受进程终止代号作为参数,进程终止代号可能是系统调用exit_group正常结束)指定的一个值,也可能是内核提供的一个错误码(异常结束)。

因此C语言库函数exit使用系统调用exit_group终止整个线程组,库函数pthread_exit使用系统调用_exit来终止某一个线程

_exit和exit_group这两个系统调用在Linux内核中的入口点函数分别为sys_exit和sys_exit_group。

2.2 系统调用声明

 include/linux/syscalls.h

asmlinkage long sys_exit(int error_code);
asmlinkage long sys_exit_group(int error_code);
 
asmlinkage long sys_wait4(pid_t pid, int __user *stat_addr,
                                int options, struct rusage __user *ru);
asmlinkage long sys_waitid(int which, pid_t pid,
                           struct siginfo __user *infop,
                           int options, struct rusage __user *ru);
asmlinkage long sys_waitpid(pid_t pid, int __user *stat_addr, int options);

2.3 系统调用号

系统调用号是一个体系结构相关的定义, 但是多数体系结构的定义如下, 在tools/include/uapi/asm-generic/unistd.h

/* kernel/exit.c */
#define __NR_exit 93
__SYSCALL(__NR_exit, sys_exit)
#define __NR_exit_group 94
__SYSCALL(__NR_exit_group, sys_exit_group)
#define __NR_waitid 95
__SC_COMP(__NR_waitid, sys_waitid, compat_sys_waitid)

2.4 系统调用实现

kernel/exit.c

SYSCALL_DEFINE1(exit, int, error_code)
{
        do_exit((error_code&0xff)<<8);
}
 
/*
 * this kills every thread in the thread group. Note that any externally
 * wait4()-ing process will get the correct exit code - even if this
 * thread is not the thread group leader.
 */
SYSCALL_DEFINE1(exit_group, int, error_code)
{
        do_group_exit((error_code & 0xff) << 8);
        /* NOTREACHED */
        return 0;
}

三、do_group_exist流程

do_group_exit()函数杀死属于current线程组所有进程。它接受进程终止代码作为参数,进程终止代号可能是系统调用exit_group()指定的一个值,也可能是内核提供的一个错误代号。

该函数执行下述操作:

  1. 检查退出进程的SIGNAL_GROUP_EXIT标志是否不为0,如果不为0,说明内核已经开始为线性组执行退出的过程。在这种情况下,就把存放在current->signal->group_exit_code的值当作退出码,然后跳转到第4步。

  2. 否则,设置进程的SIGNAL_GROUP_EXIT标志并把终止代号放到current->signal->group_exit_code字段。

  3. 调用zap_other_threads()函数杀死current线程组中的其它进程。为了完成这个步骤,函数扫描当前线程所在线程组的链表,向表中所有不同于current的进程发送SIGKILL信号,结果,所有这样的进程都将执行do_exit()函数,从而被杀死

遍历线程所在线程组的所有线程函数while_each_thread(p, t)使用了:

static inline struct task_struct *next_thread(const struct task_struct *p)
{
	return list_entry_rcu(p->thread_group.next,
			      struct task_struct, thread_group);
}
 
#define while_each_thread(g, t) \
	while ((t = next_thread(t)) != g)

同一个进程组的可以, 扫描与current->pids[PIDTYPE_PGID](这是进程组组长pid结构体)对应的PIDTYPE_PGID类型的散列表(因为是进程组组长,所以其真实的pid结构体中tasks[PIDTYPE_PGID]是这个散列表的表头)中的每个PID链表

 调用do_exit()函数,把进程的终止代码传递给它。正如我们将在下面看到的,do_exit()杀死进程而且不再返回

/*
 * Take down every thread in the group.  This is called by fatal signals
 * as well as by sys_exit_group (below).
 */
void __noreturn
do_group_exit(int exit_code)
{
	struct signal_struct *sig = current->signal;

	if (sig->flags & SIGNAL_GROUP_EXIT)
		exit_code = sig->group_exit_code;
	else if (sig->group_exec_task)
		exit_code = 0;
	else {
		struct sighand_struct *const sighand = current->sighand;

		spin_lock_irq(&sighand->siglock);
		if (sig->flags & SIGNAL_GROUP_EXIT)
			/* Another thread got here before we took the lock.  */
			exit_code = sig->group_exit_code;
		else if (sig->group_exec_task)
			exit_code = 0;
		else {
			sig->group_exit_code = exit_code;
			sig->flags = SIGNAL_GROUP_EXIT;
			zap_other_threads(current);
		}
		spin_unlock_irq(&sighand->siglock);
	}

	do_exit(exit_code);
	/* NOTREACHED */
}

四、 do_exit流程

进程终止所要完成的任务都是由do_exit函数来处理。kernel/exit.c

void __noreturn do_exit(long code)
{
    ......
}
EXPORT_SYMBOL_GPL(do_exit);

4.1 触发synchronize_group_exit通知链实例的处理函数

kernel/exit.c

static void synchronize_group_exit(struct task_struct *tsk, long code)
{
	struct sighand_struct *sighand = tsk->sighand;
	struct signal_struct *signal = tsk->signal;

	spin_lock_irq(&sighand->siglock);
	signal->quick_threads--;
	if ((signal->quick_threads == 0) &&
	    !(signal->flags & SIGNAL_GROUP_EXIT)) {
		signal->flags = SIGNAL_GROUP_EXIT;
		signal->group_exit_code = code;
		signal->group_stop_count = 0;
	}
	spin_unlock_irq(&sighand->siglock);
}

4.2 检查进程的blk_plug是否为空

保证task_struct中的plug字段是空的,或者plug字段指向的队列是空的plug字段的意义是stack plugging

WARN_ON(tsk->plug);

其中__blk_flush_plug函数定义block/blk-core.c

void __blk_flush_plug(struct blk_plug *plug, bool from_schedule)
{
	if (!list_empty(&plug->cb_list))
		flush_plug_callbacks(plug, from_schedule);
	blk_mq_flush_plug_list(plug, from_schedule);
	/*
	 * Unconditionally flush out cached requests, even if the unplug
	 * event came from schedule. Since we know hold references to the
	 * queue for cached requests, we don't want a blocked task holding
	 * up a queue freeze/quiesce event.
	 */
	if (unlikely(!rq_list_empty(plug->cached_rq)))
		blk_mq_free_plug_rqs(plug);
}

4.3 检查进程设置进程PF_EXITING

通过exit_signals来设置

	exit_signals(tsk);  /* sets PF_EXITING */

4.4 内存屏障

    /*
     * tsk->flags are checked in the futex code to protect against
     * an exiting task cleaning up the robust pi futexes.
     */
    /*  内存屏障,用于确保在它之后的操作开始执行之前,它之前的操作已经完成  */
    smp_mb();
    /*  一直等待,直到获得current->pi_lock自旋锁  */
    raw_spin_unlock_wait(&tsk->pi_lock);

4.5 同步进程的mm的rss_stat

    /* sync mm's RSS info before statistics gathering */
    if (tsk->mm)
        sync_mm_rss(tsk->mm);

4.6 获取current->mm->rss_stat.count[member]计数

    /*
        cct_update_integrals - update mm integral fields in task_struct
        更新进程的运行时间, 获取current->mm->rss_stat.count[member]计数 
        http://lxr.free-electrons.com/source/kernel/tsacct.c?v=4.6#L152
    */
    acct_update_integrals(tsk);

函数的实现如下kernel/tsacct.c

/**
 * acct_update_integrals - update mm integral fields in task_struct
 * @tsk: task_struct for accounting
 */
void acct_update_integrals(struct task_struct *tsk)
{
	u64 utime, stime;
	unsigned long flags;

	local_irq_save(flags);
	task_cputime(tsk, &utime, &stime);
	__acct_update_integrals(tsk, utime, stime);
	local_irq_restore(flags);
}

其中task_cputime获取了进程的cpu时间__acct_update_integr定义kernel/tsacct.c

static void __acct_update_integrals(struct task_struct *tsk,
				    u64 utime, u64 stime)
{
	u64 time, delta;

	if (!likely(tsk->mm))
		return;

	time = stime + utime;
	delta = time - tsk->acct_timexpd;

	if (delta < TICK_NSEC)
		return;

	tsk->acct_timexpd = time;
	/*
	 * Divide by 1024 to avoid overflow, and to avoid division.
	 * The final unit reported to userspace is Mbyte-usecs,
	 * the rest of the math is done in xacct_add_tsk.
	 */
	tsk->acct_rss_mem1 += delta * get_mm_rss(tsk->mm) >> 10;
	tsk->acct_vm_mem1 += delta * READ_ONCE(tsk->mm->total_vm) >> 10;
}

4.7 清除定时器

 继续:kernel/exit.c

	group_dead = atomic_dec_and_test(&tsk->signal->live);
	if (group_dead) {
		/*
		 * If the last thread of global init has exited, panic
		 * immediately to get a useable coredump.
		 */
		if (unlikely(is_global_init(tsk)))
			panic("Attempted to kill init! exitcode=0x%08x\n",
				tsk->signal->group_exit_code ?: (int)code);

#ifdef CONFIG_POSIX_TIMERS
		hrtimer_cancel(&tsk->signal->real_timer);
		exit_itimers(tsk);
#endif
		if (tsk->mm)
			setmax_mm_hiwater_rss(&tsk->signal->maxrss, tsk->mm);
	}

4.8 收集进程会计信息

  继续:kernel/exit.c

    acct_collect(code, group_dead);

4.9 审计

 继续:kernel/exit.c 

    if (group_dead)
        tty_audit_exit();	//记录审计事件
    audit_free(tsk);	//  释放struct audit_context结构体

4.10 释放进程占用的资源

释放线性区描述符和页表

  继续:kernel/exit.c

    /*  释放存储空间
    放弃进程占用的mm,如果没有其他进程使用该mm,则释放它。
     */
    exit_mm(tsk);

输出进程会计信息

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

释放用户空间的“信号量”

exit_sem(tsk);   /*  释放用户空间的“信号量”  */

遍历current->sysvsem.undo_list链表,并清除进程所涉及的每个IPC信号量的操作痕迹

释放锁

exit_shm(tsk);  /* 释放锁  */

释放文件对象相关资源

exit_files(tsk); /*  释放已经打开的文件   */

释放struct fs_struct结构体

exit_fs(tsk);   /*  释放用于表示工作目录等结构  */

脱离控制终端

    if (group_dead)
        disassociate_ctty(1);

释放命名空间

exit_task_namespaces(tsk);  /*  释放命名空间  */
exit_task_work(tsk);

释放task_struct中的thread_struct结构

    exit_thread();   

触发thread_notify_head链表中所有通知链实例的处理函数,用于处理struct thread_info结构体

Performance Event功能相关资源的释放

perf_event_exit_task(tsk);

Performance Event功能相关资源的释放

cgroup_exit(tsk);

注销断点

    /*
     * FIXME: do that only when needed, using sched_exit tracepoint
     */
    flush_ptrace_hw_breakpoint(tsk);

更新所有子进程的父进程

    exit_notify(tsk, group_dead);

进程事件连接器(通过它来报告进程fork、exec、exit以及进程用户ID与组ID的变化)

    proc_exit_connector(tsk);

用于NUMA,当引用计数为0时,释放struct mempolicy结构体所占用的内存

#ifdef CONFIG_NUMA
    task_lock(tsk);
    mpol_put(tsk->mempolicy);
    tsk->mempolicy = NULL;
    task_unlock(tsk);
#endif

释放struct futex_pi_state结构体所占用的内存

#ifdef CONFIG_FUTEX
    if (unlikely(current->pi_state_cache))
        kfree(current->pi_state_cache);
#endif

释放struct io_context结构体所占用的内存

    if (tsk->io_context)
        exit_io_context(tsk);

释放与进程描述符splice_pipe字段相关的资源

    if (tsk->splice_pipe)
        free_pipe_info(tsk->splice_pipe);


    if (tsk->task_frag.page)
        put_page(tsk->task_frag.page);

4.11 检查有多少未使用的进程内核栈

    check_stack_usage();

4.12 抢占和任务更新

	preempt_disable();
	if (tsk->nr_dirtied)
		__this_cpu_add(dirty_throttle_leaks, tsk->nr_dirtied);
  1. 禁用抢占:首先,使用 preempt_disable() 禁用当前 CPU 的抢占,以确保后续代码段在执行时不被中断。
  2. 检查脏页:接下来,检查当前进程(tsk)是否有脏页(即 tsk->nr_dirtied 是否大于零)。如果当前进程有脏页,执行下一步。
  3. 更新脏页统计:如果当前进程有脏页,则将该进程修改的脏页数(tsk->nr_dirtied)累加到 dirty_throttle_leaks 变量中。这个变量是一个 每个 CPU 独立的变量,它用于记录所有任务中已修改的脏页数。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值