进程

程序是指储存在外部储存(如硬碟)的一个可执行文件,而进程是指处于执行期间的程序,进程包括代码段(textsection)和数据段(datasection),不仅代码段和数据段外,进程通常还包含打开的文件,要处理的讯号和CPU上下文等等.

进程描述符

Linux进程使用structtask_struct来描述(include/linux/sched.h),如下:

struct task_struct {
 /*
 * offsets of these are hardcoded elsewhere - touch with care
 */
 volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
 unsigned long flags; /* per process flags, defined below */
 int sigpending;
 mm_segment_t addr_limit; /* thread address space:
 0-0xBFFFFFFF for user-thead
 0-0xFFFFFFFF for kernel-thread
 */
 struct exec_domain *exec_domain;
 volatile long need_resched;
 unsigned long ptrace;
 int lock_depth; /* Lock depth */
/*
 * offset 32 begins here on 32-bit platforms. We keep
 * all fields in a single cacheline that are needed for
 * the goodness() loop in schedule().
 */
 long counter;
 long nice;
 unsigned long policy;
 struct mm_struct *mm;
 int processor;
 ...
}

Linux把所有的进程使用单向数组联接上去,如右图(来源):

linuxfork()_linux fork_linuxfork创建进程

linux fork_linuxfork()_linuxfork创建进程

Linux内核为了推动获取当前进程的的task_struct结构,使用了一个方法,就是把task_struct放置在内核栈的栈底,这样就可以通过esp寄存器快速获取到当前运行进程的task_struct结构.如右图:

linuxfork()_linux fork_linuxfork创建进程

取当前运行进程的task_struct代码如下:

static inline struct task_struct * get_current(void)
{
 struct task_struct *current;
 __asm__("andl %%esp,%0; ":"=r" (current) : "0" (~8191UL));
 return current;
}

进程状态

进程描述符的state数组用于保存进程的当前状态,进程的状态有以下几种:

个钟状态间的转换如右图:

linuxfork()_linuxfork创建进程_linux fork

进程的创建

linuxfork()_linux fork_linuxfork创建进程

Linux系统中,进程的创建使用fork()系统调用,fork()调用会创建一个与父进程一样的子进程,惟一不同就是fork()的返回值,父进程返回的是子进程的进程ID,而子进程返回的是0。

Linux创建子进程时使用了写时复制(CopyOnWrite),也就是创建子进程时使用的是父进程的显存空间红旗linux6.0,当子进程或则父进程更改数据时才能复制相应的显存页。

当调用fork()系统调用时会深陷内核空间而且调用sys_fork()函数,sys_fork()函数会调用do_fork()函数linux forklinux forklinux内存管理,代码如下(arch/i386/kernel/process.c):

asmlinkage int sys_fork(struct pt_regs regs)
{
 return do_fork(SIGCHLD, regs.esp, ®s, 0);
}

do_fork()主要的工作是申请一个进程描述符,之后初始化进程描述符的各个数组,包括调用copy_files()函数复制打开的文件,调用copy_sighand()函数复制讯号处理函数,调用copy_mm()函数复制进程虚拟显存空间,调用copy_namespace()函数复制命名空间.代码如下:

linuxfork创建进程_linuxfork()_linux fork

int do_fork(unsigned long clone_flags, unsigned long stack_start,
 struct pt_regs *regs, unsigned long stack_size)
{
 ...
 p = alloc_task_struct(); // 申请进程描述符
 ...
 if (copy_files(clone_flags, p))
 goto bad_fork_cleanup;
 if (copy_fs(clone_flags, p))
 goto bad_fork_cleanup_files;
 if (copy_sighand(clone_flags, p))
 goto bad_fork_cleanup_fs;
 if (copy_mm(clone_flags, p))
 goto bad_fork_cleanup_sighand;
 if (copy_namespace(clone_flags, p))
 goto bad_fork_cleanup_mm;
 retval = copy_thread(0, clone_flags, stack_start, stack_size, p, regs);
 ...
 wake_up_process(p);
 ...
}

值得注意的是do_fork()还调用了copy_thread()这个函数,copy_thread()这个函数主要用于设置进程的CPU执行上下文structthread_struct结构.代码如下:

int copy_thread(int nr, unsigned long clone_flags, unsigned long esp,
 unsigned long unused,
 struct task_struct * p, struct pt_regs * regs)
{
 struct pt_regs * childregs;
 // 指向栈顶(见图2)
 childregs = ((struct pt_regs *) (THREAD_SIZE + (unsigned long) p)) - 1;
 struct_cpy(childregs, regs); // 复制父进程的栈信息
 childregs->eax = 0; // 这个是子进程调用fork()之后的返回值, 也就是0
 childregs->esp = esp; // 设置新的栈空间
 p->thread.esp = (unsigned long) childregs; // 子进程当前的栈地址, 调用switch_to()的时候esp设置为这个地址
 p->thread.esp0 = (unsigned long) (childregs+1); // 子进程内核空间栈地址
 p->thread.eip = (unsigned long) ret_from_fork; // 子进程将要执行的代码地址
 savesegment(fs,p->thread.fs);
 savesegment(gs,p->thread.gs);
 unlazy_fpu(current);
 struct_cpy(&p->thread.i387, ¤t->thread.i387);
 return 0;
}

do_fork()函数最后调用wake_up_process()函数唤起子进程,让子进程步入运行状态.

内核线程

linux fork_linuxfork创建进程_linuxfork()

Linux内核有好多任务须要去做,比如定时把缓冲中的数据刷到硬碟,当显存不足的时侯进行显存的回收等,这种工作都须要通过内核线程来完成.内核线程与普通进程的主要区别就是:内核线程没有自己的虚拟空间结构(structmm),每次内核线程执行的时侯都是利用当前运行进程的虚拟显存空间结构来运行,由于内核线程只会运行在内核态,而每位进程的内核态空间都是一样的,所以利用其他进程的虚拟显存空间结构来运行是完成可行的.

内核线程使用kernel_thread()函数来创建,代码如下:

int kernel_thread(int (*fn)(void *), void * arg, unsigned long flags)
{
 long retval, d0;
 __asm__ __volatile__(
 "movl %%esp,%%esint"
 "int $0x80nt" /* Linux/i386 system call */
 "cmpl %%esp,%%esint" /* child or parent? */
 "je 1fnt" /* parent - jump */
 /* Load the argument into eax, and push it. That way, it does
 * not matter whether the called function is compiled with
 * -mregparm or not. */
 "movl %4,%%eaxnt"
 "pushl %%eaxnt" 
 "call *%5nt" /* call fn */
 "movl %3,%0nt" /* exit */
 "int $0x80n"
 "1:t"
 :"=&a" (retval), "=&S" (d0)
 :"0" (__NR_clone), "i" (__NR_exit),
 "r" (arg), "r" (fn),
 "b" (flags | CLONE_VM)
 : "memory");
 return retval;
}

由于这个函数式使用嵌入汇编来实现的,所以有点难懂,不过主要过程就是通过调用_clone()函数来创建一个新的进程,而创建进程是通过传入CLONE_VM标志来指定进程借用其他进程的虚拟显存空间结构

本文原创地址://gulass.cn/lsrjxljccjcc.html编辑:刘遄,审核员:暂无