序言:开始进行Linux方面的学习,搞清楚编程底层时执行的一些原理知识点,从此开始总结学习过程中的知识点。
进程的一些基本概念
概述:进程是程序执行时的一个实例,也可以看作:充分描述程序已经执行到何种程度的数据结构的汇集。每一个进程都有一个父进程,一个进程可以产生一个或者多个子进程,但最终都要死亡。 进程的目的:从内核的角度来看,进程就是担当分配系统资源(CPU时间、内存等)的实体。
进程描述符(PCB):进程描述符都是task_struct类型结构,他的字段包含了与一个进程相关的所有信息(进程的优先级、进程当前的状态(正在运行还是处于阻塞状态)、分配了什么地址空间…),进程描述符有这么多的信息需要进行存储,所以其内部组成比较复杂,其中有:thread_info(进程的基本信息)、mm_struct(指向内存区描述符的指针)、tty_struct(与进程相关的tty)、fs_struct(当前目录)、files_struct(指向文件描述符的指针)、signal_struct(所接收的信号)…
进程状态(进程描述符中state字段):进程同时只能够处于以下一种状态:可运行状态、可中断的等待状态、不可中断的等待状态、暂停状态、跟踪状态、僵死状态(这就是僵尸进程:进程的执行被终止,但是父进程还没有发布wait4()或者waitpid()系统调用来返回有关死亡进程的信息,发布wait()类系统调用之前,内核不能丢弃包含在死进程描述符中的数据,因为父进程可能那个还需要它)、僵死撤销状态。
PID(process ID):进程标识符:进程标识符只是进程描述符中pid字段中的内容。Linux中引入了线程组的概念,一个线程组中的所有线程使用和该线程组的领头线程相同的PID,也就是该组中抵押给轻量级进程的PID,它被存入进程描述符的tgid字段中;getpif()系统调用返回当前进程的tgid值而不是pid的值,所以一个多线程应用的所有线程共享相同的PID。
运行队列:Linux使用一个队列链表将处于TASK_RUNNING状态的所有进程组织在一起。对于暂停、僵死、死亡状态的进程访问比较简单,或者通过PID或者通过特定父进程的子进程链表,所以不必对这三种状态进程进行分组。
等待队列:进程必须经常等待某些事件的发生,例如:等待一个磁盘操作的终止、等待释放系统资源、或者等待时间经过固定的间隔。等待队列表示一组睡眠的进程,当某一条件变为真时,由内核唤醒它们。等待链表由双向链表实现,包括指向进程描述符的指针。等待队列中的进程分为两种:互斥进程、非互斥进程,两种睡眠的进程由内核来进行判断唤醒。
进程资源限制:每个进程都有一组相关的资源限制,限制指定了进程能使用的系统资源的数量。这些避免了进程过度使用进程的资源(cpu、内存、磁盘空间等)。对当前资源限制存放在current->signal->rlim字段,即进程的信号描述符的一个字段中。
进程切换:进程切换指的是:有能力将正在运行的进程挂起,并恢复以前挂起的某个进程的执行。也可被称为上下文切换或者任务切换。进程之间的切换只发生在内核态。在执行进程切换之前,用户态进程使用的所有寄存器内容都已保存在内核态堆栈中,这其中包括ss和esp这对寄存器的内容(用来存储用户态堆栈指针的地址)。
硬件上下文:进程恢复执行前必须装入寄存器的一组数据称为硬件上下文。硬件上下文是进程可执行上下文的一个子集。在Linux中,进程硬件上下文的一部分存放在TSS段,而剩余部分放在内核堆栈中。
进程切换: 1. 切换页全局目录以安装一个新的地址空间。 2. 切换内核态堆栈和硬件上下文,因为硬件上下文提供了内核执行新进程所需要的所有信息,包含CPU寄存器。
保存和加载FPU、MMX及XMM寄存器:
- FPU:算术浮点单元,从Intel80486DX开始,FPU已被集成到CPU中。如果一个进程正在使用ESCAPE指令,那么浮点寄存器的内容就属于它的硬件上下文,并且应该被保存。
- Intel在它的微处理器中引入一个新的汇编指令及,叫作MMX指令,用来加速多媒体应用程序的执行。MMX指令作用域FPU的浮点寄存器。MMX能够加速多媒体应用程序的执行,因为他们在处理器内部引入了单指令多数据(SIMD)流水线。
- Pentium III模型扩展了这种SIMD能力:引入SSE扩展,该扩展为处理包含在8个128位寄存器(叫作XMM寄存器)的浮点值增加了功能。
内核要想使用FPU、MMX和SSE/SSE2单元,典型的场合有:当移动或者清除大内存区字段时,或者当计算校验和函数时。
创建进程: Unix操作系统紧紧依赖进程创建来满足用户的需求,例如:只要用户输入一条命令,shell进程就创建一个新进程,新进程执行shell的另一个拷贝。传统的Unix操作系统以统一的方式对待所有的进程:子进程复制父进程所拥有的资源,这种方法使进程的创建非常慢且效率低,因为子进程需要拷贝父进程整个地址空间。现代Unix内核通过引入三种不同的机制解决了这个问题:
- 写时复制
- 轻量级进程允许父子进程共享每进程在内核的很多数据结构,如页表(也就是整个用户态地址空间)、打开文件表及信号处理。
- vfork()系统调用创建的进程能共享其父进程的内存地址空间。为了防止父进程重写子进程需要的数据,阻塞父进程的执行,一直到子进程退出或执行一个新的程序为止。
clone()、fork()及vfork()系统调用: 在Linux中,轻量级进程是由名为clone()的函数创建的。clone()是C语言中定义的要给封装函数,它负责建立新轻量级进程的堆栈并且调用对编程者隐藏的clone()系统调用。 传统的fork()系统调用在Linux中是以clone()实现的,其中clone()的flags参数指定为SIGCHLD信号及所有清0的clone标志,而它的child_stack参数是父进程当前的堆栈指针。因此父进程和子进程暂时共享同一个用户态堆栈,但是,要感谢写时复制机制,通常只要父子进程中有一个试图去改变栈,则立即各自得到用户态堆栈的一份拷贝。 同样的vfork()系统调用在Linux中也是用clone()实现的,其中clone()的参数flags指定为SIGCHLD信号和CLONE_VM及CLONE_VFORK标志,clone()的参数child_stack等于父进程当前的栈指针。 注:还有一些其它的函数do_fork()、copy_process()…
内核线程: 内核线程用来处理一些重要的任务,这些重要的任务包括哦刷新磁盘高速缓存,交换出不用的页框,维护网络连接等等;内核线程不受不必要的用户态上下文的拖累,在Linux中,内核线程在以下几个方面不同于普通进程:
-
内核线程只运行在内核态,而普通进程既可以运行在内核态,也可以运行在用户态。 -
因为内核线程只运行在内核态,它们只使用大于PAGE_OFFSET的线性地址空间。另一方面,不管在用户态还是在内核态,普通进程可以用4GB。 创建一个内核线程: kernel_thread()函数创建一个新的内核线程,它接受的参数有:所有执行的内核函数的地址(fn)、要传递给函数的参数(arg)、一组clone标志(flags)。该函数本质上以下面的方式调用do_fork(): do_fork(flags | CLONE_VM | CLONE_UNTRACED, 0, pregs, 0, NULL, NULL); 进程0: 所有进程的祖先叫作进程0,idle进程或因为历史的原因叫作swapper进程,它是在Linux的初始化阶段从无到有创建的一个内核线程。start_kernel()函数初始化内核需要的所有数据结构,激活中断,创建另一个叫作进程1的内核进程(一般叫作init进程):kernel_thread(init, NULL, CLONE_FS | CLONE_SIGHAND); 进程1: 由进程0创建的内核线程执行init()函数,init()依次完成内核初始化,init()调用execve()系统调用装入可执行程序init。结果init内核线程变成一个普通进程,在系统关闭之前,init进程一直存活,因为它创建和监控在操作系统外层执行的所有进程的活动。
撤销进程: 进程终止的一般方式是调用exit()库函数,该函数释放C函数库所分配的资源,执行编程者所注册的每个函数,并结束从系统回收进程的哪个系统调用。
进程终止: Linux2.6中两个终止用户态应用的系统调用:exit_group():终止整个线程组,即整个基于多线程的应用,主要实现这个系统调用的内核函数是do_group_exit()。exit():终止某一个线程,而不管该线程所属线程组中的所有其它进程,主要实现这个系统调用的内核函数是do_exit()。
进程删除: 不允许Unix内核在进程一终止后就丢弃包含在进程描述符字段中的数据,只有父进程发出了与被终止的进程相关的wait()类系统调用之后,下允许这样做,这就是引入僵死状态的原因:尽管从技术上来说进程已死,但必须保存它的描述符,直到父进程得到通知。
|