java打印对象内存地址 分布式事务 事务消息 分布式事务 几种解决方案 分布式事务-Seata 分布式事务-Seata 分布式事务-LCN-TCC 分布式事务-LCN 分布式事务-消息队列-定时任务-本地事件表 Zuul网关实战02 Zuul网关实战01 灰度发布落地实战2 灰度发布落地实战1 Gsnova on Heroku build Systemd Debian system initialization manage multi id_rsa ubuntu 64bits cannot run 32bits app REHL power auditing Debug Assembly for ARMv8 on QEMU ARM体系结构--寄存器 Run Debian iso on QEMU ARMv8 QEMU ARM64 guide cross compiler install buildroot install QEMU install python入门--数据结构 python入门--内置数据类型 python入门--类 异常 python入门--条件表达式 方法 python入门--数字 字符串 数组 RTC驱动分析 块设备驱动 TCP UDP socket 触摸屏驱动 USB驱动 LED按键中断 LCD 驱动 驱动信号 根文件系统 实验 内核实验 字符设备驱动程序 绪论 uboot 实验 LCD 实验 系统时钟和UART 中断控制器 Nand Flash控制器 MMU 实验 储存管理器实验 GPIO实验 点亮LED 编译加载驱动 制作烧写内核 dnw替代方法 MINI2440 TQ2440安装配套Linux 使用NFS 制作烧写跟文件系统 grub引导Windows 烧写裸版程序-linux Ubuntu 网络没有 eth0 Linux自动挂载 烧写裸板程序 电路基础 Mac词典 Vim插件 Assembly 综合研究 Assembly 指令总结 Assembly 直接定址表 Assembly 使用BIOS进行键盘输入和磁盘读写 Assembly 外中断 Assembly 端口 Assembly int指令 Assembly 内中断 Assembly 标志寄存器 Assembly 转移指令的原理 Assembly Call和ret指令 Assembly 数据处理两个基本问题 Assembly 灵活定位内存地址 Assembly 包含多个段的程序 Assembly [bx] loop Assembly 第一个程序 Assembly 寄存器 (内存访问) Assembly 寄存器 AWS VPN with EC2 hidden file in picture(linux) Assembly 基础 idea shortcuts 常用快捷键 idea plugin folder install ruby and jekyll

内核学习-->进程管理

2015年05月06日

##1. 进程管理

###1.1 进程概念 进程就是处于执行期的程序,是程序执行的一个实例,要素是“程序”加“执行”。单纯的程序是存放在某种介质中的二进制代码。 进程创建的时候,几乎与父进程相同,与父进程有相同的程序代码,但是有各自独立的数据拷贝——堆和栈。子进程对一个内存单元的修改,父进程是不可见的,反之亦然。

###1.2 线程概念 多个轻量级进程可以共享一些资源,但是又可以由内核独立调度,一个睡眠另一个是可以运行的。轻量级进程和线程关联起来就是线程的概念。

###1.3进程描述符&线程描述符 include/linux/sched.h:

struct task_struct {
	volatile long state;	/* -1 unrunnable, 0 runnable, >0 stopped */
	void *stack;
	atomic_t usage;
	unsigned int flags;	/* per process flags, defined below */
	unsigned int ptrace;
.............................
.............................
.............................
}

进程描述符中包含的数据能够完整的描述一个正在执行的程序,涵盖了进程打开的文件,进程的地址空间,挂起的信号,进程的状态等等信息。

###1.4 进程堆栈 一个进程有两个堆栈:用户堆栈,内核堆栈 进程执行系统调用陷入内核后,处理器转换为了特权模式,对于ARM来说普通模式和用户模式的栈针(SP)是不同的寄存器,此时使用的栈指针就是内核栈指针,他指向内核为每个进程分配的内核栈空间。内核栈同时用于保存一些系统调用前的应用层信息(如用户空间栈指针、系统调用参数)。

Linux把两个不同的数据结构紧凑的放在一个单独为进程分配的存储区域内。一个是内核态的进程堆栈,另一个是线程描述符thread_info结构。

include/linux/sched.h:

union thread_union {
	struct thread_info thread_info;	//线程描述符
	unsigned long stack[THREAD_SIZE/sizeof(long)];	//内核态的进程堆栈
};

arch/arm/include/asm/thread_info.h:


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 */
	struct exec_domain	*exec_domain;	/* execution domain */
	__u32			cpu;		/* cpu */
	__u32			cpu_domain;	/* cpu domain */
	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
	struct restart_block	restart_block;
};

该段存储区域的示意图为:

Alt text

#define THREAD_SIZE 8192(0x2000)

Arm的sp寄存器是CPU的栈指针,用来存放栈顶单元的地址。从用户态刚刚切换到内核态的时候,进程的内核栈总是空的,栈起始与这段内存区的末端,并朝开始的方向增长。如何通过当前堆栈的sp的值获得当前这段内存区的起始地址,见下面的代码:


static inline struct thread_info *current_thread_info(void)
{
register unsigned long sp asm (“sp”);
return (struct thread_info *)(sp & ~(THREAD_SIZE -1));
}

通过上面的代码很容易通过sp获得thread_info在内存中的起始地址。进程常用的是进程描述符地址而不是thread_info,所以为了获得在当前CPU上运行的描述符指针,可以使用:在Linux的current.h中有这段code:


#define get_current() (current_thread_info()->task)
#define current get_current()

所以仅仅通过检查内核栈,就能获得当前正确的进程。

###1.5 进程关系 程序创建进程具有父子关系,如果一个进程创建多个子进程,则子进程之间具有兄弟关系。

假设有一个进程p,则他的task_struct中有如下字段:

Real_parent:指向了创建进程p的父进程的描述符。

Parent:指向了p的当前的父进程。

Children:链表的头部,链表中所有元素都是p创建的子进程。

Sibling:指向兄弟进程中,下一个或者前一个sibling元素的指针。

###1.6进程状态 进程的状态大体上分为

(1)正在运行或者马上运行

(2)进程挂起(interruptible,uninterruptible)

(3)进程停止(暂停)

(4)进程被跟踪,暂停

include/linux/sched.h内核中的宏定义:


#define TASK_RUNNING		0		//要么在CPU上运行,要么准备运行
#define TASK_INTERRUPTIBLE	1		//进程被挂起,直到硬件中断释放进程等待的资源,或者产生一个信号都可以把进程状态设置为TASK_RUNNING。
#define TASK_UNINTERRUPTIBLE	2	//同上,但是信号不能唤醒他。
#define __TASK_STOPPED		4		//进程的执行被暂停。
#define __TASK_TRACED		8		//进程的执行被debugger暂停。

###1.7进程队列 ###1.7.1任务队列 通过双向链表把内核中的进程联系起来。 遍历所有的进程:


#define for_each_process(p) \
	for (p = &init_task ; (p = next_task(p)) != &init_task ; )
	

###1.7.2运行队列 所有处于TASK_RUNNING状态的进程组成的队列。进程的运行队列是个抽象的概念,例如CFS公平调度算法使用的运行队列使用红黑树来组织的。调度算法通过某种调度策略来实现对可运行队列的增减任务。

###1.7.3等待队列 所有处于TASK_UNINTERRUPTIBLE,TASK_INTERRUPTIBLE状态的进程组成的队列。 当多个等待队列和信号量混合使用的时候,谨防死锁。

Alt text

我们经常在写驱动的时候在等待某个条件的时候需要阻塞一个进程,我们经常使用一些Linux的API,比如: include/linux/wait.h:

#define wait_event(wq, condition)					\
do {									\
	might_sleep();							\
	if (condition)							\
		break;							\
	__wait_event(wq, condition);					\
} while (0)


#define ___wait_event(wq, condition, state, exclusive, ret, cmd)	\
({									\
	__label__ __out;						\
	wait_queue_t __wait;						\
	long __ret = ret;	/* explicit shadow */			\
									\
	INIT_LIST_HEAD(&__wait.task_list);				\
	if (exclusive)							\
		__wait.flags = WQ_FLAG_EXCLUSIVE;			\
	else								\
		__wait.flags = 0;					\
									\
	for (;;) {							\
		long __int = prepare_to_wait_event(&wq, &__wait, state);\
									\
		if (condition)						\
			break;						\
									\
		if (___wait_is_interruptible(state) && __int) {		\
			__ret = __int;					\
			if (exclusive) {				\
				abort_exclusive_wait(&wq, &__wait,	\
						     state, NULL);	\
				goto __out;				\
			}						\
			break;						\
		}							\
									\
		cmd;							\
	}								\
	finish_wait(&wq, &__wait);					\
__out:	__ret;								\
})


#define __wait_event(wq, condition)					\
	(void)___wait_event(wq, condition, TASK_UNINTERRUPTIBLE, 0, 0,	\
			    schedule())
			    

finish_wait把进程的状态再次设置为TASK_RUNNING状态,仅发生在调用schedule()之前唤醒条件为真的情况下。

DEFINE_WAIT(__wait); 初始化一个叫wait_queue_t的等待队列元素,该等待队列结构原型为:


struct __wait_queue {
	unsigned int		flags;
	void			*private;
	wait_queue_func_t	func;
	struct list_head	task_list;
};


#define DEFINE_WAIT_FUNC(name, function)				\
	wait_queue_t name = {						\
		.private	= current,				\
		.func		= function,				\
		.task_list	= LIST_HEAD_INIT((name).task_list),	\
	}

#define DEFINE_WAIT(name) DEFINE_WAIT_FUNC(name, autoremove_wake_function)

该函数把当前进程的task_struct指针赋值给private指针变量,从而如上图所示,一个完整的等待队列元素初始化完毕,并且和相应的进程相关联起来。autoremove_wake_function为下面这个函数prepare_to_wait_event 把上面初始化好了的__wait等待队列元素,添加到以wq作为等待队列头的进程等待队列中。Wq的初始化为: DECLARE_WAIT_QUEUE_HEAD(wq)

DECLARE_WAIT_QUEUE_HEAD是个宏定义:


#define __WAIT_QUEUE_HEAD_INITIALIZER(name) {				\
	.lock		= __SPIN_LOCK_UNLOCKED(name.lock),		\
	.task_list	= { &(name).task_list, &(name).task_list } }

#define DECLARE_WAIT_QUEUE_HEAD(name) \
	wait_queue_head_t name = __WAIT_QUEUE_HEAD_INITIALIZER(name)

完成等待队列头的初始化。


void
prepare_to_wait(wait_queue_head_t *q, wait_queue_t *wait, int state)
{
	unsigned long flags;

	wait->flags &= ~WQ_FLAG_EXCLUSIVE;
注释:ldd3中描述flags为0表示非互斥进程,不知道为什么这边始终都是设置为0。
难道wait_event_xxx相关的API都是非互斥进程?不能用来等待访问临界资源。
最后查找代码prepare_to_wait发现这个函数是专门插入非互斥等待队列的。
prepare_to_wait_exclusive函数是插入互斥等待队列的。
	spin_lock_irqsave(&q->lock, flags);
	if (list_empty(&wait->task_list))
		__add_wait_queue(q, wait);
	set_current_state(state);
	spin_unlock_irqrestore(&q->lock, flags);
}
EXPORT_SYMBOL(prepare_to_wait);


struct __wait_queue_head {
	spinlock_t		lock;
	struct list_head	task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;


static inline void __add_wait_queue(wait_queue_head_t *head, wait_queue_t *new)
{
	list_add(&new->task_list, &head->task_list);
}

###1.8进程创建 Linux应用程序中,clone(),vfork(),fork()的为进程创建的系统调用。

###1.9内核进程 ###1.9.1 定义 在Linux内核中,所谓的内核线程实际上是一个共享父进程地址空间的进程,它有自己的系统堆栈;所以他们依然是一个进程,只不过这些进程可以与其他进程共享某些资源,这里的其他进程也是所谓的线程。

###1.9.2区别 内核线程没有自己的地址空间,所以他们的”current->mm”都是空的; 内核线程只能在内核空间操作,不能与用户空间交互; 跟普通进程一样,内核线程也有优先级和被调度。

###1.9.3创建 kthread_create接口,则是标准的内核线程创建接口,只须调用该接口便可创建内核线程; 默认创建的线程是存于不可运行的状态,所以需要在父进程中通过调用wake_up_process()函数来启动该线程。创建一个内核线程并要它运行起来,可以调用kthread_run接口函数。

我们还注意到kernel_thread函数也是可以创建内核线程的。由他来创建内核第一个线程1.

###1.9.4退出 当线程执行到函数末尾时会自动调用内核中do_exit()函数来退出或其他线程调用kthread_stop()来指定线程退出

###1.10进程0 ###1.10.1进程0 所有进程的祖先叫做进程0,Idle进程,他是Linux的初始化阶段从无到有创建的一个内核线程。

init/init_task.c:

struct task_struct init_task = INIT_TASK(init_task);

###1.10.2线程1 Start_kernel函数初始化内核需要的所有数据结构,激活中断,创建另一个内核线程1: kernel_init。

kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);

创建内核线程1后进程0进入idle状态:

cpu_idle();

###1.10.3进程1 线程1 ‘kernel_init’在进行了内核初始化后,会调用:

run_init_process(“/sbin/init”);

该函数主要作用就是通过execve()系统调用装入可执行程序init,从而内核线程1变成了一个普通的进程1。