进程控制块
进程控制块有几种组织方式以使操作系统能够访问到。进程控制块之间的联系常有索引方式存储和链接方式存储。
linux继承了unix的传统,使用 task_struct 结构作为进程控制块。操作系统的内核空间有一个task数组,其中每个元素指向一个任务结构,又称为task向量。NR_TASKS 是task数组长度,因此决定着系经能够容纳的进程数。
linux操作系统对进程区分普通进程与实时进程,调度程序对此作出区分。
在linux内核栈中保存有 task_struct 结构的关键内容。
linux时间片的安排:进程创建子进程之后时间片的分配原则是:1.不会因创建子进程而受到奖励,也就是创建的子进程和父进程平分剩下的时间片。2.不会因创建短期子进程而受到惩罚,也就是子进程未被重新分配过时间片而结束,则剩下的时间片会归还给父进程。也就是系统认为子进程第一次分配的时间片来自于父进程。
休眠次数多的进程可能是交互式进程,但也可能是IO密集型进程。如果操作系统不能区分这一点,将会导致真正的终端交互程序响应缓慢。因此系统不是简单根椐频繁进入休眠状态来动态调整优先级,还有判断是否交互式应用的算法,task_struct 结构提供了相应的记录,以供调度程序选择其中的一些作为调度参考。
进程相关的函数
进程的基本属性是其PID.它的作用主要是可以给进程一个标识,可以快速地访问到相应的进程当中的信息。进程运行时可通过getpid()函数访问自己的PID
而进程是由其它进程创建的(.)。所以通过getppid()函数访问父进程的PID
根据POSIX的相关规范,进程之间是以组的方式联结起来。这是为了方便地通过信号管理一组用户的进程等目的。进程的组ID从父进程继承,组ID可通过setpgrp()修改,通过getpgrp()或者getpgid()获取。
进程有相关的权限,其权限来自于进程相关的用户。进程所有者是其最基本的权限来源,可以通过getuid()获取。除此之外,进程还有相关的有效用户ID和有效组ID.分别通过geteuid()与getegid()函数获取。操作系统尝试依次从相关的ID获取权限,有一个成功就返回具有权限,最后一个失败才会返回失败。
当然还有组ID,通过getgid()获取。
次序是uid,gid,euid,egid.先用户后组,先自身后额外。
对于线程来说,TGID标识了线程所在进程组的ID.单线程进程等于其PID,多线程时等于基本线程(父进程)的PID.
系统启动时的进程变化
启动过程当中,进程形成树状的结构。内核态下执行0号进程。它创建内核态1号进程。后者负责内核的部分初始化与系统配置,并创建若干个内核线程负责设备,缓存等等。之后内核态的1号进程通过execve()运行可执行程序init,形成用户态的1号进程。它按照/etc/inittab当中的配置完成系统的启动工作。在这一过程中通常创建了若干的getty进程,每个具有不同的GRP-ID,这样就使GRP-ID有了会话期进程的含义。当检测到来自相应终端的信号的时候,getty自动执行login程序登录。由login认证之后就进入到相关的shell.
注意getty在执行shell时通过execve()实现的,所以是getty进程被shell进程取代。当shell退出时,init会检查相应的终端,然后决定是否重启getty.
后面会知道execve()相当于替换掉当前进程的代码段,所以进程号没有改变。
进程的基本管理
从当前进程创建一个子进程,有三个选项创建新进程:
pid_t fork(void);
pid_t vfork(void);
int clone(int (*fn)(void *arg), void *stack, int flags, void *arg);三个函数最终都调用同一个内核函数 do_fork() :
do_fork(unsigned long clone_flag, unsigned long usp, struct pt_regs);
其中 clone_flag 可以是 CLONE_VM,CLONE_FS,CLONE_FILES , CLONE_SIGHAND,CLONE_PID 以及 CLONE_VFORK 等。分别表示子进程与父进程共享(是共享而不是复制)相应的资源:进程空间,文件系统,打开的文件,子进程终结时向父进程发送SIGCHLD,信号处理函数,进程标识符,父进程在子进程空间被释放时唤醒。
如果有 CLONE_VFORK ,父进程一直被挂起,直到子进程空间被释放。
在linux下创建进程成功默认是让子进程先运行。
fork()函数将父进程所有资源通过数据结构复制给子进程。两个进程的地址空间完全不再干涉。需要通过进程间通信机制通信。fork()之后相当于子进程从当前位置开始执行,开始判断。父子进程不再关联。该调用向父进程返回子进程PID,向子进程返回0错误时向父进程返回-1.
其中的错误可能为:EAGAIN,ENOMEM.第一个是进程数达到上限,第二个是内存不足。vfork()的特点是子进程共享父进程的地址空间。运行之后父进程会被阻塞,直到子进程执行exec()或exit().使用vfork时进程堆栈区是不同的。vfork方法当然产生的是新的进程,只不过在创建的时候使用的是父进程的地址空间。但是在执行exec之后,程序就使用了新的地址。其实不妨这样看:vfork程序相当于新创建了一个存在于内核当中的引用,指向父进程的数据,此时子进程对于数据的任何修改都会被父进程看到。但是使用了exec之后,子进程的数据区就指向新的位置,父进程数据区的相应引用计数值就减去1.并且解除掉父进程的阻塞状态。
理解的关键在于数据区像共享文件那样存在共享计数。
vfork的作用有两点,第一是在vfork之后,exec之前,父进程被锁住,子进程对父进程可以安全地修改。其次,如果创建子进程的目的就是exec一个新程序并运行,复制页表完全是多余的,因此vfork后比fork后调用exec更合适。
无论单独的fork还是vfork,执行之后父进程都被阻塞,在子进程exec之后,或者子进程退出后才能使父进程恢复运行的能力。
clone()用于创建线程,先不讲。
创建子进程的重要目的是执行其它的程序,在linux当中通过exec()调用实现。进程调用exec()之后原有的代码段被替换成参数指定的代码段,原有的数据段和堆栈段也被废除了。只有进程号还是相同的。exec()类有若干个函数,功能大致相同。在unistd.h当中有execl,execlp,execle,execv,execve,execvp.后缀带p表示使用PATH变量查找可执行文件,,v是表示通过char*数组传参。(最后一个是NULL)。e是表示使用指定的环境变量。
不知道是不是所有的数据都被替换了呢?那么除了进程控制块之外所有的数据都要分配,因此开销也是不小的。
几个exec函数最终都变成调用 do_exec() 。其功能是加载新程序并跳转执行。
在前面讲了如何使父进程与子进程同时运行。但是有时候父进程还想知道子进程的状态,这时的解决办法是使用wait()或者waitpid()函数。
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);编程方法:当不需要得知子进程退出的原因的时候,wait(NULL)即可,否则自己分配一个整型变量的空间。这个整型变量的不同比特位代表不同的含义,使用系统提供的对应的宏可以检查退出状态。
也许会有这样的疑问,子进程退出不是在系统中就不存在了么,为什么还会存有退出原因呢。这是因为虽然操作系统原理上是这么说,但是子进程PCB的释放是在wait函数中进行的,wait的原理是,等待子进程的结束。一旦子进程处于僵死状态,函数将其彻底销毁后向父进程返回。此时才真正从系统中释放子进程的PID.
wait()的功能是阻塞父进程直到有一个子进程退出,函数返回其PID.进程退出的原因放在status变量当中。没有子进程时返回-1.
waitpid的功能更强大,可以等待指定PID的子进程退出。只不过,取正的时候代表等待指定pid的子进程,-1代表等待任何一个子进程退出,0代表等待同一进程组中的任何子进程,小于-1代表等待进程组为abs(pid)中任何一个子进程。后两个主要是为了方便会话期程序管理其子进程。options可以取WNOHANG,WUNTRACED.其中第一个表示不要阻塞父进程,第二个与作业控制有关。选项有NOHANG,且返回PID为0代表没有发现可供收集的子进程。调用中出错返回-1.
可以理解为,是进程PID的原因返回-1,是wait中未能回收子进程返回0,其余情况返回内容函数回收的那个子进程的PID.
waitpid仍然只能等待其子进程pid.
相对wait,waitpid的优点是可以等待特定子进程退出,可以非阻塞运行,可以支持作业控制。
进程退出的方法。使进程自动退出的函数是exit(), _exit() 。
#include <stdlib.h>
void exit(int status);
#include <unistd.h>
void _exit(int status);两个函数的关系相当于一个是C库函数,一个是系统函数。由于C语言和unix系列的密切关系,不能不讲C库函数的同时讲C库函数所使用的更底层函数。
两个函数的status用于指示退出状态。最终两个函数都是调用系统的 do_exit() 函数。在退出的进程中, do_exit() 还会使用各种退出函数,与文件系统有关的,与信号量有关的,与文件描述符有关的,与切换进程有关的,等等。
在linux的标准函数库当中,有一套高级IO使用了主存当中的缓冲区,C语言当中的printf(),fopen(),fread(),fwrite()都是其中的函数。此时使用 _exit() 会导致缓冲区当中的数据丢失,因此需要用exit()函数,它能够对缓冲区里的内容同步。
printf("output begin\n");
printf("content in buffer");
_exit(0);上面的代码运行时,第二行的printf语句结果并没有显示出来。
进程的有关编程
在linux下自己管理创建的进程,一般使用的函数是fork,vfork,exec,wait,waitpid, _exit() 这一类函数。但是在C语言的环境当中,还可以使用更加高级的函数,正如在文件读写当中使用带buffer的读写函数一样。这一类函数比如说有system(),exit().其中前者的功能是将命令传给shell执行,由shell返回
畅想计算机学习
在进程当中,有关操作系统原理的那部分知识显然是给系统管理员用的。系统管理直接用这些知识来调整进程的优先级,调试程序等等,但是对于应用程序开发者来说,却是没有什么意义,其中的机制才是它们最关心的,比如创建进程用什么函数。所以实际上学习操作系统就分成了两个方面,第一个是关于操作系统的各种算法的,这对于要了解操作系统原理从而要管理整个系统的人来说非常有价值。
但是对编程来讲却没有价值。在进程当中,程序员只需要分清楚以下的问题: 1.进程中的这些区块有没有被复制|; 2.有没有哪个进程被阻塞; 3.数据区是怎样被替换,又是怎样被销毁的; 4.进程从哪个地方开始执行。
所以对于程序员来说,vfork仅是相当于一个性能优化的函数,和它们头脑中布署的逻辑层次并没有关系。
最高的层次就是把代码和逻辑完全结合起来,暂且称之为是“运算原理”这样的一个部分吧。它的作用是从计算系统的观点来思考问题,编译原理只是其中的一个入门课。
进程的管理
进程的基本管理
使用ps命令查看相应的进程是在使用操作系统命令的层次上
nice值对于进程的响应速度有重要的影响,前面已经知道了nice值的作用,控制着进程的优先级。值越大,优先程度越低。
nice和renice命令分别用于调整进程的优先级。
相应地也有nice的系统调用用于调整进程优先级,实际上unix系统下的函数调用和命令调用对应关系是很紧密的。
linux的调度算法中进程的时间片从5ms到800ms不等。