操作系统实验四 进程运行轨迹的跟踪与统计(哈工大李治军)(二)

简介: 操作系统实验四 进程运行轨迹的跟踪与统计(哈工大李治军)(二)

编写fprintk()函数


log 文件将被用来记录进程的状态转移轨迹。所有的状态转移都是在内核进行的。


在内核状态下,write() 功能失效,其原理等同于《系统调用》实验中不能在内核状态调用 printf(),只能调用 printk()。编写可在内核调用的 write() 的难度较大,所以这里直接给出源码。它主要参考了 printk() 和 sys_write() 而写成的:

#include "linux/sched.h"
#include "sys/stat.h"
static char logbuf[1024];
int fprintk(int fd, const char *fmt, ...)
{
    va_list args;
    int count;
    struct file * file;
    struct m_inode * inode;
    va_start(args, fmt);
    count=vsprintf(logbuf, fmt, args);
    va_end(args);
/* 如果输出到stdout或stderr,直接调用sys_write即可 */
    if (fd < 3)
    {
        __asm__("push %%fs\n\t"
            "push %%ds\n\t"
            "pop %%fs\n\t"
            "pushl %0\n\t"
        /* 注意对于Windows环境来说,是_logbuf,下同 */
            "pushl $logbuf\n\t"
            "pushl %1\n\t"
        /* 注意对于Windows环境来说,是_sys_write,下同 */
            "call sys_write\n\t"
            "addl $8,%%esp\n\t"
            "popl %0\n\t"
            "pop %%fs"
            ::"r" (count),"r" (fd):"ax","cx","dx");
    }
    else
/* 假定>=3的描述符都与文件关联。事实上,还存在很多其它情况,这里并没有考虑。*/
    {
    /* 从进程0的文件描述符表中得到文件句柄 */
        if (!(file=task[0]->filp[fd]))
            return 0;
        inode=file->f_inode;
        __asm__("push %%fs\n\t"
            "push %%ds\n\t"
            "pop %%fs\n\t"
            "pushl %0\n\t"
            "pushl $logbuf\n\t"
            "pushl %1\n\t"
            "pushl %2\n\t"
            "call file_write\n\t"
            "addl $12,%%esp\n\t"
            "popl %0\n\t"
            "pop %%fs"
            ::"r" (count),"r" (file),"r" (inode):"ax","cx","dx");
    }
    return count;
}


因为和 printk 的功能近似,建议将此函数放入到 kernel/printk.c 中。fprintk() 的使用方式类同与 C 标准库函数 fprintf(),唯一的区别是第一个参数是文件描述符,而不是文件指针。


例如:

// 向stdout打印正在运行的进程的ID
fprintk(1, "The ID of running process is %ld", current->pid);
// 向log文件输出跟踪进程运行轨迹
fprintk(3, "%ld\t%c\t%ld\n", current->pid, 'R', jiffies);


jiffies,滴答


jiffies 在 kernel/sched.c 文件中定义为一个全局变量:

long volatile jiffies=0;

它记录了从开机到当前时间的时钟中断发生次数。在 kernel/sched.c 文件中的 sched_init() 函数中,时钟中断处理函数被设置为:

set_intr_gate(0x20,&timer_interrupt);

而在 kernel/system_call.s 文件中将 timer_interrupt 定义为:

timer_interrupt:
!    ……
! 增加jiffies计数值
    incl jiffies
!    ……


这说明 jiffies 表示从开机时到现在发生的时钟中断次数,这个数也被称为 “滴答数”。


另外,在 kernel/sched.c 中的 sched_init() 中有下面的代码:

// 设置8253模式
outb_p(0x36, 0x43);
outb_p(LATCH&0xff, 0x40);
outb_p(LATCH>>8, 0x40);


这三条语句用来设置每次时钟中断的间隔,即为 LATCH,而 LATCH 是定义在文件 kernel/sched.c 中的一个宏:

// 在 kernel/sched.c 中
#define LATCH  (1193180/HZ)
// 在 include/linux/sched.h 中
#define HZ 100


再加上 PC 机 8253 定时芯片的输入时钟频率为 1.193180MHz,即 1193180/每秒,LATCH=1193180/100,时钟每跳 11931.8 下产生一次时钟中断,即每 1/100 秒(10ms)产生一次时钟中断,所以 jiffies 实际上记录了从开机以来共经过了多少个 10ms。


注意这里是 H Z = 100 HZ=100HZ=100 的情况,前文也介绍过。所以时间其实就是近似等于中断次数乘以 1 / H Z 1/HZ1/HZ


寻找状态切换点


必须找到所有发生进程状态切换的代码点,并在这些点添加适当的代码,来输出进程状态变化的情况到 log 文件中。


此处要面对的情况比较复杂,需要对 kernel 下的 fork.c、sched.c 有通盘的了解,而 exit.c 也会涉及到。


例子 1:记录一个进程生命期的开始

第一个例子是看看如何记录一个进程生命期的开始,当然这个事件就是进程的创建函数 fork(),由《系统调用》实验可知,fork() 功能在内核中实现为 sys_fork(),该“函数”在文件 kernel/system_call.s 中实现为:

sys_fork:
    call find_empty_process
!    ……
! 传递一些参数
    push %gs
    pushl %esi
    pushl %edi
    pushl %ebp
    pushl %eax
! 调用 copy_process 实现进程创建
    call copy_process
    addl $20,%esp


所以真正实现进程创建的函数是 copy_process(),它在 kernel/fork.c 中定义为:

int copy_process(int nr,……)
{
    struct task_struct *p;
//    ……
// 获得一个 task_struct 结构体空间
    p = (struct task_struct *) get_free_page();
//    ……
    p->pid = last_pid;
//    ……
// 设置 start_time 为 jiffies
    p->start_time = jiffies;
//       ……
/* 设置进程状态为就绪。所有就绪进程的状态都是
   TASK_RUNNING(0),被全局变量 current 指向的
   是正在运行的进程。*/
    p->state = TASK_RUNNING;
    return last_pid;
}


因此要完成进程运行轨迹的记录就要在 copy_process() 中添加输出语句。


这里要输出两种状态,分别是“N(新建)”和“J(就绪)”。


例子 2:记录进入睡眠态的时间

第二个例子是记录进入睡眠态的时间。sleep_on() 和 interruptible_sleep_on() 让当前进程进入睡眠状态,这两个函数在 kernel/sched.c 文件中定义如下:

void sleep_on(struct task_struct **p)
{
    struct task_struct *tmp;
//    ……
    tmp = *p;
// 仔细阅读,实际上是将 current 插入“等待队列”头部,tmp 是原来的头部
    *p = current;
// 切换到睡眠态
    current->state = TASK_UNINTERRUPTIBLE;
// 让出 CPU
    schedule();
// 唤醒队列中的上一个(tmp)睡眠进程。0 换作 TASK_RUNNING 更好
// 在记录进程被唤醒时一定要考虑到这种情况,实验者一定要注意!!!
    if (tmp)
        tmp->state=0;
}
/* TASK_UNINTERRUPTIBLE和TASK_INTERRUPTIBLE的区别在于不可中断的睡眠
 * 只能由wake_up()显式唤醒,再由上面的 schedule()语句后的
 *
 *   if (tmp) tmp->state=0;
 *
 * 依次唤醒,所以不可中断的睡眠进程一定是按严格从“队列”(一个依靠
 * 放在进程内核栈中的指针变量tmp维护的队列)的首部进行唤醒。而对于可
 * 中断的进程,除了用wake_up唤醒以外,也可以用信号(给进程发送一个信
 * 号,实际上就是将进程PCB中维护的一个向量的某一位置位,进程需要在合
 * 适的时候处理这一位。感兴趣的实验者可以阅读有关代码)来唤醒,如在
 * schedule()中:
 *
 *  for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
 *      if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) &&
 *         (*p)->state==TASK_INTERRUPTIBLE)
 *         (*p)->state=TASK_RUNNING;//唤醒
 *
 * 就是当进程是可中断睡眠时,如果遇到一些信号就将其唤醒。这样的唤醒会
 * 出现一个问题,那就是可能会唤醒等待队列中间的某个进程,此时这个链就
 * 需要进行适当调整。interruptible_sleep_on和sleep_on函数的主要区别就
 * 在这里。
 */
void interruptible_sleep_on(struct task_struct **p)
{
    struct task_struct *tmp;
    tmp=*p;
    *p=current;
repeat:    current->state = TASK_INTERRUPTIBLE;
    schedule();
// 如果队列头进程和刚唤醒的进程 current 不是一个,
// 说明从队列中间唤醒了一个进程,需要处理
    if (*p && *p != current) {
 // 将队列头唤醒,并通过 goto repeat 让自己再去睡眠
        (**p).state=0;
        goto repeat;
    }
    *p=NULL;
//作用和 sleep_on 函数中的一样
    if (tmp)
        tmp->state=0;
}


总的来说,Linux 0.11 支持四种进程状态的转移:就绪到运行、运行到就绪、运行到睡眠和睡眠到就绪,此外还有新建和退出两种情况。其中就绪与运行间的状态转移是通过 schedule()(它亦是调度算法所在)完成的;运行到睡眠依靠的是 sleep_on() 和 interruptible_sleep_on(),还有进程主动睡觉的系统调用 sys_pause() 和 sys_waitpid();睡眠到就绪的转移依靠的是 wake_up()。所以只要在这些函数的适当位置插入适当的处理语句就能完成进程运行轨迹的全面跟踪了。


修改fork.c文件

fork.c文件在kernel目录下,这里要输出两种状态,分别是“N(新建)”和“J(就绪)”,下面做出两处修改:

int copy_process(int nr,……)
{
    struct task_struct *p;
//    ……
// 获得一个 task_struct 结构体空间
    p = (struct task_struct *) get_free_page();  
//    ……
    p->pid = last_pid;
//    ……
// 设置 start_time 为 jiffies
    p->start_time = jiffies; 
      //新增修改,新建进程
  fprintk(3,"%d\tN\t%d\n",p->pid,jiffies);   
//       ……
/* 设置进程状态为就绪。所有就绪进程的状态都是
   TASK_RUNNING(0),被全局变量 current 指向的
   是正在运行的进程。*/
    p->state = TASK_RUNNING;    
  //新增修改,进程就绪
  fprintk(3,"%d\tJ\t%d\n",p->pid,jiffies);
    return last_pid;
}


修改sched.c文件

文件位置:kernel/sched.c


修改schedule函数

//这里仅仅说一下改动了什么
/* check alarm, wake up any interruptible tasks that have got a signal */
for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
  if (*p) {
    if ((*p)->alarm && (*p)->alarm < jiffies) {
      (*p)->signal |= (1<<(SIGALRM-1));
      (*p)->alarm = 0;
            }
    if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) &&
    (*p)->state==TASK_INTERRUPTIBLE){
    (*p)->state=TASK_RUNNING;
              fprintk(3,"%d\tJ\t%d\n",(*p)->pid,jiffies);
            } 
  }
while (1) {
    c = -1; next = 0; i = NR_TASKS; p = &task[NR_TASKS];
// 找到 counter 值最大的就绪态进程
    while (--i) {
        if (!*--p)    continue;
        if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
            c = (*p)->counter, next = i;
    }       
// 如果有 counter 值大于 0 的就绪态进程,则退出
    if (c) break;  
// 如果没有:
// 所有进程的 counter 值除以 2 衰减后再和 priority 值相加,
// 产生新的时间片
    for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
          if (*p) 
              (*p)->counter = ((*p)->counter >> 1) + (*p)->priority;  
}
//切换到相同的进程不输出
if(current->pid != task[next] ->pid)
  {
  /*新建修改--时间片到时程序 => 就绪*/
  if(current->state == TASK_RUNNING)
    fprintk(3,"%d\tJ\t%d\n",current->pid,jiffies);
  fprintk(3,"%d\tR\t%d\n",task[next]->pid,jiffies);
  }
// 切换到 next 进程
switch_to(next);


修改sys_pause函数

int sys_pause(void)
{
  current->state = TASK_INTERRUPTIBLE;
  /*
  *修改--当前进程  运行 => 可中断睡眠
  */
  if(current->pid != 0)
  fprintk(3,"%d\tW\t%d\n",current->pid,jiffies);
  schedule();
  return 0;
}


修改sleep_on函数

void sleep_on(struct task_struct **p)
{
  struct task_struct *tmp;
  if (!p)
  return;
  if (current == &(init_task.task))
  panic("task[0] trying to sleep");
  tmp = *p;
  *p = current;
  current->state = TASK_UNINTERRUPTIBLE;
  /*
  *修改--当前进程进程 => 不可中断睡眠
  */
  fprintk(3,"%d\tW\t%d\n",current->pid,jiffies);
  schedule();
  if (tmp)
  {
  tmp->state=0;
  /*
  *修改--原等待队列 第一个进程 => 唤醒(就绪)
  */
  fprintk(3,"%d\tJ\t%d\n",tmp->pid,jiffies);
  }
}


修改interruptible_sleep_on函数

void interruptible_sleep_on(struct task_struct **p)
{
  struct task_struct *tmp;
  if (!p)
  return;
  if (current == &(init_task.task))
  panic("task[0] trying to sleep");
  tmp=*p;
  *p=current;
repeat: current->state = TASK_INTERRUPTIBLE;
  /*
  *修改--唤醒队列中间进程,过程中使用Wait
  */
  fprintk(3,"%d\tW\t%d\n",current->pid,jiffies);
  schedule();
  if (*p && *p != current) {
  (**p).state=0;
  /*
  *修改--当前进程 => 可中断睡眠
  */
  fprintk(3,"%d\tJ\t%d\n",(*p)->pid,jiffies);
  goto repeat;
  }
  *p=NULL;
  if (tmp)
  {
  tmp->state=0;
  /*
  *修改--原等待队列 第一个进程 => 唤醒(就绪)
  */
  fprintk(3,"%d\tJ\t%d\n",tmp->pid,jiffies);
  }
}


修改wake_up函数

void wake_up(struct task_struct **p)
{
  if (p && *p) {
  (**p).state=0;
  /*
  *修改--唤醒 最后进入等待序列的 进程
  */
  fprintk(3,"%d\tJ\t%d\n",(*p)->pid,jiffies);
  *p=NULL;
  }
}


修改exit.c文件

当一个进程结束了运行或在半途中终止了运行,那么内核就需要释放该进程所占用的系统资源。这包括进程运行时打开的文件、申请的内存等。


当一个用户程序调用exit()系统调用时,就会执行内核函数do_exit()。该函数会首先释放进程代码段和数据段占用的内存页面,关闭进程打开着的所有文件,对进程使用的当前工作目录、根目录和运行程序的i节点进行同步操作。如果进程有子进程,则让init进程作为其所有子进程的父进程。如果进程是一个会话头进程并且有控制终端,则释放控制终端(如果按照实验的数据,此时就应该打印了),并向属于该会话的所有进程发送挂断信号 SIGHUP,这通常会终止该会话中的所有进程。然后把进程状态置为僵死状态 TASK_ZOMBIE。并向其原父进程发送 SIGCHLD 信号,通知其某个子进程已经终止。最后 do_exit()调用调度函数去执行其他进程。由此可见在进程被终止时,它的任务数据结构仍然保留着。因为其父进程还需要使用其中的信息。


在子进程在执行期间,父进程通常使用wait()或 waitpid()函数等待其某个子进程终止。当等待的子进程被终止并处于僵死状态时,父进程就会把子进程运行所使用的时间累加到自己进程中。最终释放已终止子进程任务数据结构所占用的内存页面,并置空子进程在任务数组中占用的指针项。

int do_exit(long code)
{
  int i;
  free_page_tables(get_base(current->ldt[1]),get_limit(0x0f));
  free_page_tables(get_base(current->ldt[2]),get_limit(0x17));
  //    ……
  current->state = TASK_ZOMBIE;
  /*
  *修改--退出一个进程
  */
  fprintk(3,"%d\tE\t%d\n",current->pid,jiffies);
  current->exit_code = code;
  tell_father(current->father);
  schedule();
  return (-1);  /* just to suppress warnings */
}
//    ……
int sys_waitpid(pid_t pid,unsigned long * stat_addr, int options)
{
  int flag, code;
  struct task_struct ** p;
//    ……
//    ……
  if (flag) {
  if (options & WNOHANG)
    return 0;
  current->state=TASK_INTERRUPTIBLE;
  /*
  *修改--当前进程 => 等待
  */
  fprintk(3,"%d\tW\t%d\n",current->pid,jiffies);
  schedule();
  if (!(current->signal &= ~(1<<(SIGCHLD-1))))
    goto repeat;
  else
    return -EINTR;
  }
  return -ECHILD;
}


小结

总的来说,Linux 0.11 支持四种进程状态的转移:就绪到运行、运行到就绪、运行到睡眠和睡眠到就绪,此外还有新建和退出两种情况。其中就绪与运行间的状态转移是通过 schedule()(它亦是调度算法所在)完成的;运行到睡眠依靠的是 sleep_on() 和 interruptible_sleep_on(),还有进程主动睡觉的系统调用 sys_pause() 和 sys_waitpid();睡眠到就绪的转移依靠的是 wake_up()。所以只要在这些函数的适当位置插入适当的处理语句就能完成进程运行轨迹的全面跟踪了。


为了让生成的 log 文件更精准,以下几点请注意:


进程退出的最后一步是通知父进程自己的退出,目的是唤醒正在等待此事件的父进程。从时序上来说,应该是子进程先退出,父进程才醒来。

系统无事可做的时候,进程 0 会不停地调用 sys_pause(),以激活调度算法。此时它的状态可以是等待态,等待有其它可运行的进程;也可以叫运行态,因为它是唯一一个在 CPU 上运行的进程,只不过运行的效果是等待。

编译

重新编译

make all

目录
相关文章
|
7月前
|
存储 Linux API
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
在计算机系统的底层架构中,操作系统肩负着资源管理与任务调度的重任。当我们启动各类应用程序时,其背后复杂的运作机制便悄然展开。程序,作为静态的指令集合,如何在系统中实现动态执行?本文带你一探究竟!
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
|
9月前
|
算法 Linux 调度
深入理解Linux操作系统的进程管理
本文旨在探讨Linux操作系统中的进程管理机制,包括进程的创建、执行、调度和终止等环节。通过对Linux内核中相关模块的分析,揭示其高效的进程管理策略,为开发者提供优化程序性能和资源利用率的参考。
265 1
|
5月前
|
缓存 运维 前端开发
|
5月前
|
缓存 运维 前端开发
阿里云操作系统控制台:高效解决性能瓶颈与抖动之进程热点追踪
遇到“进程性能瓶颈导致业务异常”等多项业务痛点时,提供高效解决方案,并展示案例。
|
7月前
|
PHP Docker 容器
如何在宿主主机运行容器中的php守护进程
在Docker容器中同时运行多个程序(如Nginx+PHP+Ftp)时,需用`docker exec`命令启动额外服务。首先通过`php -v`查看PHP版本,再用`which php-fpm7.4`确认PHP安装路径,通常返回`/usr/sbin/php-fpm7.4`。最后直接运行该路径启动PHP-FPM服务,确保其正常工作。
131 14
|
8月前
|
监控 搜索推荐 开发工具
2025年1月9日更新Windows操作系统个人使用-禁用掉一下一些不必要的服务-关闭占用资源的进程-禁用服务提升系统运行速度-让电脑不再卡顿-优雅草央千澈-长期更新
2025年1月9日更新Windows操作系统个人使用-禁用掉一下一些不必要的服务-关闭占用资源的进程-禁用服务提升系统运行速度-让电脑不再卡顿-优雅草央千澈-长期更新
672 2
2025年1月9日更新Windows操作系统个人使用-禁用掉一下一些不必要的服务-关闭占用资源的进程-禁用服务提升系统运行速度-让电脑不再卡顿-优雅草央千澈-长期更新
|
9月前
|
C语言 开发者 内存技术
探索操作系统核心:从进程管理到内存分配
本文将深入探讨操作系统的两大核心功能——进程管理和内存分配。通过直观的代码示例,我们将了解如何在操作系统中实现这些基本功能,以及它们如何影响系统性能和稳定性。文章旨在为读者提供一个清晰的操作系统内部工作机制视角,同时强调理解和掌握这些概念对于任何软件开发人员的重要性。
|
9月前
|
Linux 调度 C语言
深入理解操作系统:从进程管理到内存优化
本文旨在为读者提供一次深入浅出的操作系统之旅,从进程管理的基本概念出发,逐步探索到内存管理的高级技巧。我们将通过实际代码示例,揭示操作系统如何高效地调度和优化资源,确保系统稳定运行。无论你是初学者还是有一定基础的开发者,这篇文章都将为你打开一扇了解操作系统深层工作原理的大门。
134 4
|
9月前
|
存储 算法 调度
深入理解操作系统:进程调度的奥秘
在数字世界的心脏跳动着的是操作系统,它如同一个无形的指挥官,协调着每一个程序和进程。本文将揭开操作系统中进程调度的神秘面纱,带你领略时间片轮转、优先级调度等策略背后的智慧。从理论到实践,我们将一起探索如何通过代码示例来模拟简单的进程调度,从而更深刻地理解这一核心机制。准备好跟随我的步伐,一起走进操作系统的世界吧!
|
9月前
|
算法 调度 开发者
深入理解操作系统:进程与线程的管理
在数字世界的复杂编织中,操作系统如同一位精明的指挥家,协调着每一个音符的奏响。本篇文章将带领读者穿越操作系统的幕后,探索进程与线程管理的奥秘。从进程的诞生到线程的舞蹈,我们将一起见证这场微观世界的华丽变奏。通过深入浅出的解释和生动的比喻,本文旨在揭示操作系统如何高效地处理多任务,确保系统的稳定性和效率。让我们一起跟随代码的步伐,走进操作系统的内心世界。
130 2

推荐镜像

更多