Linux源码学习--进程的生命周期

Linux源码学习—进程的生命周期

前言

最近在学习Linux 0.11内核源码,近几天主要阅读了进程的基本数据结构、进程的创建/加载运行/终结等代码,现对进程的生命周期进行总结和分享。欢迎大家批评指正,描述有误的地方请务必告知,非常感谢!说明:本文参考书目为《Linux内核设计的艺术》第二版和《Linux内核设计与实现》第三版。

围绕进程,本文内容的结构分布如下,其中第2,3,4,5部分均结合Linux 0.11源码进行分析。

章节主要内容
1. 进程的基本概念进程、程序、任务
2. 进程相关的数据结构tast_struct结构体
3. 进程的创建sys_fork, find_empty_process(), copy_process()
4. 进程的加载和运行do_execve(), do_no_page()
5. 进程的终结do_exit()

一、进程相关的基本概念

1.1 进程相关的一些定义

现有的进程相关的定义有很多,下边是一些可参考的描述:

  • 进程是处于执行期的程序(目标码存放在某种存储介质上)。
  • 进程是处于执行期的程序以及相关的资源的总称。
  • 狭义定义:进程是正在运行的程序的实例(an instance of a computer program that is being executed)。
  • 广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。

进程的另一个名字是任务(task),Linux内核通常把进程也叫做任务。

1.2 进程包含的资源

在1.1中有定义提到进程包含的资源,这里详细列举一些具体资源:代码段,数据段,打开的文件,挂起的信号,内核内部数据,一个/多个具有内存映射的内存地址空间,一个或多个执行线程等(还有一些其他的资源,具体可参见2.1中task_struct结构体的成员变量)。

二、进程相关的数据结构

2.0 task_struct概述

结构体task_struct中存放进程描述符(process descriptor)的内容,进程描述符中包含的数据能够完整的描述一个正在执行的程序:他打开的文件、进程的地址空间、挂起的信号、进程的状态等等。包含一个内核管理一个进程所需要的所有信息。

2.1 task_struct结构体源码详解

task_struct结构体代码位于\linux\sched.h文件,其代码如下:

在这里插入图片描述

2.1.0 结构体task_struct成员变量

state表示进程运行状态。counter为剩余的时间片,任务开始运行时,counter值等于priority的值,随着任务的运行,counter值不断递减。priority为进程优先级,该值越大,表明该进程优先级越高。signal为信号位图,每个比特位代表一个信号,信号值=位偏移值+1。sigaction为信号执行属性结构,对应信号的执行操作和标志信息。blocked是信号屏蔽码,与signal信号位图对应。exit_code是退出码,在进程退出时,该值会传递给其父进程。start_code为代码段地址。end_code为代码段长度的字节数。end_data为代码段长度+数据段长度的字节数。brk为总长度的字节数。start_stack为堆栈段地址。pid为该进程进程号。father为其父进程的进程号。pgrp为其父进程组号。session为会话号。leader为会话首领。uid/euid/suid分别为用户标识号、有效用户标识号、保存的用户标识号。gid/egid/sgid类似,为组标识号、有效组标识号、保存的组标识号。alarm为报警计时器。utime/stime分别为用户态运行时间和系统态运行时间。cutime/cstime类似,为子进程用户态运行时间和子进程系统态运行时间。start_time为进程开始运行时刻。used_math标识当前进程是否使用协处理器。tty是进程使用tty的子设备号。umask是文件创建属性屏蔽位。pwd/root/executable分别为当前工作目录节点、根目录节点、当前进程对应的可执行程序文件节点的结构。close_on_exec为执行时关闭文件句柄位图标志。filp[NR_OPEN]为当前进程使用的文件表结构。ldt[3]为本进程的局部表描述符,ldt[0]为空,ldt[1]为代码段,ldt[2]为数据段和堆栈段。tss描述了本进程的任务状态段信息结构。

2.1.1 结构体sigaction

sa_handler是对应某信号指定要采取的行动,是指向处理该信号函数的指针。sa_mask给出了对信号的屏蔽码,在信号处理程序执行时,将阻塞对这些信号的处理。sa_flags指定改变信号处理过程的信号集。sa_restorer恢复过程指针。

2.1.2 结构体m_inode

为内存中i节点的结构。i_mode为文件类型和属性(rwx位)。i_uid为文件拥有者标识符。i_size为文件大小。i_mtime为文件修改时间。i_gid为文件所有者所在组标识。i_nlinks为文件目录项链接数。i_zone[9]为直接(0-6)间接(7)或双重间接(8)逻辑块号。i_wait为等待该节点的进程。i_atime和i_ctime分别为最后访问时间、i节点自身修改时间。i_dev为i节点所在设备号。i_num为i节点号。i_count为i节点被使用的次数。i_lock/i_dirty分别为锁标志和修改(脏)标志。i_pipe和i_update分别为管道标志和更新标志。

2.1.3 结构体file

用于在文件句柄与i节点之间建立关系。f_mode为文件操作模式(RW位)。f_flags为文件打开和控制的标志。f_count为对应文件句柄(文件描述符)数。f_inode指向对应的i节点。f_ops为文件位置,读写偏移。

2.1.4 结构体desc_struct与结构体tss_struct

desc_struct定义了段描述符的数据结构。每一个段描述符由8个字节构成。

tss_struct为任务状态段数据结构。其中包含进程运行过程中的寄存器信息、段信息等。

三、进程的创建

3.0 创建过程概述

许多其他操作系统提供了产生(spawn)进程的机制。首先在新的地址空间里创建进程读入可执行文件,最后开始执行。Unix将上述步骤分解到两个函数中执行:fork()和exec()。这里我们先介绍创建的过程,重点分析fork()。

  • a. 首先,fork()通过拷贝当前进程创建一个子进程。
  • b. exec()函数负责读取可执行文件并将其载入地址空间开始运行。
# linux-0.11\kernel\system_call.s
.align 2
_sys_fork:
	call _find_empty_process
	testl %eax,%eax
	js 1f
	push %gs
	pushl %esi
	pushl %edi
	pushl %ebp
	pushl %eax
	call _copy_process
	addl $20,%esp
1:	ret

内核通过sys_fork系统调用的来实现进程的创建,其中调用find_empty_process()与copy_process()两个函数。

  • 首先调用C函数find_empty_process(),取得一个进程号pid。在任务数组task[]中找到闲置任务下标i。
  • 然后调用copy_process()复制进程。并将新创建的进程与task[i]建立联系。
3.1 find_empty_process详解

首先通过find_empty_process(),在执行repeat过程中取得一个进程号last_pid,接着在任务数组task[]中找到闲置任务下标i。这两个值之后会以传入到copy_process()中。

// linux-0.11\kernel\fork.c
int find_empty_process(void)
{
	int i;
	repeat:
		if ((++last_pid)<0) last_pid=1;
		for(i=0 ; i<NR_TASKS ; i++)
			if (task[i] && task[i]->pid == last_pid) goto repeat;
	for(i=1 ; i<NR_TASKS ; i++)
		if (!task[i])
			return i;
	return -EAGAIN;
}
3.2 copy_process详解

然后调用copy_process()复制进程。其详细流程如下:

// linux-0.11\kernel\fork.c
int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,
		long ebx,long ecx,long edx,
		long fs,long es,long ds,
		long eip,long cs,long eflags,long esp,long ss)
{
	struct task_struct *p;
	int i;
	struct file *f;

	p = (struct task_struct *) get_free_page();
	if (!p)
		return -EAGAIN;
	task[nr] = p;
	*p = *current;	/* NOTE! this doesn't copy the supervisor stack */
	p->state = TASK_UNINTERRUPTIBLE;
	p->pid = last_pid;
	p->father = current->pid;
	p->counter = p->priority;
	p->signal = 0;
	p->alarm = 0;
	p->leader = 0;		/* process leadership doesn't inherit */
	p->utime = p->stime = 0;
	p->cutime = p->cstime = 0;
	p->start_time = jiffies;
	p->tss.back_link = 0;
	p->tss.esp0 = PAGE_SIZE + (long) p;
	p->tss.ss0 = 0x10;
	p->tss.eip = eip;
	p->tss.eflags = eflags;
	p->tss.eax = 0;
	p->tss.ecx = ecx;
	p->tss.edx = edx;
	p->tss.ebx = ebx;
	p->tss.esp = esp;
	p->tss.ebp = ebp;
	p->tss.esi = esi;
	p->tss.edi = edi;
	p->tss.es = es & 0xffff;
	p->tss.cs = cs & 0xffff;
	p->tss.ss = ss & 0xffff;
	p->tss.ds = ds & 0xffff;
	p->tss.fs = fs & 0xffff;
	p->tss.gs = gs & 0xffff;
	p->tss.ldt = _LDT(nr);
	p->tss.trace_bitmap = 0x80000000;
	if (last_task_used_math == current)
		__asm__("clts ; fnsave %0"::"m" (p->tss.i387));
	if (copy_mem(nr,p)) {
		task[nr] = NULL;
		free_page((long) p);
		return -EAGAIN;
	}
	for (i=0; i<NR_OPEN;i++)
		if (f=p->filp[i])
			f->f_count++;
	if (current->pwd)
		current->pwd->i_count++;
	if (current->root)
		current->root->i_count++;
	if (current->executable)
		current->executable->i_count++;
	set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss));
	set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));
	p->state = TASK_RUNNING;	/* do this last, just in case */
	return last_pid;
}
  1. get_free_page ()为新进程创建task_struct结构。并将该结构体指针放入任务数组task[]中,元素下标由find_empty_process提供。(Line11-Line14)
  2. 新创建的子进程task_struct结构体内容与当前进程(父进程)相同。(Line15)
  3. 子进程的状态被设置为TASK_UNINTERRUPTIBLE以保证它不会被投入运行。(Line16)
  4. 子进程着手使自己与父进程区分开来,一些成员被清0或者设置为初始值,大多数task_struct的成员没有被修改。(Line17-Line48)
  5. 设置子进程的代码段基址、数据段基址、限长并将父进程的页表复制给子进程。这里只复制页目录和页表项。需要申请新的页面来存放新页表,原内存区被共享;此后两个进程将共享内存区,直到有一个进程执行写操作时,才分配新的内存页(写时复制机制)。(Line49-Line53)
  6. 遍历子进程打开的文件,对应这些文件的打开次数增一。(Line54-Line56)
  7. 将父进程的pwd, root 和executable 引用次数增一。pwd当前工作目录节点;root根目录节点;executable执行文件节点。(Line57-Line62)
  8. 在全局表中设置子进程的任务状态段/局部表描述符;分别占全局描述符表的一个字段。(Line63-Line64)
  9. 将子进程设置成可运行状态,返回子进程(当前创建的新进程)的PID。(Line65-Line66)

四、进程的加载和运行

4.0 进程加载与运行过程的概述

这里分为几个部分:

  1. 加载程序之前需要做一些准备工作,如准备进程参数和环境变量所占用的页面,检查可执行文件等。

  2. 加载子进程自己的程序,那么对于从父进程拷贝的资源,有些要解除,有些要清零,在这里需要进行个性化设置。

  3. 程序加载完成之后,会调整eip和esp使得子进程之后被运行。但子进程和父进程解除了共享页面的关系,控制页面的页表已经释放。这意味着页目录项的内容为0,包括P位也为0。那么子进程程序一开始执行,MMU解析线性地址值时就会发现对应的页目录项P位为0,因此产生缺页中断。

  4. 产生缺页中断并由操作系统响应。缺页中断信号产生后,page_fault这个服务程序将对此进行响应,并最终在_page_falut中通过调用call _do_no_page调用到缺页中断处理程序_do_no_page()中执行。

4.1 do_execve()详解

do_execve()函数负责读取可执行文件并将其载入地址空间开始运行。其详细流程如下:

int do_execve(unsigned long * eip,long tmp,char * filename,
	char ** argv, char ** envp)
{
	struct m_inode * inode;
	struct buffer_head * bh;
	struct exec ex;
	unsigned long page[MAX_ARG_PAGES];
	int i,argc,envc;
	int e_uid, e_gid;
	int retval;
	int sh_bang = 0;
	unsigned long p=PAGE_SIZE*MAX_ARG_PAGES-4;

	if ((0xffff & eip[1]) != 0x000f)
		panic("execve called from supervisor mode");
	for (i=0 ; i<MAX_ARG_PAGES ; i++)	/* clear page-table */
		page[i]=0;
	if (!(inode=namei(filename)))		/* get executables inode */
		return -ENOENT;
	argc = count(argv);
	envc = count(envp);
	
restart_interp:
	if (!S_ISREG(inode->i_mode)) {	/* must be regular file */
		retval = -EACCES;
		goto exec_error2;
	}
	i = inode->i_mode;
	e_uid = (i & S_ISUID) ? inode->i_uid : current->euid;
	e_gid = (i & S_ISGID) ? inode->i_gid : current->egid;
	if (current->euid == inode->i_uid)
		i >>= 6;
	else if (current->egid == inode->i_gid)
		i >>= 3;
	if (!(i & 1) &&
	    !((inode->i_mode & 0111) && suser())) {
		retval = -ENOEXEC;
		goto exec_error2;
	}
	if (!(bh = bread(inode->i_dev,inode->i_zone[0]))) {
		retval = -EACCES;
		goto exec_error2;
	}
	ex = *((struct exec *) bh->b_data);	/* read exec-header */
	if ((bh->b_data[0] == '#') && (bh->b_data[1] == '!') && (!sh_bang)) {
		/*
		 * This section does the #! interpretation.
		 * Sorta complicated, but hopefully it will work.  -TYT
		 */

		char buf[1023], *cp, *interp, *i_name, *i_arg;
		unsigned long old_fs;

		strncpy(buf, bh->b_data+2, 1022);
		brelse(bh);
		iput(inode);
		buf[1022] = '\0';
		if (cp = strchr(buf, '\n')) {
			*cp = '\0';
			for (cp = buf; (*cp == ' ') || (*cp == '\t'); cp++);
		}
		if (!cp || *cp == '\0') {
			retval = -ENOEXEC; /* No interpreter name found */
			goto exec_error1;
		}
		interp = i_name = cp;
		i_arg = 0;
		for ( ; *cp && (*cp != ' ') && (*cp != '\t'); cp++) {
 			if (*cp == '/')
				i_name = cp+1;
		}
		if (*cp) {
			*cp++ = '\0';
			i_arg = cp;
		}
		/*
		 * OK, we've parsed out the interpreter name and
		 * (optional) argument.
		 */
		if (sh_bang++ == 0) {
			p = copy_strings(envc, envp, page, p, 0);
			p = copy_strings(--argc, argv+1, page, p, 0);
		}
		/*
		 * Splice in (1) the interpreter's name for argv[0]
		 *           (2) (optional) argument to interpreter
		 *           (3) filename of shell script
		 *
		 * This is done in reverse order, because of how the
		 * user environment and arguments are stored.
		 */
		p = copy_strings(1, &filename, page, p, 1);
		argc++;
		if (i_arg) {
			p = copy_strings(1, &i_arg, page, p, 2);
			argc++;
		}
		p = copy_strings(1, &i_name, page, p, 2);
		argc++;
		if (!p) {
			retval = -ENOMEM;
			goto exec_error1;
		}
		/*
		 * OK, now restart the process with the interpreter's inode.
		 */
		old_fs = get_fs();
		set_fs(get_ds());
		if (!(inode=namei(interp))) { /* get executables inode */
			set_fs(old_fs);
			retval = -ENOENT;
			goto exec_error1;
		}
		set_fs(old_fs);
		goto restart_interp;
	}
	brelse(bh);
	
	if (N_MAGIC(ex) != ZMAGIC || ex.a_trsize || ex.a_drsize ||
		ex.a_text+ex.a_data+ex.a_bss>0x3000000 ||
		inode->i_size < ex.a_text+ex.a_data+ex.a_syms+N_TXTOFF(ex)) {
		retval = -ENOEXEC;
		goto exec_error2;
	}
	if (N_TXTOFF(ex) != BLOCK_SIZE) {
		printk("%s: N_TXTOFF != BLOCK_SIZE. See a.out.h.", filename);
		retval = -ENOEXEC;
		goto exec_error2;
	}
	if (!sh_bang) {
		p = copy_strings(envc,envp,page,p,0);
		p = copy_strings(argc,argv,page,p,0);
		if (!p) {
			retval = -ENOMEM;
			goto exec_error2;
		}
	}
/* OK, This is the point of no return */
	if (current->executable)
		iput(current->executable);
	current->executable = inode;
	for (i=0 ; i<32 ; i++)
		current->sigaction[i].sa_handler = NULL;
	for (i=0 ; i<NR_OPEN ; i++)
		if ((current->close_on_exec>>i)&1)
			sys_close(i);
	current->close_on_exec = 0;
	free_page_tables(get_base(current->ldt[1]),get_limit(0x0f));
	free_page_tables(get_base(current->ldt[2]),get_limit(0x17));
	if (last_task_used_math == current)
		last_task_used_math = NULL;
	current->used_math = 0;
	p += change_ldt(ex.a_text,page)-MAX_ARG_PAGES*PAGE_SIZE;
	p = (unsigned long) create_tables((char *)p,argc,envc);
	current->brk = ex.a_bss +
		(current->end_data = ex.a_data +
		(current->end_code = ex.a_text));
	current->start_stack = p & 0xfffff000;
	current->euid = e_uid;
	current->egid = e_gid;
	i = ex.a_text+ex.a_data;
	while (i&0xfff)
		put_fs_byte(0,(char *) (i++));
	eip[0] = ex.a_entry;		/* eip, magic happens :-) */
	eip[3] = p;			/* stack pointer */
	return 0;
exec_error2:
	iput(inode);
exec_error1:
	for (i=0 ; i<MAX_ARG_PAGES ; i++)
		free_page(page[i]);
	return(retval);
}

  1. 为管理str1进程参数和环境变量所占用的页面做准备,读取当前进程所在文件的节点i。(Line14-Line21)
  2. 检查文件是否为regular file。(Line24-Line27)
  3. 检查被执行文件的执行权限,看本进程是否有权执行它。(Line28-Line39)
  4. 通过I 节点找到文件头(读取执行头部分),对文件进行检测,如果执行文件开始的两个字节为’#!’,并且sh_bang 标志没有置位,则处理脚本文件的执行。(Line44-Line117)
  5. 代码、数据和堆的总长度不能超过48M。(Line119-Line124)
  6. 如果执行文件执行头部分长度不等于一个内存块大小(1024 字节),也不能执行。转exec_error2。(Line125-Line129)
  7. 如果sh_bang 标志没有设置,则复制指定个数的环境变量字符串和参数到参数和环境空间中。若sh_bang 标志已经设置,则表明是将运行脚本程序,此时环境变量页面已经复制,无须再复制。(Line130-Line137)
  8. 现在要加载自己的程序了。要从子进程的可执行文件载入程序,就不需要共享父进程可执行文件的i节点了。于是iput解除与父进程可执行文件的i节点的关系,用子进程可执行文件节点inode取代。(Line139-Line141)
  9. 把信号句柄清空,准备加载str1进程自己的信号处理程序。(Line142-Line143)
  10. 根据执行时关闭(close_on_exec)文件句柄位图标志,关闭指定的打开文件,并把打开文件的屏蔽位清零。(Line144-Line147)
  11. str1和shell之前共享相同的页面,现在子进程要加载自己的程序,因此要解除关系。于是释放代码段共享的页面和数据段共享的页面。释放掉共享的页面,页表项清零;释放掉页表自身占用的页面,页目录项清零。(Line148-Line149)
  12. str1进程要加载自己的程序,需要根据程序的长度重新设置LDT。传入的参数是ex.a_text(代码段长度)和page(参数和环境字符串空间的页面指针数组)。(Line153-Line154)
  13. 对子进程的task_struct中的brk、start_stack等信息进行设置。根据ex头部的字段设置程序控制字段。(Line155-Line163)
  14. 最后调整eip和esp, 使软中断返回后,直接从子进程程序开始位置执行。子进程和父进程解除了共享页面的关系,控制页面的页表已经释放,这意味着页目录项的内容为0,包括P位也为0。子进程程序一开始执行,MMU解析线性地址值时就会发现对应的页目录项P位为0,因此产生缺页中断。
4.2 do_no_page()详解

产生缺页中断并由操作系统响应。缺页中断信号产生后,page_fault这个服务程序将对此进行响应,并最终在_page_falut中通过调用call _do_no_page调用到缺页中断处理程序_do_no_page()中执行。

void do_no_page(unsigned long error_code,unsigned long address)
{
	int nr[4];
	unsigned long tmp;
	unsigned long page;
	int block,i;

	address &= 0xfffff000;
	tmp = address - current->start_code;
	if (!current->executable || tmp >= current->end_data) {
		get_empty_page(address);
		return;
	}
	if (share_page(tmp))
		return;
	if (!(page = get_free_page()))
		oom();
/* remember that 1 block is used for header */
	block = 1 + tmp/BLOCK_SIZE;
	for (i=0 ; i<4 ; block++,i++)
		nr[i] = bmap(current->executable,block);
	bread_page(page,current->executable->i_dev,nr);
	i = tmp + 4096 - current->end_data;
	tmp = page + 4096;
	while (i-- > 0) {
		tmp--;
		*(char *)tmp = 0;
	}
	if (put_page(page,address))
		return;
	free_page(page);
	oom();
}

  1. 在do_no_page中首先做以下两方面的检测。
  • 进程是否已经把程序加载进来了,或者产生缺页中断的线性地址值是否已经超出了程序代码末端。executable为进程所在文件i节点,end_data为程序代码末端。(Line8-Line13)
  • 是否有可能与某个现有的进程共享代码。(Line14)
  1. 现在需要将子进程的程序从硬盘上加载进来。首先在主存中申请一个空闲页面,准备将程序最起始的部分加载进来。所有分配给子进程的页面存在两套映射关系,一套是与内核线性地址空间的映射,一套是与进程线性地址空间的映射。(Line16)
  2. 将可执行程序从硬盘加载到新分配的页面中,一次加载4KB内容。从硬盘上读取进程信息。 (Line19-Line28)
  3. 将分配给子进程的物理内存地址与其线性地址空间对应。程序载入之后,将这页内存映射到进程的线性地址空间内。Put_page()映射到子进程的线性地址空间内。在这个过程中对页目录项进行了设置。(Line29-Line31)

五、进程的终结

5.0 进程退出过程的概述

进程调用Exit()函数进行退出,最终会映射到sys_exit()函数去执行,并调用do_exit()函数来处理进程退出的相关事务。进程退出包括两个方面:

  • 释放进程代码与数据所占用的物理内存并解除与进程对应的可执行文件的关系,由子进程负责。
  • 释放进程的管理结构task_struct所占用的物理内存并解除与task[64]的关系,由父进程负责。
5.1 do_exit()详解
int do_exit(long code)
{
	int i;

	free_page_tables(get_base(current->ldt[1]),get_limit(0x0f));
	free_page_tables(get_base(current->ldt[2]),get_limit(0x17));
	for (i=0 ; i<NR_TASKS ; i++)
		if (task[i] && task[i]->father == current->pid) {
			task[i]->father = 1;
			if (task[i]->state == TASK_ZOMBIE)
				/* assumption task[1] is always init */
				(void) send_sig(SIGCHLD, task[1], 1);
		}
	for (i=0 ; i<NR_OPEN ; i++)
		if (current->filp[i])
			sys_close(i);
	iput(current->pwd);
	current->pwd=NULL;
	iput(current->root);
	current->root=NULL;
	iput(current->executable);
	current->executable=NULL;
	if (current->leader && current->tty >= 0)
		tty_table[current->tty].pgrp = 0;
	if (last_task_used_math == current)
		last_task_used_math = NULL;
	if (current->leader)
		kill_session();
	current->state = TASK_ZOMBIE;
	current->exit_code = code;
	tell_father(current->father);
	schedule();
	return (-1);	/* just to suppress warnings */
}
  1. 释放子进程所占页面。释放内存页面(包括已清栈但尚未释放的内存页面),并将管理这些页面的页表以及页目录项释放掉。(Line5-Line6)
  2. 如果当前进程有子进程,就将子进程的father 置为1(其父进程改为进程1)。如果该子进程已经处于僵死(ZOMBIE)状态,则向进程1 发送子进程终止信号SIGCHLD。(Line7-Line13)
  3. 关闭当前打开的所有文件,对当前进程工作目录pwd、根目录root 以及运行程序的i 节点进行同步操作,并分别置空。如果当前进程是领头(leader)进程并且其有控制的终端,则释放该终端。如果当前进程上次使用过协处理器,则将last_task_used_math 置空。如果当前进程是leader 进程,则终止所有相关进程。(Line14-Line28)
  4. 内核将子进程设置为僵死状态,并设置退出码,向它的父进程发送“子进程退出“信号。(Line29-Line31)
  5. 重新调度进程的运行。当父进程被调度时,会释放进程管理结构task_struct所占用的物理内存并解除与task[64]的关系。(Line32)
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值