实验四 进程管理(二)
1、阅读下列程序,编译并运行,分析进程执行过程的时间消耗(总共消耗的时间和CPU消耗的时间),并解释执行的结果。在编写一个计算密集型程序替代grep,比较两次时间的花销
程序源码如下:
#include<stdio.h>
#include<stdlib.h>
#include<sys/times.h>
#include<time.h>
#include<unistd.h>
void time_print(char *,clock_t);
int main(void)
{
clock_t start,end;
struct tms t_start,t_end;
start = times(&t_start);
system(“grep the /usr/doc/×/× > /dev/null 2> /dev/null”);
end=times(&t_end);
time_print(“elapsed”,end-start);
puts(“parent times”);
time_print(“\tuser CPU”,t_end.tms_utime);
time_print(“\tsys CPU”,t_end.tms_stime);
puts(“child times”);
time_print(“\tuser CPU”,t_end.tms_cutime);
time_print(“\tsys CPU”,t_end.tms_cstime);
exit(EXIT_SUCCESS);
}
void time_print(char *str, clock_t time)
{
long tps = sysconf(_SC_CLK_TCK);
printf(“%s: %6.2f secs\n”,str,(float)time/tps);
}
代码的部分解释
程序的运行结果如下:
对于上述程序的系统解释:
-
当程序调用system函数时, 它先产生一个子进程, 然后是子进程而不是父进程完成所有工作并消耗了CPU时间
-
进程的执行时间0.01并不等于用户CPU时间和系统CPU时间之和 0.00秒。 原因是子进程执行的grep操作是I/O密集型而非CPU密集型的操作。 它扫描了这里讨论所使用的系统上的多个文件, 缺少的0.01秒全部用从硬盘读取数据。
-
times返回的时间是相对而非绝对时间(系统自举后经过的时钟滴答数), 所以要让它有实用价值, 就必须做两次测量并使用它们的差值。 这就引入了 流逝时间, 或者称为墙上时钟时间。 resusg1通过把起止的时钟滴答数分别保存在start和end中来做到这一点。 另一种进程计时值可从<sys/times.h>中定义的tms结构中获得。 tms结构保存着一个进程及其子进程的当前CPU时间。
使用密集型程序代替grep指令,程序代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <sys/times.h>
#include <time.h>
#include <unistd.h>
void time_print(char *,clock_t);
int main()
{
clock_t start,end; //clock_t是一个长整形
struct tms t_start,t_end; //struct tms结构体用于定义进程的各种运行的时间
start = times(&t_start); //times()函数的作用是 取得进程运行相关的时间 将其存放在start
//下述是程序密集型计算
int row = 100,column = 200,h = 200; //通过对三维数组的计算 使cpu消耗较多的时间
for(int i=0;i<row;i++)
{
for(int j=0;j<column;++j)
for(int k=0;k<h;++k)
printf("%d ",i+j+k);
printf("\n");
}
end = times(&t_end); //执行上述的grep操作后返回结束的时间
time_print("elapsed",end-start); //两次的时间差 也成为流逝时间
puts("parents times");
time_print("\tuser CPU",t_end.tms_utime);
time_print("\tsys CPU",t_end.tms_stime);
puts("child times");
time_print("\tuser CPU",t_end.tms_cutime);
time_print("\tsys CPU",t_end.tms_cstime);
exit(EXIT_SUCCESS);
}
void time_print(char *str,clock_t time)
{
long tps = sysconf(_SC_CLK_TCK); //_SC_CLK_TCK是定义每秒钟有多少滴答的宏 而sysconf是将时钟滴答数转化为秒数
printf("%s: %6.2f secs\n",str,(float)time/tps);
}
- 程序的执行结果如下
上述程序运行的时间主要是父进程的运行时间,原因:由于在此程序中,并没有调用system()函数 所以没有产生子进程。而三维数组的输出实际是在父进程中运行的,所以子进程的时间没有,而父进程消耗了一部分时间
2、阅读下面程序,编译并运行,等待或者按^c,分别观察执行结果并分析,注释主要的语句。讲述flag的作用。
程序代码如下:
#include<unistd.h>
#include<stdio.h>
#include<sys/types.h>
#include<signal.h>
#include<stdlib.h>
int flag;
void stop();
int main(void)
{
int pid1,pid2;
signal(3,stop);
while((pid1=fork()) ==-1);
if(pid1>0){
while((pid2=fork()) ==-1);
if(pid2>0){
flag=1;
sleep(5);
kill(pid1,16);
kill(pid2,17);
wait(0);
wait(0);
printf("\n parent is killed\n");
exit(EXIT_SUCCESS);
}else{
flag=1;
signal(17,stop);
printf("\n child2 is killed by parent\n");
exit(EXIT_SUCCESS);
}
}else{
flag=1;
signal(16,stop);
printf("\n child1 is killed by parent\n");
exit(EXIT_SUCCESS);
}
}
void stop(){
flag = 0;
}
- 首先对上述程序中出现的函数进行说明
signal函数 : 为指定的信号安装一个新的信号处理函数。signal函数的原型为:void (*signal(int signo,void(*func)(int))) 也就是说,signo是信号名,而func可以是常量SIG_IGN或SIG_DFL或者是要调用的函数地址。当指定函数地址时,则在信号发生时,调用该函数,称为"捕捉"该信号。
kill函数 : 函数原型为int kill(pid_t pid,int sig),pid大于零时,表示pid是信号欲送往的进程的标识 sig为准备发送的信号代码
wait函数 : 父进程一旦调用了wait就会立刻阻塞自己,有wait进行分析是否当前的某一个子进程已经退出,如果找到了一个已经变为僵尸的子进程,wait会收集这个子进程的信息并将它销毁后返回。
下述为代码说明
下面为程序执行的示意图:
flag的作用:对于每一个进程而言都有一个flag,这个flag起到状态标志的作用,当flag为1时 表明进程正在运行,当flag为0时,表明进程结束
3、编写程序,要求父进程创建一个子进程,使父进程和各个子进程各自在屏幕上输出一些信息,但是父进程的信息总是在子进程的信息之后出现
代码以及注释如下:
执行情况如下:
4、编写程序,要求父进程创建一个子进程,子进程执行shell命令find / -name hda* 的功能,子进程结束时由父进程打印子进程结束的信息。执行中父进程改变子进程的优先级。
程序代码如下:
执行结果如下:
5、查阅Linux系统中struct task_struct 的定义,说明每项成员的作用。
注:search in /usr/src/linux-2.6/include/linux/sched.h
task_struct的概要:
对于进程来说,所有的进程信息都会被存放在一个称作进程控制块的数据结构中。对于windows来说,每个进程在内核中都有一个进程控制块(PCB)来维护进程的信息,linux内核的进程控制块是task_struct结构体。
task_struct是linux内核额一种数据结构,它会被装载到RAM里并且包含着进程的信息,task_struct这个数据结构中包含了下列的内容:
标识符: 描述本进程的唯一标示符,用来区别其他进程。
状态: 任务状态,退出代码,退出信号
优先级: 相对于其他进程的优先级。
程序计数器: 程序中即将被执行的下一条指令的地址。
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
上下文数据: 进程执行时处理器的寄存器中的数据。
I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
task_struct中各个成员的介绍:
进程状态/调度数据成员(State)
这个信息中可细分为6中状态,分别为:
TASK_RUNNING:
处在这个状态的进程,不是在运行就是准备运行,只是等待运行本进程的资源到位。即准备运行的进程只要得到CPU就可以立即投入运行。进程中有一个运行队列run_queue,容纳所有可运行的进程,调度进程时,从中选择一个进程进行执行。当前运行进程一直处于该队列中。
TASK_INTERRUPTIBLE:
处于等待队列中的进程,等待到资源分配到时觉醒,也可由其它进程通过信号(signal)或定时中断唤醒,唤醒后进入运行队列 run-queue,等待被调度。
TASK_ZOMBIE:
顾名思义,僵尸进程。就如电影里面的僵尸一样,人虽已死(指灵魂),但肉体还活着,处于行尸走肉的状态。僵尸进程表示已释放掉所用的资源(即灵魂已逝),但没有释放本身的PCB(task_struct)。
TASK_SWAPPING: 进程页面被交换出内存的进程。
unsigned long flags(进程标志):
PF_ALIGNWARN: 打印“对齐”警告信息。
PF_PTRACED: 被ptrace系统调用监控。
PF_TRACESYS: 正在跟踪。
PF_FORKNOEXEC: 进程刚创建,但还没执行。
PF_SUPERPRIV: 超级用户特权。
PF_DUMPCORE: dumped core。
PF_SIGNALED: 进程被信号(signal)杀出。
PF_STARTING: 进程正被创建。
PF_EXITING: 进程开始关闭。
PF_USEDFPU: 该进程使用FPU(SMP only)。
PF_DTRACE: delayed trace (used on m68k)。
进程调度信息:
表示当前进程或一个进程允许运行的时间,待到该进程的时间片运行结束,CPU会从运行队列上拿出另一个进程运行。
need_resched: 调度标志
Nice: 静态优先级
Counter: 动态优先级
Policy: 调度策略开始运行时被赋予的值
rt_priority: 实时优先级
标识符(Identifiers)
PID(process identifier):
32位无符号整型数据。但最大值取32767。表示每一个进程的标识符。也是内核提供给用户程序的借口,用户程序通过pid操作程序。
因为Unix的原因引入还引入了线程组的概念。称为:tgid。一个线程组中的所有线程使用和该线程组中的第一个轻量级线程的pid,被存在tgid成员中。当进程没有线程时,tgid=pid;当有多线程时,tgid表示的是主线程的id,而pid表示每一个线程自己的id。
进程通信有关信息(IPC:Inter_Process Communication)
unsigned long signal: 进程接收到的信号。每位表示一种信号,共32种。置位有效。
unsigned long blocked: 进程所能接受信号的位掩码。置位表示屏蔽,复位表示不屏蔽。
Spinlock_t sigmask_lock: 信号掩码的自旋锁
Long blocked: 信号掩码
Struct sem_undo semundo: 为避免死锁而在信号量上设置的取消操作
Struct sem_queue semsleeping: 与信号量操作相关的等待队列
struct signal_struct sig: 信号处理函数
进程信息
Linux中存在多进程,而多进程中进程之间的关系可能是父子关系,兄弟关系。
除了祖先进程外,其他进程都有一个父进程,通过folk创建出子进程来执行程序。除了表示各自的pid外,子进程的绝大多数信息都是拷贝父进程的信息。且父进程对子进程手握生杀大权,即子进程时是父进程创建出来的,而父进程也可以发送命令杀死子进程。
时间信息
Start_time: 进程创建时间
Per_cpu_utime: 进程在执行时在用户态上耗费的时间。
Pre_cpu_stime: 进程在执行时在系统态上耗费的时间。
ITIMER_REAL: 实时定时器,不论进程是否运行,都在实时更新。
ITIMER_VIRTUAL: 虚拟定时器,只有进程运行在用户态时才会更新。
ITIMER_PROF: 概况定时器,进程在运行处于用户态和系统态时更新。
文件信息:
文件的打开和关闭都是资源的一种操作,Linux中的task_struct中有两个结构体储存这两个信息。
*Sruct fs_struct fs: 进程的可执行映象所在的文件系统,有两个索引点,称为root和pwd,分别指向对应的根目录和当前目录。
*Struct files_struct files: 进程打开的文件
地址空间/虚拟内存信息
每个进程都有自己的一块虚拟内存空间,用mm_struct来表示,mm_struct中使用两个指针表示一段虚拟地址空间,然后在最终时通过页表映射到真正的物理内存上。
页面管理信息
Int swappable: 进程占用的内存页面是否可换出。
Unsigned long min_flat,maj_flt,nswap: 进程累计换出、换入页面数。
Unsigned long cmin_flat,cmaj_flt,cnswap: 本进程作为祖先进程,其所有层次子进程的累计换出、换入页面数。
对称对处理机信息
Int has_cpu: 进程是否当前拥有CPU
Int processor: 进程当前正在使用的CPU
Int lock_depth: 上下文切换时内核锁的深度
上下文信息:
struct desc_struct ldt: 进程关于CPU段式存储管理的局部描述符表的指针。
struct thread_struct tss: 任务状态段。与Intel的TSS进行互动,当前运行的TSS保存在PCB的tss中,新选中的的进程的tss保存在TSS。
信号量数据成员
struct sem_undo semundo: 进程每一次操作一次信号量,都会生成一个undo操作。保存在sem_undo结构体中,最终在进程异常终止结束的时候,sem_undo的成员semadj就会指向一个数组,这个数组中每个成员都表示之前每次undo的量。
truct sem_queue semsleeping: 进程在操作信号量造成堵塞时,进程会被送入semsleeping指示的关于该信号量的sem_queue队列。
进程队列指针
struct task_struct ×next_task,×prev_task: 所有进程均有各自的PCB。且各个PCB会串在一起,形成一个双向链表。其next_task和prev_task就表示上一个或下一个PCB,即前后指针。进程链表的头和尾都是0号进程。
struct task_struct *next_run,*prev_run: 由进程的run_queue中产生作用的,指向上一个或下一个可运行的进程,链表的头和尾都是0号进程。
struct task_struct *p_opptr: 原始父进程(祖先进程)
struct task_struct *p_pptr : 父进程
struct task_struct *p_cptr: 子进程
struct task_struct *p_ysptr: 弟进程
struct task_struct *p_osptr: 兄进程
以上分别是指向原始父进程(original parent)、父进程(parent)、子进程(youngest child)及新老兄弟进程(younger sibling,older sibling)的指针。
current: 当前正在运行进程的指针。
struct task_struct init_task: 0号进程的PCB,进程的跟=根,始终是INIT_TASK。
char comm[16]: 进程正在执行的可执行文件的文件名。
int errno: 进程最后一次出错的错误号。0表示无错误