欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

进程管理

程序员文章站 2022-04-26 17:09:23
...

一、进程的理解

1、每个进程都有一段程序供其执行,即存储在代码段的指令。但是这段指令不一定是某个进程独有的,也有可能和其它进程共享代码段的指令,比如父子进程共享代码段。

2、每个进程都有自己的系统堆栈空间。因为一个进程有两种执行状态,即用户态和内核态,当它在内核态执行时,其实就是通过系统调用执行一些内核函数,那么这些内核函数的指令是存储在内核代码段的,被所有进程共享。但是内核函数的执行中也是需要必要的堆栈空间,这就是每个进程的系统堆栈空间,这部分空间的作用是进行内核函数中参数的压栈以及存储内核函数中定义的局部变量。

3、每个进程都有“户口”,这就是内核中的task_struct数据结构。每个进程都有一个,也就是“进程控制块”。这个数据结构中记录了进程所占有的各种系统资源(内存,文件、终端等等),这也就是说进程是系统进行资源分配的基本单位。

4、进程还有自己专用的用户堆栈空间,用来存储自己的数据结构,比如tast_struct、mm_struct等等

二、tast_struct和系统堆栈空间的存储

           内核会为每个进程分配两个连续的物理页共8KB的内存空间来存储tast_struct和系统堆栈空间。

tast_struct结构占用了低地址处大约1KB空间,而系统堆栈空间从最高地址处向下增长。

          进程管理

进程在内核态运行时,常常需要访问进程的tast_struct结构,为此内核定义了current宏来获取进程tast_struct结构的地址

struct task_struct;

static inline struct task_struct * get_current(void)
{
	return current_thread_info()->task;
}
 
#define current get_current()

三、进程的状态

1、TASK_RUNNING,该状态并不是表示这个进程正在运行,而是表示这个进程可以被调度执行,即处于就绪状态。内核就可以把该进程的tast_struct结构插入以run_list为头结点的“运行队列”中。

2、TASK_INTERRUPTIBLE,睡眠状态,可以被其它进程发送的信号(signal)唤醒。当进程处在“阻塞性”的系统调用中等待某一事件的发生时,进程就处于这一状态。

3、TASK_INTERRUPTIBLE,深度睡眠状态,不能被信号唤醒。很少用

4、TASK_ZOMBILE,僵尸状态,进程已经消亡,当它的资源(task_struct)没有被父进程回收。

5、TASK_STOPPED,暂停状态,主要用于调试。当进程收到一个SIGSTOP信号后就从运行状态进入暂停状态;然后在收到一个SIGCONT信号后进程就恢复运行状态。

四、进程管理的数据结构

1、进程链表

        系统中所有进程的tast_struct结构都处在一个双向循环链表中,链表的表头就是0进程的tast_struct结构init_task。

/* 插入task_struct结构的宏 */
#define SET_LINKS(p) do {					\
	if (thread_group_leader(p))				\
		list_add_tail(&(p)->tasks,&init_task.tasks);	\
	add_parent(p, (p)->parent);				\
	} while (0)
/* 删除task_struct结构的宏 */
#define REMOVE_LINKS(p) do {					\
	if (thread_group_leader(p))				\
		list_del_init(&(p)->tasks);			\
	remove_parent(p);					\
	} while (0)
/* 遍历进程链表的宏 */
#define for_each_process(p) \
	for (p = &init_task ; (p = next_task(p)) != &init_task ; )

2、运行队列

        内核把所有可运行的进程(TASK_RUNING)放入一个以run_list开头可运行队列。这个队列按优先级级别把进程分到不同的队列中。        具体内容参见《深入理解Linux内核》第七章

3、联系进程PID和进程task_struc结构的哈希表

        很多系统调用(如kill)需要的参数都是进程的PID,那么在内核中必须能通过PID快速的得到进程task_struct结构的指针。

顺序扫描进程链表逐一检查task_struct结构的pid字段是可行的,但相当低效。为了加速查找,内核引用了哈希表。

内核中定义pid_hashfn宏(哈希函数)用来把进程PID转换为哈希表的索引值

#define pid_hashfn(nr) hash_long((unsigned long)nr, pidhash_shift)

static inline unsigned long hash_long(unsigned long val, unsigned int bits)
{
	unsigned long hash = val;

#if BITS_PER_LONG == 64
	/*  Sigh, gcc can't optimise this alone like it does for 32 bits. */
	unsigned long n = hash;
	n <<= 18;
	hash -= n;
	n <<= 33;
	hash -= n;
	n <<= 3;
	hash += n;
	n <<= 3;
	hash -= n;
	n <<= 4;
	hash += n;
	n <<= 2;
	hash += n;
#else
	/* On some cpus multiply is faster, on others gcc will do shifts */
	hash *= GOLDEN_RATIO_PRIME;
#endif

	/* High bits are more random, so use them. */
	return hash >> (BITS_PER_LONG - bits);
}

进程管理

4、等待队列

后续补充,参见《深入理解Linux内核》第三章P101

五、进程的创建、执行与消亡

            Linux将进程的创建和目标程序的执行分为两步:

    ①调用fork、clone或vfork从父进程中创建一个子进程,子进程拥有自己的task_struct结构和系统堆栈空间

    ②调用execve()让子进程执行一个可执行程序的映象

1、进程的创建

clone(int (*fn)(void*), void* child_stack, int flag, void* arg)  

fn:指定子进程的函数,这个函数返回,子进程结束。函数的返回值表示子进程的退出状态码。

child_stack:指定子进程的用户堆栈空间的起始地址,调用clone的父进程应该总是为子进程指定新的堆栈。

flag:flag的低1字节表示子进程退出时发送给父进程的信号,通常为SIGCHLD。高3字节表示有选择的复制父进程的数据段。对于不需要复制的数据结构则通过指针的复制来共享

arg:传递给fn的参数

        其实clone主要用来创建线程,通过设置flag参数有选择的复制父进程的数据结构。极端情况下,就是完全共享父进程的地址空间。Linux中线程创建函数pthread_create就是调用clone实现的。

/*
 * 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_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 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_STOPPED		0x02000000	/* Start in stopped state */

fork()

①子进程完全复制父进程所拥有的资源(数据段),即傻瓜式创建。这种创建进程的方法很低效,子进程需要拷贝父进程的整     个地址空间。它们对自己数据结构的 修改不会影响到对方。

②fork采用的是不受阻的进程创建方式,即“异步”的方式。父子进程执行的次序并不确定。但可以调用wait函数等待子进程执     行。

vfork()

①创建一个新的子进程(其实也就是一个线程),子进程共享父进程的所有资源,即通过指针复制共享数据结构,拥有相同的地址空间

②vfork保证子进程先执行,父进程被阻塞。即采用父进程受阻的方式创建进程。直到子进程退出或者调用execve函数,父进程才能被继续调度

2、系统调用execve

        进程通常是按照父进程原样复制出来的,在多数情况下,如果子进程不能执行特定的目标程序,那么子进程的创建也就没什么意义了。Linux提供了execve系统调用实现这一目的。

3、进程终止

exit_group():终止整个线程组,即整个基于多线程的应用

exit():终止某个线程,而不管该线程所属线程组中的所有其它线程

4、回收子进程

        进程可以创建一个子进程来执行特定的任务,然后调用wait()函数检查子进程是否终止。如果子进程已经终止,那么它的退出状态码将告诉父进程这个任务是否已经成功完成。

        为了遵循这个规则,不允许内核在进程一终止就丢弃包含在进程描述符字段中的数据。只用父进程发出了与被终止进程相关的wait()系统调用后,才允许删除数据。这就是引入僵尸进程的原因,尽管进程已死,但必须保存它的描述符,直到父进程得到通知。

         如果父进程先于子进程结束,系统就会存在大量的僵尸进程,它们的进程描述符就会永久占据着RAM。这也是编程中需要注意的地方。

相关标签: 进程管理