3.进程管理

3.1.进程

        进程就是处于运行状态的程序。进程除了程序(代码段,text_section)外,还包含了打开的文件、挂起的信号、内核内部数据、处理器状态、虚拟内存映射的内存空间地址、线程以及存放全局变量的数据段等。

        线程是内核调度的对象,一个进程可以包含一个或多个线程。Linux对线程和进程不做特别区分。在Linux中,线程不过是特殊的进程罢了。

        在Linux中,除了init进程是手动创建外,其它进程都是通过fork()系统调用创建的,fork()通过复制现有进程来创建一个新进程。调用fork()的进程称为父进程,由fork()创建的进程称为子进程。

        fork()系统调用会从内核返回两次:一次回到父进程,另一次回到子进程。

        由fork()创建的进程不会马上执行,而是在调用exec()这组函数后,才会将新的程序装载到内存中。

        Linux也把进程称为任务(task),任务通常指从内核角度看到的进程。

3.2.进程描述符及任务结构

        内核把进程列表存放在任务队列(task list)的双向循环链表中。链表项类型为task_struct(进程描述符)的结构,task_struct在<linux/sched.h>中被定义,包含了内核管理一个进程所需要的所有信息:打开的文件、进程的地址空间、挂起的信号、进程状态等等。

  

图3-1 进程描述符及任务队列

3.2.1.分配进程描述符

        在2.6版本以前的内核中,各个进程的task_struct存放在内核栈的末端,2.6之后的版本使用slab分配器动态生成task_struct,所以只需要在栈增长方向的尾端上创建一个新的结构struct thread_info。

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

/* version 2.6, x86 */
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 restart_block  restart_block;
    void                  *sysenter_return;
    int                   uaccess_err;
};
图3-2 进程描述符和内核栈

        在5.10.220版本的内核中,x86的struct thread_info在文件<asm/thread_info.h>中定义如下:

/* version 5.10.220, x86 */
struct thread_info {
    unsigned long    flags;    /* low level flags */
    u32              status;   /* thread synchronous flags */
};

        明显与2.6不同,struct thread_info中没有指向struct task_struct的指针。struct task_struct定义在<linux/sched.h>中:

/* version 5.10.220, x86 */
struct task_struct{
#ifdef CONFIG_THREAD_INFO_IN_TASK
    /*
     * For reasons of header soup (see current_thread_info()), this
     * must be the first element of task_struct.
     */
    struct thread_info    thread_info;
#endif
        ...........
};

        可见在5.10.220版本的内核中,x86架构下,进程内核栈的尾端存放的还是struct task_struct结构;当然由于struct thread_info结构在struct task_struct结构的最前面被定义,所以struct thread_info仍然在内核栈的尾端。

        同样是在5.10.220版本的内核中,arm的struct thread_info在文件<asm/thread_info.h>中定义如下:

/* version 5.10.220, arm */
/*
 * low level task data that entry.S needs immediate access to.
 * __switch_to() assumes cpu_context follows immediately after cpu_domain.
 */
struct thread_info {
	unsigned long		flags;		/* low level flags */
	int			preempt_count;	/* 0 => preemptable, <0 => bug */
	mm_segment_t		addr_limit;	/* address limit */
	struct task_struct	*task;		/* main task structure */
	__u32			cpu;		/* cpu */
	__u32			cpu_domain;	/* cpu domain */
#ifdef CONFIG_STACKPROTECTOR_PER_TASK
	unsigned long		stack_canary;
#endif
	struct cpu_context_save	cpu_context;	/* cpu context */
	__u32			syscall;	/* syscall number */
	__u8			used_cp[16];	/* thread used copro */
	unsigned long		tp_value[2];	/* TLS registers */
#ifdef CONFIG_CRUNCH
	struct crunch_state	crunchstate;
#endif
	union fp_state		fpstate __attribute__((aligned(8)));
	union vfp_state		vfpstate;
#ifdef CONFIG_ARM_THUMBEE
	unsigned long		thumbee_state;	/* ThumbEE Handler Base register */
#endif
};

        可见,5.10.220版本的内核中,arm架构下struct thread_info结构包含的内容符合《内核设计与实现》一书中的描述。

3.2.2.进程描述符的存放

       进程通过一个唯一的进程表示值(process identification value, PID)来标识每个进程。PID是一个数,为pid_t隐含类型,实际上就是一个int类型。PID的最大值由<linux/threads.h>定义的,该值越小一轮调度的时间就越短。

/*
 * A maximum of 4 million PIDs should be enough for a while.
 * [NOTE: PID/TIDs are limited to 2^30 ~= 1 billion, see FUTEX_TID_MASK.]
 */
#define PID_MAX_LIMIT (CONFIG_BASE_SMALL ? PAGE_SIZE * 8 : \
	(sizeof(long) > 4 ? 4 * 1024 * 1024 : PID_MAX_DEFAULT))

        用户可以通过管理员权限修改/proc/sys/kernel/pid_max来设置上限。

图3-3 Ubuntu24.04 x86_64的PID_MAX=4194304

        在内核中,访问某个任务通常需要获得指向该任务task_struct的指针。因此通过current宏查找到正在执行的进程描述符(tss)极为重要。有些架构的CPU会专门拿出一个寄存器来存放指向task_struct任务的指针,但是对于寄存器资源不那么丰富的x86架构,只能在尾端创建thread_info结构,通过计算偏移间接查找task_struct结构。

        在x86架构下,current把栈指针的后13位屏蔽,用来计算出thread_info的偏移。该操作是通过current_thread_info()函数来完成,汇编代码如下:

movl $-8192, %eax  # 内核栈大小为8K
# movl $-4096, %eax# 内核栈大小为4K
andl $esp, %eax  

        最后,current再从thread_info的task域中提取并返回task_struct的地址:

current_thread_info()->task;

        在5.10.220版本内核中,x86架构下,current_thread_info()函数的实现如下:

/* <linux/thread_info.h> */
#ifdef CONFIG_THREAD_INFO_IN_TASK
/*
 * For CONFIG_THREAD_INFO_IN_TASK kernels we need <asm/current.h> for the
 * definition of current, but for !CONFIG_THREAD_INFO_IN_TASK kernels,
 * including <asm/current.h> can cause a circular dependency on some platforms.
 */
#include <asm/current.h>
#define current_thread_info() ((struct thread_info *)current)
#endif


/* <asm/current.h> */
DECLARE_PER_CPU(struct task_struct *, current_task); // 用宏DECLARE_PER_CPU声明struct task *类型的current_task变量, 用于给每个CPU核心提供一个独立的变量实例
static __always_inline struct task_struct *get_current(void)
{
	return this_cpu_read_stable(current_task);
}

#define current get_current()

/* arch/x86/kernel/cpu/common.c */
/*
 * The following percpu variables are hot.  Align current_task to
 * cacheline size such that they fall in the same cacheline.
 */
DEFINE_PER_CPU(struct task_struct *, current_task) ____cacheline_aligned =
	&init_task;
EXPORT_PER_CPU_SYMBOL(current_task);

        由此可知,在5.10.220版本内核x86架构中的current_thread_info()指向current_task的thread_info域。

3.2.3.进程状态

        进程有5种状态:

        1.TASK_RUNNING(运行)——进程是可执行的:正在执行或者在运行队列中等待执行;

        2.TASK_INTERRUPTIBLE(可中断)——进程正在睡眠(阻塞),等待某些条件达成,内核就会把进程状态设置为运行;

        3.TASK_UNINTERRUPTIBLE(不可中断)——与可中断状态类似,但是就算接收到信号进程也不会被唤醒;

        4.__TASK_TRACED——被其它进程跟踪的进程,例如通过ptrace对调试程序进行跟踪;

        5.__TASK_STOPPED——进程停止执行,通常发生在接收到SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU等信号的时候。

图3-3 进程状态转换

/* <linux/sched.h> */
/* Used in tsk->state: */
#define TASK_RUNNING			0x0000
#define TASK_INTERRUPTIBLE		0x0001
#define TASK_UNINTERRUPTIBLE    0x0002
#define __TASK_STOPPED			0x0004
#define __TASK_TRACED			0x0008

3.2.4.设置进程当前状态

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

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

        该函数将指定的进程设置为指定的状态。必要时,它会设置内存屏障,来强制执行内存访问顺序(一般只有在SMP系统中才有此必要), 否则等价于:

task->state = state;

        set_current_state(state)等价于set_task_state(current, state)。

/* <linux/sched.h> */
#define __set_current_state(state_value)				\
	current->state = (state_value)

#define set_current_state(state_value)					\
	smp_store_mb(current->state, (state_value))

3.2.5.进程上下文

        可执行程序代码段是进程的重要组成部分,操作系统会将可执行文件的代码段加载到进程的地址空间执行。当程序执行了系统调用或者触发了某个异常,程序就会陷入内核态,被称为内核“代表进程执行”并处于进程上下文中。在进程内核态的上下文中current宏是有效的。

        系统调用和异常处理程序是对内核明确定义的接口。进程只有通过这些接口才能陷入内核执行——对内核的所有访问都必须通过这些接口。

3.2.6.进程家族树

        Linux系统的进程之间存在明显的继承关系。所有的进程都是PID为1的init进程的后代。内核在系统的最后阶段启动init进程。该进程读取系统的初始化脚本(initscript)并执行其它的相关程序,最终完成系统启动的整个过程。

        Linux中每个进程必有一个父进程,通过也可以通过0个或多个子进程。拥有同一个父进程的进程被称为兄弟。进程间的关系存放在task_struct中,每个task_struct结构都包含一个parent指针指向父进程的task_struct,还包含一个称为children的子进程链表。所以,对于当前进程,可以通过下面的代码获得其父进程的进程描述符:

struct task_struct *my_parent = current->parent;

3.3.进程创建

        Unix把进程的创建与执行分解到两个单独的函数中去执行:fork()和exec()。首先,fork()通过拷贝当前进程创建一个子进程,父进程和子进程的区别仅仅在于PID(进程号)、PPID(父进程号)和某些资源(例如,挂起的信号,他没有必要被继承)。exec()函数负责读取可执行文件的并将其载入地址空间开始运行。

3.3.1.写时拷贝

        Linux的fork()使用写时拷贝(copy-on-write)页实现。写时拷贝是一种可以推迟甚至免除拷贝数据的技术,内核不需要复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝。

        只有在写操作的时候,数据才会被复制。也就是说,资源的复制只有在需要写入的时候才进行,在此之前,只是以只读的方式共享。这种技术使地址空间上的页拷贝推迟到实际发生写入的时候才进行。

        fork()的实际开销就是复制父进程的页表以及给子进程创建位移的进程描述符task_struct。

3.3.2.fork()

        Linux通过clone()系统调用实现fork(),该系统调用会通过一系列的参数标志来指明父、子进程需要共享的资源。fork()、vfork()和__clone()库函数都会先设置参数标志然后去调用clone(),然后由clone()去调用do_fork()。

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

        do_fork(定义在kernel/fork.c)完成了进程创建的大部分工作,该函数调用copy_process()函数,然后让进程开始运行:

        1)调用dup_task_struct()为新进程创建一个内核栈,thread_info结构和task_struct,这些值与当前进程的值相同。此时子进程和父进程的描述符时完全相同的。

        2)检查并确保创建子进程后,当前用户所拥有的进程数目没有超出给他分配的资源的限制。

        3)区分子进程和父进程,将进程描述符内的部分成员清0或设为初始值,但是task_struct中的大多数成员仍然是从父进程继承而来。

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

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

        6)调用alloc_pid()为新进程分配一个有效的PID。

        7)根据传递给clone()的参数标志,copy_process()拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。

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

        再回到do_fork()函数,如果copy_process()函数成功返回,新创建的子进程被唤醒并让其投入运行。内核有意选择子进程首先执行。子进程被创建后马上调用exec()函数,可以避免写时拷贝的额外开销,如果父进程首先执行的话,有可能会开始向地址空间写入。

SYSCALL_DEFINE0(fork)
{
#ifdef CONFIG_MMU
	struct kernel_clone_args args = {
		.exit_signal = SIGCHLD,
	};

	return kernel_clone(&args);
#else
	/* can not support in nommu mode */
	return -EINVAL;
#endif
}

3.3.3.vfork()

        vfork()与fork()相比差别在于不拷贝父进程的页表项。子进程作为父进程的一个单独的线程再它的地址空间运行,父进程被阻塞,直到子进程退出或执行exec()。子进程不能向地址空间写入。

        vfork()系统调用的实现是通过clone()系统调用传递一个特殊标志来进行的:

        1)再调用copy_process()时,task_struct的vfor_done成员被设置为NULL。

        2)在执行do_fork()时,如果给定特别标志,则vfork_done会指向一个特定地址。

        3)子进程先开始执行后,父进程不是马上恢复执行,而是一直等待,直到子进程通过vfork_done指针向它发送信号。

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

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

#ifdef __ARCH_WANT_SYS_VFORK
SYSCALL_DEFINE0(vfork)
{
	struct kernel_clone_args args = {
		.flags		= CLONE_VFORK | CLONE_VM,
		.exit_signal	= SIGCHLD,
	};

	return kernel_clone(&args);
}
#endif
/*
 *  Ok, this is the main fork-routine.
 *
 * It copies the process, and if successful kick-starts
 * it and waits for it to finish using the VM if required.
 *
 * args->exit_signal is expected to be checked for sanity by the caller.
 */
pid_t kernel_clone(struct kernel_clone_args *args)
{
	u64 clone_flags = args->flags;
	struct completion vfork;
	struct pid *pid;
	struct task_struct *p;
	int trace = 0;
	pid_t nr;

	/*
	 * For legacy clone() calls, CLONE_PIDFD uses the parent_tid argument
	 * to return the pidfd. Hence, CLONE_PIDFD and CLONE_PARENT_SETTID are
	 * mutually exclusive. With clone3() CLONE_PIDFD has grown a separate
	 * field in struct clone_args and it still doesn't make sense to have
	 * them both point at the same memory location. Performing this check
	 * here has the advantage that we don't need to have a separate helper
	 * to check for legacy clone().
	 */
	if ((args->flags & CLONE_PIDFD) &&
	    (args->flags & CLONE_PARENT_SETTID) &&
	    (args->pidfd == args->parent_tid))
		return -EINVAL;

	/*
	 * Determine whether and which event to report to ptracer.  When
	 * called from kernel_thread or CLONE_UNTRACED is explicitly
	 * requested, no event is reported; otherwise, report if the event
	 * for the type of forking is enabled.
	 */
	if (!(clone_flags & CLONE_UNTRACED)) {
		if (clone_flags & CLONE_VFORK)
			trace = PTRACE_EVENT_VFORK;
		else if (args->exit_signal != SIGCHLD)
			trace = PTRACE_EVENT_CLONE;
		else
			trace = PTRACE_EVENT_FORK;

		if (likely(!ptrace_event_enabled(current, trace)))
			trace = 0;
	}

	p = copy_process(NULL, trace, NUMA_NO_NODE, args);
	add_latent_entropy();

	if (IS_ERR(p))
		return PTR_ERR(p);

	/*
	 * Do this prior waking up the new thread - the thread pointer
	 * might get invalid after that point, if the thread exits quickly.
	 */
	trace_sched_process_fork(current, p);

	pid = get_task_pid(p, PIDTYPE_PID);
	nr = pid_vnr(pid);

	if (clone_flags & CLONE_PARENT_SETTID)
		put_user(nr, args->parent_tid);

	if (clone_flags & CLONE_VFORK) {
		p->vfork_done = &vfork;
		init_completion(&vfork);
		get_task_struct(p);
	}

	wake_up_new_task(p);

	/* forking complete and child started to run, tell ptracer */
	if (unlikely(trace))
		ptrace_event_pid(trace, pid);

	if (clone_flags & CLONE_VFORK) {
		if (!wait_for_vfork_done(p, &vfork))
			ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
	}

	put_pid(pid);
	return nr;
}

3.4.线程在Linux中的实现

        线程机制提供了在同一程序内共享内存地址空间运行的一组线程,这些线程可以共享打开的文件和其它资源。线程机制支持并发程序设计技术(concurrent programming),在多处理器系统上可以实现真正的并行处理(parallelism)。

        Linux实现线程的机制非常独特,从内核的角度上来说,Linux没有线程这个概念。LIinux把所有的线程都当做进程来实现。内核并没有准备特别的调度算法或是定义特别的数据结构来表征线程,线程仅仅被视为一个与其它进程共享某些资源的进程。每个线程都拥有唯一的task_struct,所以在内核中能够,线程看起来就像是一个普通的进程。

3.4.1.创建线程

        创建线程与创建普通进程类似,差别在于调用clone()时需要传入一些参数来知名需要共享的资源:

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

        一个普通fork() 的实现如下:

clone(SIGCHLD, 0);

        vfork()的实现如下:

clone(CLONE_VFORK | CLONE_VM | SIGCHLD, 0);

表3-1 clone()参数标志
参数标志含义
CLONE_FILES父子进程共享打开的文件
CLONE_FS父子进程共享文件系统信息
CLONE_IDLETASK将PID设置为0(只供idle进程使用)
CLONE_NEWNS为子进程创建新的命名空间
CLONE_PARENT指定子进程与父进程拥有同一个父进程
CLONE_PTRACE继续调试子进程
CLONE_SETTID将TID回写至用户空间
CLONE_SETTLS为子进程创建新的TLS
CLONE_SIGHAND父子进程共享信号处理函数及被阻断的信号
CLONE_SYSVSEM父子进程共享System V SEM_UNDO语义
CLONE_THREAD父子进程放入相同的线程组
CLONE_VFORK调用vfork()所以父进程准备睡眠等待子进程将其唤醒
CLONE_UNTRACED防止跟踪进程在进程上强制执行CLONE_PTRACE
CLONE_STOP以TASK_STOPPED状态开始进程
CLONE_SETTLS为子进程创建新的TLS(thread-local storage)
CLONE_CHILD_CLEARTID清除子进程的TID
CLONE_CHILD_SETTID设置子进程的TID
CLONE_PARENT_SETTID设置父进程的TID
CLONE_VM父子进程共享地址空间

 3.4.1.创建线程

        内核通过内核线程(kernel thread)——独立运行在内核空间的标准进程,在后台执行一些操作。内核线程和普通的进程间的区别在于内核线程没有独立的地址空间(实际上指向地址空间的mm指针被设置为NULL)。内核线程只在内核空间运行,从来不会切换到用户空间去。内核线程和普通进程一样,可以被调度,也可以被抢占。

        内核通过从kthreadd内核进程中衍生出所有新的内核线程,在<linux/kthread.h>中声明有接口,从现有内核线程中创建一个新的内核线程的方法如下:

static __printf(4, 0)
struct task_struct *__kthread_create_on_node(int (*threadfn)(void *data),
						    void *data, int node,
						    const char namefmt[],
						    va_list args)
{
	DECLARE_COMPLETION_ONSTACK(done);
	struct task_struct *task;
	struct kthread_create_info *create = kmalloc(sizeof(*create),
						     GFP_KERNEL);

	if (!create)
		return ERR_PTR(-ENOMEM);
	create->threadfn = threadfn;
	create->data = data;
	create->node = node;
	create->done = &done;

	spin_lock(&kthread_create_lock);
	list_add_tail(&create->list, &kthread_create_list);
	spin_unlock(&kthread_create_lock);

	wake_up_process(kthreadd_task);
	/*
	 * Wait for completion in killable state, for I might be chosen by
	 * the OOM killer while kthreadd is trying to allocate memory for
	 * new kernel thread.
	 */
	if (unlikely(wait_for_completion_killable(&done))) {
		/*
		 * If I was SIGKILLed before kthreadd (or new kernel thread)
		 * calls complete(), leave the cleanup of this structure to
		 * that thread.
		 */
		if (xchg(&create->done, NULL))
			return ERR_PTR(-EINTR);
		/*
		 * kthreadd (or new kernel thread) will call complete()
		 * shortly.
		 */
		wait_for_completion(&done);
	}
	task = create->result;
	if (!IS_ERR(task)) {
		static const struct sched_param param = { .sched_priority = 0 };
		char name[TASK_COMM_LEN];

		/*
		 * task is already visible to other tasks, so updating
		 * COMM must be protected.
		 */
		vsnprintf(name, sizeof(name), namefmt, args);
		set_task_comm(task, name);
		/*
		 * root may have changed our (kthreadd's) priority or CPU mask.
		 * The kernel thread should not inherit these properties.
		 */
		sched_setscheduler_nocheck(task, SCHED_NORMAL, &param);
		set_cpus_allowed_ptr(task,
				     housekeeping_cpumask(HK_FLAG_KTHREAD));
	}
	kfree(create);
	return task;
}

        新的任务由kthread内核进程通过clone()系统调用而创建的。新的进程运行threadfn函数,给其传递的参数为data。进程会被命名为namefmt,namefim接受可变参数列表类似于printf()的格式化参数。新创建的进程处于不可运行状态,需要通过调用wake_up_process()进行唤醒。创建一个进程并让它运行,可以通过调用kthread_run()来实现:

#define kthread_run(threadfn, data, namefmt, ...)			   \
({									   \
	struct task_struct *__k						   \
		= kthread_create(threadfn, data, namefmt, ## __VA_ARGS__); \
	if (!IS_ERR(__k))						   \
		wake_up_process(__k);					   \
	__k;								   \
})

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

/**
 * kthread_stop - stop a thread created by kthread_create().
 * @k: thread created by kthread_create().
 *
 * Sets kthread_should_stop() for @k to return true, wakes it, and
 * waits for it to exit. This can also be called after kthread_create()
 * instead of calling wake_up_process(): the thread will exit without
 * calling threadfn().
 *
 * If threadfn() may call kthread_exit() itself, the caller must ensure
 * task_struct can't go away.
 *
 * Returns the result of threadfn(), or %-EINTR if wake_up_process()
 * was never called.
 */
int kthread_stop(struct task_struct *k)
{
	struct kthread *kthread;
	int ret;

	trace_sched_kthread_stop(k);

	get_task_struct(k);
	kthread = to_kthread(k);
	set_bit(KTHREAD_SHOULD_STOP, &kthread->flags);
	kthread_unpark(k);
	wake_up_process(k);
	wait_for_completion(&kthread->exited);
	ret = k->exit_code;
	put_task_struct(k);

	trace_sched_kthread_stop_ret(ret);
	return ret;
}

3.5.进程终结

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

        一般来说进程的析构是自身引起的,终结进程的大部分工作由定义在<linux/exit.c>中的do_exit()来完成:

        1)将task_struct中的标志成员设置为PF_EXITING;

        2)调用del_timer_sync()删除任一内核定时器;

        3)如果BSD的进程记账功能是开启的,do_exit()调用acct_update_integrals()来输出记账信息;

        4)然后调用exit_mm()函数释放进程占用的mm_struct,如果没有别的进程使用它们(也就是说,这个地址空间没有被共享),就彻底释放它们;

        5)接下来调用sem_exit()函数,如果进程排队等待IPC信号,它则离开读列;

        6)调用exit_files()和exit_f(),以分别递减文件描述符、文件系统数据的引用计数。如果其中某个计数值将为零,就代表没有进程在使用相应的资源,只有本进程在使用相应的资源,此时资源释放;

        7)接着吧存放在task_struct的exit_code成员中的任务任务退出代码置为由exit()提供的退出代码,或者去完成任何其他由内核机制规定的推出动作。推出代码存放在父进程随时检索。

        8)调用exit_notify()向父进程发送信号,给子进程重新找养父,养父一般为线程组中的其他线程或者init进程,并把进程状态(存放在task_struct结构的exit_state中)设成EXIT_ZOMBIE。

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

        至此,除了与其他进程相关联的资源(除了本进程,其他进程也在使用)、内核栈、thread_info结构、task_struct结构外,其他与本进程相关联的资源都被释放掉了,此时进程处于EXIT_ZOMBIE状态,其存在的唯一目的就是向它的父进程提供信息,父进程检索到信息后,或通知内核那是无关的信息后,由进程所持有的剩余内存被释放,归还系统使用。

3.5.1.删除进程描述符

        在调用do_exit()后,系统仍然保留了进程描述符,这样做可以让系统有办法在进程终结后仍然能获得它的信息。因此,进程终结时所需的清理工作和进程描述符的删除被分开执行。在父进程获得已终结的子进程信息后,子进程的task_struct结构才被释放。

        wait()这一族函数是通过唯一的系统该调用wait4()来实现的。它的标准动作时挂起调用它的进程,直到其中一个子进程退出,此时函数会返回子进程的PID。此外,调用该函数时提供的指针会包含子函数退出时的退出代码。

        当最终需要释放进程描述符时。release_task()会被调用,用以完成以下工作:

        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.孤儿进程的进退维股

        父进程在子进程退出之前退出,需要有机制来保证子进程能找到一个新父亲,否则这些孤儿进程会在退出时永远处于僵死状态,占用内存。解决方法是给子进程在当前线程组内找一个线程作为父亲,如果不行,就让init做它们的父进程。在do_exit()总会调用exit_notify(),该函数会调用forget_original_parent(),后者再调用find_new_reaper()来执行寻父过程。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1 进程是操作系统中把一个任务分解成一个个子任务执行单元的基本概念。2. 进程包括程序代码、数据、寄存器状态、程序计数器等数据。3. 程序的编译是把高级语言源程序转换成机器语言指令;链接是把编译后的目标程序与库函数连接到一起;加载是把链接后的可执行文件放入内存中;运行是把已加载的可执行文件释放到CPU上执行。4. 程序顺序执行是指按程序代码的顺序执行,每条条指令按顺序编译、链接、加载、运行。5. 程序的并发执行是指在一定时间内,多个程序可以同时运行,比如多任务操作系统的程序。6. 进程与程序的区别与联系:程序是指一段可以运行的机器语言代码,而进程是指一个正在运行中的程序,同一个程序可以对应多个进程,而进程中又包含有程序代码。7. 进程状态及其转换:进程可以处于就绪、运行、阻塞、结束等状态,并可以在这些状态之间转换。8. 进程管理块(PCB)是操作系统用来管理进程的数据结构,其中包括进程标识符、进程状态、进程控制块等信息。9. 调度程序是操作系统根据计算机系统状态,选择合适的进程运行的程序。10. 进程的上下文指的是进程正在运行时,CPU中各部件所处的状态。11. 上下文切换是指操作系统在执行多个进程时,通过保存和恢复进程的上下文,实现不同进程之间的切换。12. 进程控制原语是操作系统提供的一组接口,用于控制进程的生命周期。13. 进程可以通过系统调用或者用户编程实现,创建一个新的进程,并设置其运行环境。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值