进程是程序执行的一个实例,也是系统资源调度的最小单位。如果同一个程序被多个用户同时运行,那么这个程序就有多个相对独立的进程,与此同时他们又共享相同的执行代码,在Linux系统中进程的概念类似于任务或者线程(task & threads).
进程是一个程序运行时候的一个实例实际上说的是它就是一个可以充分描述程序以达到了其可以运行状态的的一个数据和代码集合。一个进程会被产生并会复制出自己的子代,类似细胞分裂一样。从系统的角度来看进程的任务实际上就是担当承载系统资源的单位,系统在调度和分配资源的时候也会以他们作为基本单位开始进行分配。(系统中的资源很多例如CPU的时间片、内存堆栈等等)
系统是通过相应的数据结构去表示每一个进程以及他们的扩展数据结构,实际上这个结构就是我们所说的PCB,在Linux这个数据结构我们呢称为task_struct,实际上应该至少包含以下信息,比如优先级,运行状态,所在的内存空间,文件访问权限等等。
Task_struct的函数结构如下所示:
2.进程的状态
进程执行时,会根据具体情况改变状态,进程状态是调度和对换的一句,Linux中的进程主要有下面的几种状态
(1)运行态:进程正在使用CPU运行的状态。处于运行态的进程又称为当前进程(current process)。
(2)可运行态:进程已分配到除CPU外所需要的其它资源,等待系统把CPU分配给它之后即可投入运行。
(3)等待态:又称睡眠态,它是进程正在等待某个事件或某个资源时所处的状态。 等待态进一步分为可中断的等待态和不可中断的等待态。处于可中断等待态的进程可以由信号(signal)解除其等待态。处于不可中断等待态的进程,一般是直接或间接等待硬件条件。 它只能用特定的方式来解除,例如使用唤醒函数wake_up()等。
可中断的等待状态:进程被挂起,直到等到一个可以唤醒他的东西,例如一个硬件中断、某项系统资源、或一个信号量。当它等到这 些唤醒条件的之后就会进入可运行状态。
不可中断的等待:一种常见的状态就是这个进程正在访问一个独占的临界资源,这种时候处于一种不可抢占的状态。通常当进程接收 到SIGSTOP、SIGTSTP、SIGTTIN或 SIGTTOU信号后就处于这种状态。例如,正接受调试的进程就处于这种状态。
(4)暂停态:进程需要接受某种特殊处理而止运行所处的状态。通常进程在接受到外部进程的某个信号进入暂停态,例如,正在接受调试的进程就处于这种状态。
(5)僵死状态
进程虽然已经终止,但由于某种原因,父进程还没有执行wait()系统调用,终止进程的信息也还没有回收。顾名思义,处于该状态的进程就是死进程,这种进程实际上是系统中的垃圾,必须进行相应处理以释放其占用的资源。
我们在设置这些状态的时候是可以直接用语句进行的比如:p—>state = TASK_RUNNING。同时内核也会使用set_task_state和set_current_state。
3.进程的创建
可以使用fork,vfork,clone三个系统调用来创建一个新的进程,而且都死通过do_fork实现的。
1.子进程被创建后继承了父进程的资源。
2.子进程共享父进程的虚存空间。
3.写时拷贝 (copy on write):子进程在创建后共享父进程的虚存内存空间,写时拷贝技术允许父子进程能读相同的物理页。只要两者有一个进程试图写一个物理页,内核就把这个页的内容拷贝到一个新的物理 页,并把这个新的物理页分配给正在写的进程
4.子进程在创建后执行的是父进程的程序代码。
负责创建进程的函数的层次结构如下:
fork()函数创建新进程是通过下列一系列函数实现的:
fork()->sys_clone()->do_fork()->copy_process()->dup_task_struct()->copy_thread()->ret_from_fork()
do_fork 函数首先会分配一个新的 PID(但是我还没找到该调用)。接下来,do_fork 检查调试器是否在跟踪父进程。如果是,在 clone_flags 内设置 CLONE_PTRACE 标志以做好执行 fork 操作的准备。
之后 do_fork 函数还会调用 copy_process,向其传递这些标志、堆栈、注册表、父进程以及最新分配的 PID。
新的进程在 copy_process 函数内作为父进程的一个副本创建。此函数能执行除启动进程之外的所有操作,启动进程在之后进行处理。copy_process 内的第一步是验证 CLONE 标志以确保这些标志是一致的。如果不一致,就会返回 EINVAL 错误。接下来,询问 Linux Security Module (LSM) 看当前任务是否可以创建一个新任务。
接下来,调用 dup_task_struct 函数(在 ./linux/kernel/fork.c 内),这会分配一个新 task_struct 并将当前进程的描述符复制到其内。在新的线程堆栈设置好后,一些状态信息也会被初始化,并且会将控制返回给 copy_process。控制回到 copy_process 后,除了其他几个限制和安全检查之外,还会执行一些常规管理,包括在新 task_struct 上的各种初始化。
部分dup_task_struct源码如下:
//dup_task_struct根据父进程创建子进程内核栈和进程描述符:
static struct task_struct *dup_task_struct(struct task_struct *orig)
{
struct task_struct *tsk;
struct thread_info *ti;
int node = tsk_fork_get_node(orig);
int err;
/*创建进程描述符对象*/
tsk = alloc_task_struct_node(node);
if (!tsk)
return NULL;
/*给新进程分配一个新的内核堆栈*/
ti = alloc_thread_info_node(tsk, node);
if (!ti) /*如果thread info结构没申请到,释放tsk*/
goto free_tsk;
/*复制task_struct,使子进程描述符和父进程一致*/
err = arch_dup_task_struct(tsk, orig);
if (err)
goto free_ti;
tsk->stack = ti; /*task对应栈*/
#ifdef CONFIG_SECCOMP
tsk->seccomp.filter = NULL;
#endif
/*初始化thread info结构*/
setup_thread_stack(tsk, orig);//使子进程thread_info内容与父进程一致但task指向子进程task_struct
clear_user_return_notifier(tsk);
clear_tsk_need_resched(tsk);
set_task_stack_end_magic(tsk);
#ifdef CONFIG_CC_STACKPROTECTOR
tsk->stack_canary = get_random_int();
/*初始化stack_canary变量*/
.......
之后,会调用一系列复制函数来复制此进程的各个方面,比如复制开放文件描述符(copy_files)、复制符号信息(copy_sighand 和 copy_signal)、复制进程内存(copy_mm)以及最终复制线程(copy_thread)。
最终复制线程(copy_thread)部分源码
//新进程有自己的堆栈且会根据task_pt_regs中的内容进行修改。
int copy_thread(unsigned long clone_flags,
unsigned long sp, unsigned long arg,
struct task_struct *p)
{
struct pt_regs *childregs = task_pt_regs(p);
struct task_struct *tsk;
int err;
//调度到子进程时的内核栈顶
p->thread.sp = (unsigned long) childregs;
p->thread.sp0 = (unsigned long) (childregs+1);
memset(p->thread.ptrace_bps, 0, sizeof(p->thread.ptrace_bps));
if (unlikely(p->flags & PF_KTHREAD))
{
/* kernel thread */
memset(childregs, 0, sizeof(struct pt_regs));
p->thread.ip =(unsignedlong)ret_from_kernel_thread;
task_user_gs(p) = __KERNEL_STACK_CANARY;
childregs->ds = __USER_DS;
childregs->es = __USER_DS;
childregs->fs = __KERNEL_PERCPU;
childregs->bx = sp; /* function */
childregs->bp = arg;
childregs->orig_ax = -1;
childregs->cs = __KERNEL_CS | get_kernel_rpl(); childregs->flags = X86_EFLAGS_IF | X86_EFLAGS_FIXED;
p->thread.io_bitmap_ptr = NULL;
return 0;
}
*childregs = *current_pt_regs();//复制内核堆栈
childregs->ax = 0;//eax寄存器值强置为0,即子进程返回到用户态时返回值为0
if (sp)
childregs->sp = sp;//sp为父进程传给子进程的用户态栈,可以与父进程共享
p->thread.ip = (unsigned long) ret_from_fork; //调度到子进程时的第一条指令地址
task_user_gs(p) = get_user_gs(current_pt_regs());
p->thread.io_bitmap_ptr = NULL;
tsk = current;
之后,这个新任务会被指定给一个处理程序,同时对允许执行进程的处理程序进行额外的检查(cpus_allowed)。新进程的优先级从父进程的优先级继承后,执行一小部分额外的常规管理,而且控制也会被返回给 do_fork。在此时,新进程存在但尚未运行。do_fork 函数通过调用 wake_up_new_task 来修复此问题。此函数(可在 ./linux/kernel/sched.c 内找到)初始化某些调度程序的常规管理信息,将新进程放置在运行队列之内,然后将其唤醒以便执行。最后,一旦返回至 do_fork,此 PID 值即被返回给调用程序,进程完成。
4.总结
实际上,用户空间的寄存器、用户态堆栈等信息在切换到内核态的上下文时保存在内核栈中,父进程在内核态(dup_task_struct)复制出子进程,但子进程作为一个独立的进程,之后被调度运行时必须有一个指令地址,进程切换时,ip地址及当前内核栈的位置esp都存在于thread_info中,由copy_thread设置其thread.ip指向ret_from_fork作为子进程执行的第一条语句,并完成了内核态到用户态的切换。
进程创建由系统调用来建立新进程,归根结底都是调用do_fork来实现。do_fork主要就是调用copy_process。
copy_process()主要完成进程数据结构,各种资源的初始化。初始化方式可以重新分配,也可以共享父进程资源,主要根据clone_flags参数来确定。将task_struct结构体分配给子进程,并为其分配pid,最后将其加入可运行队列中。
dup_task_struct()为子进程获取进程描述符。
copy_thread()函数将父进程内核栈复制到子进程中,同时设置子进程调度后执行的第一条语句地址为do_frok返回,并将保存返回值的寄存器eax值置为0,因此子进程返回为0,而父进程继续执行之后的初始化,最后返回子进程的pid(tgid)。
原文地址:http://blog.51cto.com/mi55u/2046624