百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分析 > 正文

Linux内核设计与实现—进程管理(linux内核原理与实现)

liebian365 2025-04-05 20:00 4 浏览 0 评论

进程

进程就是处于执行期的程序(目标码存放在某种存储介质上)。进并不仅仅局限于一段可执行程序代码(Unix 称其为代码段,text section)。通常进程还要包含其他资源,像打开的文件,挂起的信号,内核内部数据,处理器状态,一个或多个具有内存映射的内存地址空间及一个或多个执行线程,当然还包括用来存放全局变量的数据段等。实际上,进程就是正在执行的程序代码的实时结果。内核需要有效而又透明地管理所有细节。

执行线程,简称线程 (thread),是在进程中活动的对象。每个线程都拥有一个独立的程序计数器、进程栈和一组进程寄存器。内核调度的对象是线程,而不是进程。在传统的 Unix 系统中,一个进程只包含一个线程,但现在的系统中,包含多个线程的多线程程序司空见惯。Linux 系统的线程实现非常特别:它对线程和进程并不特别区分。对 Linux 而言,线程只不过是一种特殊的进程罢了。

程序本身并不是进程,进程是处于执行期的程序以及相关的资源的总称。实际上,完全可能存在两个或多个不同的进程执行的是同一个程序。并且两个或两个以上并存的进程还可以共享许多诸如打开的文件、地址空间之类的资源。

无疑,进程在创建它的时刻开始存活。在 Linux 系统中,这通常是调用 fork() 系统的结果该系统调用通过复制一个现有进来创建一个全新的进程。调用 fork()的进称为父进程,新产生的进程称为子进程。在该调用结束时,在返回点这个相同位置上,父进程恢复执行,子进程开始执行。fork() 系统调用从内核返回两次一次回到父进程,另一次回到新产生的子进程。

通常,创建新的进程都是为了立即执行新的、不同的程序,而接着调用 exec() 这组函数就可以创建新的地址空间,并把新的程序载入其中。

最终,程序通过 exit()系统调用退出执行。这个函数会终结进并将其占用的资源释放掉父进程可以通过 wait4()系统调用查询子进程是否终结,这其实使得进程拥有了等待特定进程执行完毕的能力。进程退出执行后被设置为僵死状态,直到它的父进程调用 wait() 或 waitpid()为止。

进程描述符及任务结构

内核把进程的列表存放在叫做任务队列 (task list)的双向循环链表中。链表中的每一项都是类型为task_struct、称为进程描述符 (process descriptor)的结构,该结构定义在<linux/sched.h> 文件中。进程描述符中包含一个具体进程的所有信息。
task struct 相对较大,在32 位机器上,它大约有 1.7B。但如果考虑到该结构内包含了内核管理一个进程所需的所有信息,那么它的大小也算相当小了。进程描述符中包含的数据能完整地描述一个正在执行的程序:它打开的文件,进程的地址空间,挂起的信号,进程的状态,还有其他更多信息.

struct task_struct {
	/**
	 * 进程状态。
	 */
	volatile long state;	/* -1 unrunnable, 0 runnable, >0 stopped */
	/**
	 * 进程的基本信息。
	 */
	struct thread_info *thread_info;
	atomic_t usage;
	unsigned long flags;	/* per process flags, defined below */
	unsigned long ptrace;

	int lock_depth;		/* Lock depth */

	/**
	 * 进行的动态优先权和静态优先权
	 */
	int prio, static_prio;
	/**
	 * 进程所在运行队列。每个优先级对应一个运行队列。
	 */
	struct list_head run_list;
	/**
	 * 指向当前运行队列的prio_array_t
	 */
	prio_array_t *array;

	/**
	 * 进程的平均睡眠时间
	 */
	unsigned long sleep_avg;
	/**
	 * timestamp-进程最近插入运行队列的时间。或涉及本进程的最近一次进程切换的时间
	 * last_ran-最近一次替换本进程的进程切换时间。
	 */
	unsigned long long timestamp, last_ran;
	/**
	 * 进程被唤醒时所使用的代码。
	 *     0:进程处于TASK_RUNNING状态。
	 *     1:进程处于TASK_INTERRUPTIBLE或者TASK_STOPPED状态,而且正在被系统调用服务例程或内核线程唤醒。
	 *     2:进程处于TASK_INTERRUPTIBLE或者TASK_STOPPED状态,而且正在被ISR或者可延迟函数唤醒。
	 *     -1:表示从UNINTERRUPTIBLE状态被唤醒
	 */
	int activated;

	/**
	 * 进程的调度类型:sched_normal,sched_rr或者sched_fifo
	 */
	unsigned long policy;
	/**
	 * 能执行进程的CPU的位掩码
	 */
	cpumask_t cpus_allowed;
	/**
	 * time_slice-在进程的时间片中,还剩余的时钟节拍数。
	 * first_time_slice-如果进程肯定不会用完其时间片,就把该标志设置为1.
	 *            xie.baoyou注:原文如此,应该是表示任务是否是第一次执行。这样,如果是第一次执行,并且在开始运行
	 *                         的第一个时间片内就运行完毕,那么就将剩余的时间片还给父进程。主要是考虑到有进程
	 *                         会大量的动态创建子进程时,而子进程会立即退出这种情况。如果不还给父进程时间片,会对这种进程不公平。
	 */
	unsigned int time_slice, first_time_slice;

#ifdef CONFIG_SCHEDSTATS
	struct sched_info sched_info;
#endif

	/**
	 * 通过此链表把所有进程链接到一个双向链表中。
	 */
	struct list_head tasks;
	/*
	 * ptrace_list/ptrace_children forms the list of my children
	 * that were stolen by a ptracer.
	 */
	/**
	 * 链表的头。该链表包含所有被debugger程序跟踪的P的子进程。
	 */
	struct list_head ptrace_children;
	/**
	 * 指向所跟踪进程其实际父进程链表的前一个下一个元素。
	 */
	struct list_head ptrace_list;

	/**
	 * mm:指向内存区描述符的指针
	 */
	struct mm_struct *mm, *active_mm;

/* task state */
	struct linux_binfmt *binfmt;
	long exit_state;
	int exit_code, exit_signal;
	int pdeath_signal;  /*  The signal sent when the parent dies  */
	/* ??? */
	unsigned long personality;
	/**
	 * 进程发出execve系统调用的次数。
	 */
	unsigned did_exec:1;
	/**
	 * 进程PID
	 */
	pid_t pid;
	/**
	 * 线程组领头线程的PID。
	 */
	pid_t tgid;
	/* 
	 * pointers to (original) parent process, youngest child, younger sibling,
	 * older sibling, respectively.  (p->father can be replaced with 
	 * p->parent->pid)
	 */
	/**
	 * 指向创建进程的进程的描述符。
	 * 如果进程的父进程不再存在,就指向进程1的描述符。
	 * 因此,如果用户运行一个后台进程而且退出了shell,后台进程就会成为init的子进程。
	 */
	struct task_struct *real_parent; /* real parent process (when being debugged) */
	/**
	 * 指向进程的当前父进程。这种进程的子进程终止时,必须向父进程发信号。
	 * 它的值通常与real_parent一致。
	 * 但偶尔也可以不同。例如:当另一个进程发出监控进程的ptrace系统调用请求时。
	 */
	struct task_struct *parent;	/* parent process */
	/*
	 * children/sibling forms the list of my children plus the
	 * tasks I'm ptracing.
	 */
	/**
	 * 链表头部。链表指向的所有元素都是进程创建的子进程。
	 */
	struct list_head children;	/* list of my children */
	/**
	 * 指向兄弟进程链表的下一个元素或前一个元素的指针。
	 */
	struct list_head sibling;	/* linkage in my parent's children list */
	/**
	 * P所在进程组的领头进程的描述符指针。
	 */
	struct task_struct *group_leader;	/* threadgroup leader */

	/* PID/PID hash table linkage. */
	/**
	 * PID散列表。通过这四个表,可以方便的查找同一线程组的其他线程,同一会话的其他进程等等。
	 */
	struct pid pids[PIDTYPE_MAX];

	struct completion *vfork_done;		/* for vfork() */
	/**
	 * 子进程在用户态的地址。这些用户态地址的值将被设置或者清除。
	 * 在do_fork时记录这些地址,稍后再设置或者清除它们的值。
	 */
	int __user *set_child_tid;		/* CLONE_CHILD_SETTID */
	int __user *clear_child_tid;		/* CLONE_CHILD_CLEARTID */

	/**
	 * 进程的实时优先级。
	 */
	unsigned long rt_priority;
	/**
	 * 以下三对值用于用户态的定时器。当定时器到期时,会向用户态进程发送信号。
	 * 每一对值分别存放了两个信号之间以节拍为单位的间隔,及定时器的当前值。
	 */
	unsigned long it_real_value, it_real_incr;
	cputime_t it_virt_value, it_virt_incr;
	cputime_t it_prof_value, it_prof_incr;
	/**
	 * 每个进程的动态定时器。用于实现ITIMER_REAL类型的间隔定时器。
	 * 由settimer系统调用初始化。
	 */
	struct timer_list real_timer;
	/**
	 * 进程在用户态和内核态下经过的节拍数
	 */
	cputime_t utime, stime;
	unsigned long nvcsw, nivcsw; /* context switch counts */
	struct timespec start_time;
/* mm fault and swap info: this can arguably be seen as either mm-specific or thread-specific */
	unsigned long min_flt, maj_flt;
/* process credentials */
	uid_t uid,euid,suid,fsuid;
	gid_t gid,egid,sgid,fsgid;
	struct group_info *group_info;
	kernel_cap_t   cap_effective, cap_inheritable, cap_permitted;
	unsigned keep_capabilities:1;
	struct user_struct *user;
#ifdef CONFIG_KEYS
	struct key *session_keyring;	/* keyring inherited over fork */
	struct key *process_keyring;	/* keyring private to this process (CLONE_THREAD) */
	struct key *thread_keyring;	/* keyring private to this thread */
#endif
	int oomkilladj; /* OOM kill score adjustment (bit shift). */
	char comm[TASK_COMM_LEN];
/* file system info */
	/**
	 * 文件系统在查找路径时使用,避免符号链接查找深度过深,导致死循环。
	 * link_count是__do_follow_link递归调用的层次。
	 * total_link_count调用__do_follow_link的总次数。
	 */
	int link_count, total_link_count;
/* ipc stuff */
	struct sysv_sem sysvsem;
/* CPU-specific state of this task */
	struct thread_struct thread;
/* filesystem information */
	/**
	 * 与文件系统相关的信息。如当前目录。
	 */
	struct fs_struct *fs;
/* open file information */
	/**
	 * 指向文件描述符的指针
	 */
	struct files_struct *files;
/* namespace */
	struct namespace *namespace;
/* signal handlers */
	/**
	 * 指向进程的信号描述符的指针
	 */
	struct signal_struct *signal;
	/**
	 * 指向进程的信号处理程序描述符的指针
	 */
	struct sighand_struct *sighand;

	/**
	 * blocked-被阻塞的信号的掩码
	 * real_blocked-被阻塞信号的临时掩码(由rt_sigtimedwait系统调用使用)
	 */
	sigset_t blocked, real_blocked;
	/**
	 * 存放私有挂起信号的数据结构
	 */
	struct sigpending pending;

	/**
	 * 信号处理程序备用堆栈的地址
	 */
	unsigned long sas_ss_sp;
	/**
	 * 信号处理程序备用堆栈的大小
	 */
	size_t sas_ss_size;
	/**
	 * 指向一个函数的指针,设备驱动程序使用这个函数阻塞进程的某些信号
	 */
	int (*notifier)(void *priv);
	/**
	 * 指向notifier函数可能使用的数据
	 */
	void *notifier_data;
	sigset_t *notifier_mask;
	
	void *security;
	struct audit_context *audit_context;

/* Thread group tracking */
   	u32 parent_exec_id;
   	u32 self_exec_id;
/* Protection of (de-)allocation: mm, files, fs, tty, keyrings */
	spinlock_t alloc_lock;
/* Protection of proc_dentry: nesting proc_lock, dcache_lock, write_lock_irq(&tasklist_lock); */
	spinlock_t proc_lock;
/* context-switch lock */
	spinlock_t switch_lock;

/* journalling filesystem info */
	/**
	 * 当前活动日志操作处理的地址。
	 */
	void *journal_info;

/* VM state */
	struct reclaim_state *reclaim_state;

	struct dentry *proc_dentry;
	struct backing_dev_info *backing_dev_info;

	struct io_context *io_context;

	unsigned long ptrace_message;
	siginfo_t *last_siginfo; /* For ptrace use.  */
/*
 * current io wait handle: wait queue entry to use for io waits
 * If this thread is processing aio, this points at the waitqueue
 * inside the currently handled kiocb. It may be NULL (i.e. default
 * to a stack based synchronous wait) if its doing sync IO.
 */
	wait_queue_t *io_wait;
/* i/o counters(bytes read/written, #syscalls */
	u64 rchar, wchar, syscr, syscw;
#if defined(CONFIG_BSD_PROCESS_ACCT)
	u64 acct_rss_mem1;	/* accumulated rss usage */
	u64 acct_vm_mem1;	/* accumulated virtual memory usage */
	clock_t acct_stimexpd;	/* clock_t-converted stime since last update */
#endif
#ifdef CONFIG_NUMA
  	struct mempolicy *mempolicy;
	short il_next;
#endif
};

进程状态

进程描述符中的 state 城描述了进程的当前状态。系统中的每个进都必然处于五种进程状态中的一种。该域的值也必为下列五种状态标志之一:

  1. TASK RUNNING(运行)进程是可执行的它或者正在执行,或者在运行队列中等待执行。这是进在用户空间中执行的唯一可能的状态这种状态也可以应用到内核空间中正在执行的进程。
  2. TASK INTERRUPTIBLE (可中断)一进程正在睡眠(就是说它被阻塞),等待某些条件的达成。一旦这些条件达成,内核就会把进程状态设置为运行。处于此状态的进程也会因为接收到信号而提前被唤醒并随时准备投入运行。
  3. TASK UNINTERRUPTIBLE(不可中断)--除了就算是接收到信号也不会被唤醒或准备投入运行外,这个状态与可打断状态相同。这个状态通常在进程必须在等待时不受干扰或等待事件很快就会发生时出现。由于处于此状态的任务对信号不做响应,所以较之可中断中断状态,使用得较少。
  4. TASK_TRACED一被其他进程跟踪的进程,例如通过 ptrace 对调试程序进行跟踪。
  5. TASK_STOPPED(停止)-进程停止执行进程没有投运行也不能投人运行。通常这种状态发生在接收到 SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU 等信号的时候。此外在调试期间接收到任何信号,都会使进程进入这种状态。

设置当前进程状态

内核经常需要调整某个进程的状态。这时最好使用 set_task_state(task,state)函数:
set_task_state(task, state);
/* 将任务 task 的状态设置为 state */
该函数将指定的进程设置为指定的状态。必要的时候,它会设置内存屏障来强制其他处理器作重新排序。否则,它等价于 :
task->state = state;
set_current_state(state)和 set_task_state(current, state)含义是等同的。参看 <linux/sched.h> 中对这些相关函数实现的说明。

/**
 * 设置进程状态。同时会加上内存屏障。
 */
#define set_task_state(tsk, state_value)		\
	set_mb((tsk)->state, (state_value))

/**
 * 设置当前进程状态。同时会加上内存屏障。
 */
#define __set_current_state(state_value)			\
	do { current->state = (state_value); } while (0)
#define set_current_state(state_value)		\
	set_mb(current->state, (state_value))

进程上下文

可执行程序代码是进程的重要组成部分。这些代码从一个可执行文件载入到进程的地址空间执行。一般程序在用户空间执行。当一个程序调执行了系统调用或者发了某个异常,它就陷入了内核空间。此时,我们称内核“代表进程执行”并处于进程上下文中。在此上下文中 current 宏是有效的。除非在此间隙有更高优先级的进程需要执行由调度器做出了相应调整,否则在内核退出的时候,程序恢复在用户空间会继续执行。

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

进程创建

Unix 的进程创建很特别。许多其他的操作系统都提供了产生 (spawn)进程的机制,首先在新的地址空间里创建进程,读入可执行文件,最后开始执行。Unix 采用了与众不同的实现方式它把上述步骤分解到两个单独的函数中去执行:fork()和exec()。首先fork()通过贝当前进程创建一个子进程。子进程与父进程的区别仅仅在于 PID(每个进程唯一)PPID(进的进程号,子进程将其设置为被拷贝进程的 PID)和某些资源和统计量(例如,起的信号,它没有必要被继承)。exec()函数负责读取可执行文件并将其载入地址空间开始运行。把这两个函数组合起来使用的效果跟其他系统使用的单一函数的效果相似。

进程终结

进程终归是要终结的。当一个进程终结时,内核必须释放它所占有的资源并告知其父进程。一般来说,进程的析构是自身引起的。它发生在进程调用 exit() 系统调用时,既可能显式地调用这个系统调用,也可能隐式地从某个程序的主函数返回(其实 C语言编译器会在 main()函数的返回点后面放置调用 exit() 的代码)。当进接受到它不能处理不能忽略的信号或异常时,它还可能被动地终结。不管进程是怎么终结的,该任务大部分都要靠 do_exit()(定义于kernel/exit.c)来完成,它要做下面这些烦的工作:

  1. 将 tast_struct 中的标志成员设置为 PF_EXITING
  2. 调用 del_timer_sync() 删除任一内核定时器。根据返回的结果,它确保没有定时器在排队,也没有定时器处理程序在运行。
  3. 如果 BSD的进程记账功能是开启的,do_exit()用acct_update_integrals()来输出记账信息。
  4. 然后调用exit_mm进用的 mm_struct如没有别的进使用它们(就是说,这个地址空间没有被共享),就彻底释放它们。
  5. 接下来调用 sem_exit()函数。如果进排队PC 信号,它则离开队列。
  6. 用 exit_fles()和exit_fs(),分别减文符文系统据的引用计。如其中某个引用计数的数值降为零,那么就代表没有进程在使用相应的资源,此时可以释放。
  7. 接着把存放在 task_struct 的 exit_code 成员中的任务退出代码置为由exit()提供的退出代码,或者去完成任何其他由内核机制规定的退出动作。退出代码存放在这里供父进程随时检索。
  8. 调用exit_notify()向父进程发送信号,给子进程重新找养父,养父为线程组中的其他线程或者为init 进程,并把进程状态(存放在 task_struct 结构的 exit_state 中)设成 EXIT_ZOMBIE。
  9. do_exit() 调用 schedule() 切换到新的进程。因为处于 EXIT_ZOMBIE 状态的进程不会再被调度,所以这是进程所执行的最后一段代码。do exit()永不返回至此,与进程相关联的所有资源都被释放掉了。进程不可运行并处于EXIT_ZOMBIE 出状态。它占用的所有内存就是内核栈、thread_info 结构和 tast_struct 构。此时进存在的唯一目的就是向它的父进程提供信息。父进程检索到信息后,或者通知内核那是无关的信息后,由进程所持有的剩余内存被释放,归还给系统使用。
/**
 * 所有进程的终止都是本函数处理的。
 * 它从内核数据结构中删除对终止进程的大部分引用(注:不是全部,进程描述符就不是)
 * 它接受进程的终止代码作为参数。
 */
fastcall NORET_TYPE void do_exit(long code)
{
	struct task_struct *tsk = current;
	int group_dead;

	profile_task_exit(tsk);

	if (unlikely(in_interrupt()))
		panic("Aiee, killing interrupt handler!");
	if (unlikely(!tsk->pid))
		panic("Attempted to kill the idle task!");
	if (unlikely(tsk->pid == 1))
		panic("Attempted to kill init!");
	if (tsk->io_context)
		exit_io_context();

	if (unlikely(current->ptrace & PT_TRACE_EXIT)) {
		current->ptrace_message = code;
		ptrace_notify((PTRACE_EVENT_EXIT << 8 sigtrap pf_exiting tsk->flags |= PF_EXITING;
	/**
	 * 从动态定时器队列中删除进程描述符。
	 */
	del_timer_sync(&tsk->real_timer);

	if (unlikely(in_atomic()))
		printk(KERN_INFO "note: %s[%d] exited with preempt_count %d\n",
				current->comm, current->pid,
				preempt_count());

	acct_update_integrals();
	update_mem_hiwater();
	group_dead = atomic_dec_and_test(&tsk->signal->live);
	if (group_dead)
		acct_process(code);

	/**
	 * exit_mm从进程描述符中分离出分页相关的描述符。
	 * 如果没有其他进程共享这些数据结构,就删除这些数据结构。
	 */
	exit_mm(tsk);

	/**
	 * exit_sem从进程描述符中分离出信号量相关的描述符
	 */
	exit_sem(tsk);
	/**
	 * __exit_files从进程描述符中分离出文件系统相关的描述符
	 */
	__exit_files(tsk);
	/**
	 * __exit_fs从进程描述符中分离出打开文件描述符相关的描述符
	 */
	__exit_fs(tsk);
	/**
	 * exit_namespace从进程描述符中分离出命名空间相关的描述符
	 */	
	exit_namespace(tsk);
	/**
	 * exit_thread从进程描述符中分离出IO权限位图相关的描述符
	 */		
	exit_thread();
	exit_keys(tsk);

	if (group_dead && tsk->signal->leader)
		disassociate_ctty(1);

	/**
	 * 如果实现了被杀死进程的执行域和可执行格式的内核函数在内核模块中
	 * 就递减它们的值。
	 * 注:这应该是为了防止意外的卸载模块。
	 */
	module_put(tsk->thread_info->exec_domain->module);
	if (tsk->binfmt)
		module_put(tsk->binfmt->module);

	/**
	 * 设置退出代码
	 */
	tsk->exit_code = code;
	/**
	 * exit_notify执行比较复杂的操作,更新了很多内核数据结构
	 */
	exit_notify(tsk);
#ifdef CONFIG_NUMA
	mpol_free(tsk->mempolicy);
	tsk->mempolicy = NULL;
#endif

	BUG_ON(!(current->flags & PF_DEAD));
	/**
	 * 完了,让其他线程运行吧
	 * 因为schedule会忽略处于EXIT_ZOMBIE状态的进程,所以进程现在是不会再运行了。
	 */
	schedule();
	/**
	 * 当然,谁还会让死掉的进程继续运行,说明内核一定是错了
	 * 注:难道schedule被谁改了,没有判断EXIT_ZOMBIE???
	 */
	BUG();
	/* Avoid "noreturn function does return".  */
	/**
	 * 仅仅为了防止编译器报警告信息而已,仅此而已。
	 */
	for (;;) ;
}

删除进程描述符

在调用了 do_exit()之后,尽管线程已经死不能再运行了,但是系统还保留了它的进程描述符。前面说过,这样做可以让系统有办法在子进程终结后仍能获得它的信息。因此,进程终结时所需的清理工作和进程描述符的删除被分开执行。在父进程获得已终结的子进程的信息后,或者通知内核它并不关注那些信息后,子进程的 task_struct 结构才被释放。
wait() 这一族函数都是通过唯一(但是很复杂)的一个系统调用 wait4() 来实现的。它的标准动作是挂起调用它的进程,直到其中的一个子进程退出,此时函数会返回该子进程的 PID。此外,调用该函数时提供的指针会包含子函数退出时的退出代码。
当最终需要释放进程描述符时,release_task()会被调用,用以完成以下工作

  1. 它调用__exit_signal0,该函数调用_unhash_process(),后者又调用 detach_pid() 从 pidhash
    上删除该进程,同时也要从任务列表中删除该进程。
  2. _exit_signal()释放目前死进程所使用的所有剩余资源,进行最统计和记录
  3. 如果这个进程是线程组最后一个进程,并且领头进程已经死掉,那么 release_task()就要通知僵死的领头进程的父进程。
  4. release_task()调用 put_task_struct() 释进程内核和 thread_info 结构所占的页,并释tast struct 所占的slab 高速缓存。
    至此,进程描述符和所有进程独享的资源就全部释放掉了。
/**
 * 释放进程描述符。如果进程已经是僵死状态,就会回收它占用的RAM。
 */
void release_task(struct task_struct * p)
{
	int zap_leader;
	task_t *leader;
	struct dentry *proc_dentry;

repeat: 
	/**
	 * 递减进程拥有者的进程个数。
	 */
	atomic_dec(&p->user->processes);
	spin_lock(&p->proc_lock);
	proc_dentry = proc_pid_unhash(p);
	write_lock_irq(&tasklist_lock);
	/**
	 * 如果进程正被跟踪,函数将它从调试程序的ptrace_children链表中删除,并让该进程重新属于初始的父进程。
	 */
	if (unlikely(p->ptrace))
		__ptrace_unlink(p);
	BUG_ON(!list_empty(&p->ptrace_list) || !list_empty(&p->ptrace_children));
	/**
	 * 删除所有的挂起信号并释放进程的signal_struct描述符。
	 * 如果该描述符不再被其他的轻量级进程使用,函数进一步删除这个数据结构。
	 * 它还会调用exit_itimers,删除所有POSIX时间间隔定时器。
	 */
	__exit_signal(p);
	/**
	 * 删除信号处理函数。
	 */
	__exit_sighand(p);
	/**
	 * __unhash_process将进程从各种hash表中摘除。它执行:
	 *    变量nr_threads减1
	 *    两次调用detach_pid,分别从PIDTYPE_PID和PIDTYPE_TGID类型的PID散列表中删除进程描述符。
	 *    如果进程是线程组的领头进程,那么再调用两次detach_pid,从PIDTYPE_PGID和PIDTYPE_SID类型的散列表中删除进程描述符。
	 *    用宏REMOVE_LINKS从进程链表中解除进程描述符的链接。
	 */
	__unhash_process(p);

	/*
	 * If we are the last non-leader member of the thread
	 * group, and the leader is zombie, then notify the
	 * group leader's parent process. (if it wants notification.)
	 */
	zap_leader = 0;
	leader = p->group_leader;
	/**
	 * 如果进程不是线程组的领头进程,领头进程处于僵死状态,并且进程是线程组的最后一个成员。
	 */
	if (leader != p && thread_group_empty(leader) && leader->exit_state == EXIT_ZOMBIE) {
		BUG_ON(leader->exit_signal == -1);
		/**
		 * 向领头进程的父进程发送一个信号,通知它:进程已经死亡。
		 */
		do_notify_parent(leader, leader->exit_signal);
		/*
		 * If we were the last child thread and the leader has
		 * exited already, and the leader's parent ignores SIGCHLD,
		 * then we are the one who should release the leader.
		 *
		 * do_notify_parent() will have marked it self-reaping in
		 * that case.
		 */
		zap_leader = (leader->exit_signal == -1);
	}

	/**
	 * sched_exit函数调整父进程的时间片。
	 */
	sched_exit(p);
	write_unlock_irq(&tasklist_lock);
	spin_unlock(&p->proc_lock);
	proc_pid_flush(proc_dentry);
	release_thread(p);
	/**
	 * 递减进程描述符的使用计数器。如果计数器变成0,则终止所有残留的对进程的引用。
	 *     递减进程所有者的user_struct数据结构的使用计数器。如果计数为0,就释放该结构。
	 *     释放进程描述符以及thread_info描述符和内核态堆栈所占用的内存区域。
	 */
	put_task_struct(p);

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



相关推荐

[西门子PLC] S7-1200PLC中所支持的数据类型详解

数据类型呢,就是讲数据的长度和属性的,也就是指定数据元素的大小,还有怎么去解释数据。每个指令起码得支持一种数据类型,有的指令还能支持好多种数据类型。所以呀,指令上用的操作数的数据类型一定得跟指令支持的...

C语言wctomb函数详解:宽字符到多字节字符的「翻译官」

核心定位wctomb是C语言中用于将宽字符转换为多字节字符的「翻译官」,它能将单个宽字符(wchar_t)转换为多字节字符(如UTF-8编码的中文)。就像一位翻译官,它能将一种语言(宽字符)翻译成...

Python 中数组和列表之间的区别(python列表和c语言数组区别)

在这篇文章中,您将了解Python中数组和列表之间的区别。Python列表Python列表是一种内置数据结构,是包含在方括号[]的元素集合。它们具有许多独特的属性,使它们与其他数据结构不同。有...

Linux内核设计与实现—进程管理(linux内核原理与实现)

进程进程就是处于执行期的程序(目标码存放在某种存储介质上)。进并不仅仅局限于一段可执行程序代码(Unix称其为代码段,textsection)。通常进程还要包含其他资源,像打开的文件,挂起的信号,...

实际工程项目中西门子S7-1500如何批量读取和写入机器人信号

方法一:DPRD_DAT:读取DP标准从站的一致性数据该指令适用于中央模块以及DP标准从站和PROFINETIO设备。可以使用以下数据类型:BOOL,BYTE,CHAR,WCHAR,WO...

C语言mbstowcs函数详解:多字节字符串到宽字符的「翻译官」

核心定位mbstowcs是C语言中用于将多字节字符串转换为宽字符字符串的「翻译官」,它能将多字节字符(如UTF-8编码的中文)转换为宽字符(wchar_t)。就像一位翻译官,它能将一种语言(多字节...

C语言mbtowc函数详解:多字节字符到宽字符的「翻译官」

核心定位mbtowc是C语言中用于将多字节字符转换为宽字符的「翻译官」,它能将单个多字节字符(如UTF-8编码的中文)转换为宽字符(wchar_t)。就像一位翻译官,它能将一种语言(多字节字符)翻...

西门子PLC系列连载|No.5 初识西门子1200PLC数据类型

导语:在之前的文章中我们介绍了PLC的相关基础知识和一些小的程序段,也讲解过博途软件使用的一些基本方法。那么我们在本章内容将为大家讲解关于西门子1200系列PLC的常用数据类型,以及这些数据类型的区别...

计算机中常见的字符编码及存储方式

常见的字符编码ASCII、GBK、GB2312、Unicode等等常识用多个字节来代表的字符称之为宽字符,而Unicode码只是宽字符编码的一种实现,宽字符并不一定是Unicodechar窄字...

西门子SCL高级语言之数据转换介绍

(整数转浮点数INT_TO_REAL)我们在做项目中经常用到各种类型的数据,这就需要转换(CONVERT)指令来转换,由于博途数据转换指令只有它一个,那我们就只记住它就可以了,注意设置需要转换...

SCL编程语言学习(2)-启保停电路(起保停电路plc程序)

“启保停”电路是学习过程中最常见的一个案例,也是最简单易懂的控制程序。如果采用梯形图编程,如图1所示。在实际工程的电路中,很少有这么简单的起保停电路,一般都需要考虑急停、限位、过载保护等多项因素,启停...

GCC的常用编译选项(gcc编译工具)

GCC(GNUCompilerCollection,GNU编译器套件)是由GNU开发的编程语言译器。对于C语言源代码文件,使用GCC生成可执行文件的过程不仅仅是编译的过程,而是要经历四个相...

「C语言」初始化数组,C语言中初始化特定列表和元素

如果没有显式地初始化数组变量,那么就会采用一般规则:如果数组具有动态存储周期,那么数组元素的值就是没有定义的。否则,所有的元素都会被默认地初始化为0(如果数组元素是指针,则会被初始化为NULL)。编...

C++11新特性(c++11新特性 lambda)

1、智能指针2、Lambda表达式3、线程库4、原子操作5、统一的列表初始化{}6、右值引用和移动构造7、引入nullptr指针8、类型推导auto和decltype智能指针:智能指针是一个...

西门子 S7-1200 PLC 数据类型详解

关注“PLC发烧友”,一起涨知识!回复:西门子全套,领西门子系列PLC电子资料包!数据类型用来描述数据的长度和属性,即用于指定数据元素的大小及如何解释数据,每个指令至少支持一个数据类型,而部分指令支持...

取消回复欢迎 发表评论: