建议阅读这里列举的参考资料,本文仅用于备忘。
参考资料
本次的分享是非常浅的,更多内容请查看:
具体的请阅读《Linux内核0.11完全注释_V3.0 by 赵炯 (z-lib.org).pdf》
以及inter的官方英文手册: 《i386.pdf》
要想理解机器怎么工作的,必须要读一遍汇编:《汇编语言(第3版) 》王爽著
调汇编可以用vscode提供的插件,具体请参考这3篇文章:
https://blog.csdn.net/weixin_45708190/article/details/121134296
https://marketplace.visualstudio.com/items?itemName=xsro.masm-tasm
https://gitee.com/dosasm/masm-tasm/
关于操作系统,我个人推荐去大家看两个老师的视频,B站上面都有。
一个是 操作系统哈尔滨工业大学李治军老师 , 这个是真正将linux0.11源码的,需要汇编。
另外一个是南京大学jyy老师 , 这个课程,你只需要有C语言基础就可以。
什么是CPU ?
jyy老师的课程要求实现一个cpu,按照那个实验要求去实现一个就能明白,下面简单说明一下。
简单来说,CPU只会取指令,执行指令。
从微观的角度来看,程序就是状态机。
我们在通讯协议的时候,经常用状态机,比如在OTA中,我们定义9007这种指令会让我们跳转到升级这种状态。
而在CPU中,我们定义当收到某种101010组合时,应该执行什么样的状态切换。
比如,CPU 中寄存器 ax ,bx 分别为 1,0 ,这是一种状态。 我们把这种状态叫做 (1,0)
在执行: mov ax,bx 后, ax,bx 都为 1 , 这是另外一种状态。我们把这种状态叫做 (1,1)
CPU因为执行了 程序 mov ax,bx ,由状态(1,0) -> 状态(1,1)
我们把OTA用的协议是家电通讯协议,CPU使用的协议是x86指令集,riscv指令集, arm指令集。
什么是进程
进程是运行中的程序。
是系统进行资源分配和调度的基本单位。
裸机也可以跑,批处理也可以跑,为什么要有操作系统呢?
因为我们希望 计算机能同时处理多个任务(看上去) ,更合理的去利用CPU资源。
多任务并发执行,而CPU只有一个,这种矛盾 , 操作系统是怎么处理的?
下面,我们针对linux0.11来简单说一下。
如何描述一个进程
GDT是全局表,GDT中有 LDT和TSS。
一个进程由两部分构成:任务执行空间(由LDT指定),任务状态段TSS
当进行任务切换的时候,我们切换到内核,把LDT,TSS换成新任务的LDT,TSS就可以了,Linux中就是这么做的。
我们回到CPU视角,如果从状态机的视角来看,是不是相当于把旧任务的状态拍张照片存起来, 然后把新任务之前拍的照片载入回来?
任务切换就是 从 旧任务的状态X ,完全切换到 新任务的状态Y。
这只是操作系统提供的抽象,CPU实际上是不管的,它还是给什么指令就执行什么指令。
一些细节
段页结合的内存管理
我们程序是分段的,比如编译出来的程序,分 代码段,数据段,堆栈段
我们希望我们的程序就像我们写的代码那样一段一段的在内存中,
但是这样就不能充分利用我们的内存了,比如任务A分配了一大段内存,但是实际上它目前还没有用,而其他任务不能再申请这段内存了。这是很不划算的。
也就是说,我们物理内存,希望内存是一页一页的,1页4KB,那么这样最多浪费4KB。
所以我们引入了虚拟内存。
我们通过逻辑地址转换为虚拟地址,再转换到真正的物理地址。有了这种办法,我们还可以实现写时复制,以及硬盘内存交换。
而这种转换是由硬件完成的。
详细请查看《Linux内核0.11完全注释_V3.0 by 赵炯 (z-lib.org).pdf》第四章
什么是系统调用
可以去看李治军老师的课程,实现一个系统调用就能明白。
其实就是把函数写到 IDT表中,
然后CPU中有一条指令 INT , 这条指令会根据你传入参数 在 IDT表中 找到对应的 函数去执行
比如:执行 INT 0X80 就会调用system_call ,向eax中填入对应的功能号,就能跳到sys_call_table中的对应号码的函数。
用户程序要用硬件资源,都是要经过操作系统的,包括一个简单的printf(其实也不是很简单)。
走查一遍linux0.11中的系统调用代码。
创建任务0,任务1
这两个任务的开始,都是通过main函数的。
我们看main函数中关于这两个任务的操作。
void sched_init(void) 手动填好了task0的内容
void sched_init(void)
{
int i;
struct desc_struct * p; //描述符表结构指针。
if (sizeof(struct sigaction) != 16) //检查内核代码是否有人修改。
panic("Struct sigaction MUST be 16 bytes");
//在全局描述符表中设置初始任务,
set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));//gdt[4]
set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));//gdt[5]
p = gdt+2+FIRST_TSS_ENTRY; //gdt[6]
for(i=1;i<NR_TASKS;i++) {
//清除任务数组和描述符表项。
task[i] = NULL;
p->a=p->b=0;
p++;
p->a=p->b=0;
p++;
}
/* Clear NT, so that we won't have troubles with that later on */
//NT标志用于控制程序的递归调用 nested task , 当NT置位,当前中断执行iret指令时就会发生任务切换,NT指出TSS中的back_link字段是否有效。
__asm__("pushfl ; andl $0xffffbfff,(%esp) ; popfl");
ltr(0);//加载到任务寄存器tr
lldt(0);//加载到局部描述符表寄存器。
outb_p(0x36,0x43); /* binary, mode 3, LSB/MSB, ch 0 */ //初始化8253定时器。
outb_p(LATCH & 0xff , 0x40); /* LSB */
outb(LATCH >> 8 , 0x40); /* MSB */ //10ms发一次中断。
set_intr_gate(0x20,&timer_interrupt);
outb(inb_p(0x21)&~0x01,0x21); //开启时钟中断。
set_system_gate(0x80,&system_call);
}
每个任务在GDT表中占有两个描述符选项:任务状态段TSS的描述符tss和局部描述符表LDT的描述符ldt
任务0完成配置之后的GDT表的样子。
此时,任务0的 tss,LDT ,以及这两者的选择符跟GDT表已经关联起来,剩下的,只要发生iret就能正常的进行任务切换,由于一开始并没task在跑,所以我们要任务的模拟一次iret.
让程序特权级变成用户模式3.
#define move_to_user_mode() \
__asm__ ("movl %%esp,%%eax\n\t" \ //保存堆栈到eax寄存器,
"pushl $0x17\n\t" \ //首先将堆栈段选择符SS 入栈, 0x17任务数据段
"pushl %%eax\n\t" \ //esp入栈
"pushfl\n\t" \ //将eflage入栈
"pushl $0x0f\n\t" \ //将task0代码段选择符CS入栈 , 0x0f任务代码段。
"pushl $1f\n\t" \ //将标号1的偏移地址eip压入,
"iret\n" \ //执行中断返回。
"1:\tmovl $0x17,%%eax\n\t" \ //此时开始执行任务0
"movw %%ax,%%ds\n\t" \ //初始化段寄存器指向本局部表的数据段。
"movw %%ax,%%es\n\t" \
"movw %%ax,%%fs\n\t" \
"movw %%ax,%%gs" \
:::"ax")
其中0x17是进程0的局部描述符表中数据段的选择符,0x0f是进程0的局部描述符表中代码段的选择符
当发生iret之后,ss,esp,eflags,cs,eip 都会被弹出,赋值到对应的寄存器,此时代码段跟数据段已经从内核切换到了进程0
然后把其他段也更新为0x17
然后立刻fork.
nr是任务号,是任务数组task中的索引,值得注意的是,除了进程0,其他任务号,跟这个任务什么时候启动是没有关系的。
int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,
long ebx,long ecx,long edx,
long fs,long es,long ds,
long eip,long cs,long eflags,long esp,long ss) //系统调用fork用到。
{
struct task_struct *p;
int i;
struct file *f;
p = (struct task_struct *) get_free_page(); //指向一个真实的物理地址页面
if (!p)
return -EAGAIN;
task[nr] = p; //task 指向这个进程的控制块
*p = *current; /* NOTE! this doesn't copy the supervisor stack */
p->state = TASK_UNINTERRUPTIBLE; //防止中断了。
p->pid = last_pid; //由 find_empty_process 找到。
p->father = current->pid;
p->counter = p->priority; //运行时间片等于优先级一般为15
p->signal = 0;
p->alarm = 0;
p->leader = 0; /* process leadership doesn't inherit */
p->utime = p->stime = 0; //用户态与核心态运行时间都为0
p->cutime = p->cstime = 0; //子进程用户态和核心态运行时间。
p->start_time = jiffies; //开始运行时间是当前的滴答数。
p->tss.back_link = 0;
p->tss.esp0 = PAGE_SIZE + (long) p; //指向内核栈的顶端。
p->tss.ss0 = 0x10;//内核态栈的段选择符
p->tss.eip = eip;//指令代码指针
p->tss.eflags = eflags;//标志寄存器
p->tss.eax = 0;//fork返回时,新进程会返回0
p->tss.ecx = ecx;//一些寄存器现场。
p->tss.edx = edx;
p->tss.ebx = ebx;
p->tss.esp = esp;
p->tss.ebp = ebp;
p->tss.esi = esi;
p->tss.edi = edi;
p->tss.es = es & 0xffff;
p->tss.cs = cs & 0xffff;
p->tss.ss = ss & 0xffff;
p->tss.ds = ds & 0xffff;
p->tss.fs = fs & 0xffff;
p->tss.gs = gs & 0xffff;
p->tss.ldt = _LDT(nr); //任务局部表描述符的选择符,把GDT中本任务LDT段描述符的选择符保存到本任务的TSS段中。
p->tss.trace_bitmap = 0x80000000;//IO位图基地址。高16位有效
if (last_task_used_math == current)
__asm__("clts ; fnsave %0"::"m" (p->tss.i387));//如果协处理器之前调用了,就把这个也保存到当前进程中,因为这是fork之前发生的。
if (copy_mem(nr,p)) {
task[nr] = NULL;
free_page((long) p);
return -EAGAIN;
}
for (i=0; i<NR_OPEN;i++) //如果父进程中由文件时打开的,对应文件打开次数+1
if (f=p->filp[i])
f->f_count++;
if (current->pwd) //pwd,root,executable的引用次数+1
current->pwd->i_count++;
if (current->root)
current->root->i_count++;
if (current->executable)
current->executable->i_count++;
set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss));//gdt+(nr<<1)+FIRST_TSS_ENTRY ,任务NR的tss描述符项,在全局表中的地址。每个任务占用GDT表中的2项
set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));//将这个地址写到全局描述符中。
p->state = TASK_RUNNING; /* do this last, just in case */ //将新任务设置为就绪态。
return last_pid; //这个地方就是父进程的返回值,这也是为什么父进程中fork的返回值刚好是子进程的pid
}
拿到新的页,来建PCB, 同时把父进程的内存页面,打开的文件,pwd,root,executable等等的引用+1,需要操作时再触发中断,实现写时复制。
这个时候,0号进程的子进程,就建立好了。
这个函数,其实只是帮进程1向 系统注册了一些表。
CPU继续运行 进程0,然后进程0 ,一直调pause,这个函数是让内核去重新调度,找其他进程。
for(;;) pause();//没有其他任务执行,就来这里。父进程就一直暂停,暂时停止,其实是重新调度,任务0没有别的事。
那么这个时候,task表中已经有了子进程了,重新调度之后,子进程就进入了调度:
if (!fork()) {
/* we count on this going ok */
init();//在新建的子进程,任务1中执行。 fork 子进程返回0
}
切换到进程1 之后, 我们就把刚才设置好的现场恢复,eax刚好为0 ,进入init()。
在linux中,我们是通过PCB来描述一个进程的,TSS,LDT 就是我们非常关键的数据结构。
当线程切换的时候,就把TSS中的内容扣入CPU,将LDT表切换,即可。
也就是说,把当前进程的运行到的状态记录下来,把当前进程的代码,数据在内存中的位置记录下来。
切换任务之前保存好,切换任何的时候再把这些东西恢复。
同时,在内核中维护几张表,来保存这些内容的索引即可。
跟踪进程运行轨迹
pid X time
其中:- pid是进程的ID
X可以是N,J,R,W和E中的任意一个
N 进程新建 ,new
J 进入就绪态 , J
R 进入运行态 , run
W 进入阻塞态, wait
E 退出 ,exit
time表示X发生的时间。这个时间不是物理时间,而是系统的滴答时间(tick)
仔细阅读sched.c文件
本实验主要分析整个的调度流程,主要方法是,当发生状态转换时,进程的state变化时,我们把这个记录记下来。
先看看打印函数:
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);
if (fd < 3) /* 如果输出到stdout或stderr,直接调用sys_write即可 */
{
__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"
"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的描述符都与文件关联。事实上,还存在很多其他情况,这里并没有考虑 */
{
if (!(file=task[0]->filp[fd])) /* 从进程0的文件描述符表中得到文件句柄 */
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" //使用file_write,往文件/var/process.log中写入。
"addl $12, %%esp\n\t"
"popl %0\n\t"
"pop %%fs"
::"r" (count), "r" (file), "r" (inode)
:"ax", "cx", "dx");
}
return count;
}
linux源码中,只有这几种状态:
#define TASK_RUNNING 0 //正在运行或者已就绪
#define TASK_INTERRUPTIBLE 1 //进程处于可中断等待状态
#define TASK_UNINTERRUPTIBLE 2 //程序处于不可中断等待状态,主要用于I/O操作等待。
#define TASK_ZOMBIE 3 //程序处于僵死状态,已经停止运行,但父进程还没发信号。
#define TASK_STOPPED 4 //程序已停止。
N 进程新建 ,new
新进程在fork的时候产生,fork的关键是copy_process
int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,
long ebx,long ecx,long edx,
long fs,long es,long ds,
long eip,long cs,long eflags,long esp,long ss)
{
...
p->start_time = jiffies;
fprintk(3, "%ld\t%c\t%ld\n", last_pid, 'N', jiffies); //新建
...
p->state = TASK_RUNNING; /* do this last, just in case */
fprintk(3, "%ld\t%c\t%ld\n", last_pid, 'J', jiffies); //完成后,立马进入就绪态。
return last_pid;
}
W 进入阻塞态, wait
//系统调用waitpid(),挂起当前进程,直到pid指定的子进程退出。
int sys_waitpid(pid_t pid,unsigned long * stat_addr, int options)
{
...
if (flag) {
if (options & WNOHANG)
return 0;
current->state=TASK_INTERRUPTIBLE;
fprintk(3, "%ld\t%c\t%ld\n", current->pid, 'W', jiffies);//系统调用,等待。
schedule();
if (!(current->signal &= ~(1<<(SIGCHLD-1))))
goto repeat;
else
return -EINTR;
}
return -ECHILD;
}
E 退出 ,exit
int do_exit(long code)
{
...
current->state = TASK_ZOMBIE;
fprintk(3, "%ld\t%c\t%ld\n", current->pid, 'E', jiffies);
/* print message end in 3-processTack */
current->exit_code = code;
tell_father(current->father);
schedule();
return (-1); /* just to suppress warnings */
}
就绪与运行
void schedule(void)
void schedule(void)
{
int i,next,c;
struct task_struct ** p;
/* 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));//到点,给信号alarm
(*p)->alarm = 0;
}
if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) && //除去被block掉的信号,还有其他信号,
(*p)->state==TASK_INTERRUPTIBLE) //且进程是可中断的
{
(*p)->state=TASK_RUNNING;
fprintk(3, "%ld\t%c\t%ld\n", (*p)->pid, 'J', jiffies);//进程进入就绪。
}
}
/* this is the scheduler proper: */
while (1) {
c = -1;
next = 0;
i = NR_TASKS;
p = &task[NR_TASKS];
while (--i) {
if (!*--p)
continue;
if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
c = (*p)->counter, next = i;//检查所有任务中,最需要被调度的进程,将这个号码放入next,这个就是下次要调度的进程。
}
if (c) break; /* 找到一个counter不等于0 且是TASK_RUNNING状态中的counter最大的进程;或者当前系统没有一个可以运行的进程,此时c=-1, next=0,进程0得到调度,所以调度算法是不在意进程0的状态是不是TASK_RUNNING,这就意味这进程0可以直接从睡眠切换到运行! */
for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
if (*p)
(*p)->counter = ((*p)->counter >> 1) +
(*p)->priority;
}
if (task[next]->pid != current->pid) {
//打印就绪,运行,就切到那个进程。
if (current->state == TASK_RUNNING) {
fprintk(3, "%ld\t%c\t%ld\n", current->pid, 'J', jiffies);
}
fprintk(3, "%ld\t%c\t%ld\n", task[next]->pid, 'R', jiffies);
}
switch_to(next);
}
int sys_pause(void)
/*
* 系统无事可做的时候,进程0会不停地调用sys_pause(),以激活调度算法。此时它的状态可以是等待态,
* 等待有其他可运行的进程;也可以是运行态,因为它是唯一一个在CPU上运行的进程,只不过运行的效果是等待。
*/
int sys_pause(void)
{
current->state = TASK_INTERRUPTIBLE;
if (current->pid != 0) //进程0死循环,一直调用这个函数。防止一直打印。
{
fprintk(3, "%ld\t%c\t%ld\n", current->pid, 'W', jiffies);
}
schedule();
return 0;
}
void sleep_on(struct task_struct **p)
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插入“等待队列”头部,tmp是原来的头部 */
current->state = TASK_UNINTERRUPTIBLE;
fprintk(3, "%ld\t%c\t%ld\n", current->pid, 'W', jiffies);
schedule();
if (tmp) {
fprintk(3, "%ld\t%c\t%ld\n", tmp->pid, 'J', jiffies);
/* print message end in 3-processTack */
tmp->state=TASK_RUNNING; /* 唤醒队列中的上一个(tmp)睡眠进程。0换作TASK_RUNNING更好。在记录进程被唤醒时一定要考虑到这种情况,实验者一定要注意!!! */
}
}
void interruptible_sleep_on(struct task_struct **p)
/*
* TASK_UNINTERRUPTIBLE和TASK_INTERRUPTIBLE的区别在于不可中断的睡眠只能由wake_up()显示唤醒,
* 再由上面的 schedule()语句后面的
*
* if (tmp) tmp->state=0;
*
* 依次唤醒,所以不可中断的睡眠一定是严格按照从“队列”(一个依靠放在进程内核栈中的指针变量tmp
* 维护的队列)的首部进行唤醒。而对于可中断的进程,除了用wake_up唤醒以外,也可以用信号(给进程
* 发送一个信号,实际上就是将进程PCB中维护的一个向量的某一位置位,进程需要在合适的时候处理
* 这一位。感兴趣的实验者可以阅读有关代码)来唤醒,如在 schedule()中:
*
* for(p = &LAST_TASK; p > &FRIST_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) //将当前任务置为可中断的等待状态,并放入*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; //等待的任务可中断。
schedule();//只有当进程被唤醒才会回到这里。
if (*p && *p != current) {
//此current不等于当时的current,说明队列中间有任务被唤醒了。
(**p).state=TASK_RUNNING;//将队头唤醒。
fprintk(3, "%ld\t%c\t%ld\n", (*p)->pid, 'J', jiffies);
goto repeat;//重新调度。
}
*p=NULL;
if (tmp)//从队头开始,逐个释放。
tmp->state=0;
fprintk(3, "%ld\t%c\t%ld\n", (*p)->pid, 'J', jiffies);
}
void wake_up(struct task_struct **p)
void wake_up(struct task_struct **p)
{
if (p && *p) {
(**p).state=0;
fprintk(3, "%ld\t%c\t%ld\n", (*p)->pid, 'J', jiffies);
*p=NULL;
}
}
进程运行log分析
1 N 48 copy_process //进程1新建(init())。此前是进程0建立和运行,我们是在move_to_user_mode后才新建/var/process.log文件进行进程运行轨迹记录的,所以进程0的新建和运行没有出现在log文件里
1 J 48 copy_process //新建后进入就绪队列
0 J 48 schedule /* --- 进程0从运行->就绪,让出CPU。(执行的是pause,调用)
1 R 48 schedule 进程1运行 --- */
2 N 49 copy_process /* --- 进程1建立进程2。
2 J 49 copy_process 进程2会运行/etc/rc脚本,然后退出 --- */
1 W 49 sys_waitpid //进程1开始等待(等待进程2退出)
2 R 49 schedule //进程2运行
3 N 64 copy_process /* --- 进程2建立进程3
3 J 64 copy_process 进程3是/bin/sh建立的运行脚本的子进程 --- */
2 E 68 do_exit //进程2不等进程3退出,就先走一步了
1 J 68 schedule /* --- 进程1此前在等待进程2退出,被阻塞。进程2退出后,重新进入就绪队列
1 R 68 schedule 进程1运行 --- */
4 N 69 copy_process /* --- 进程1建立进程4,即shell
4 J 69 copy_process 进程4进入就绪队列 --- */
1 W 69 sys_waitpid //进程1等待shell退出(除非执行exit命令,否则shell不会退出)
3 R 69 schedule //进程3开始运行
3 W 75 sys_pause //进程3主动睡觉,让出CPU
4 R 75 schedule //进程4开始运行
5 N 107 copy_process /* --- 进程5是shell建立的不知道做什么的进程(可能是ls命令?)
5 J 107 copy_process 进程5新建后进入就绪队列 --- */
4 W 108 sleep_on //进程4进入不可中断睡眠状态
5 R 108 schedule //进程5开始运行
4 J 110 wake_up //进程5唤醒睡觉的进程4,进程4进入就绪队列
5 E 110 do_exit //进程5很快退出
4 R 110 schedule //进程4执行
4 W 115 interruptible_sleep_on //shell等待用户输入命令
0 R 115 schedule //因为无事可做,所以进程0重出江湖
4 J 902 wake_up //用户开始输入命令了,唤醒了shell(从等待用户输入到用户开始输入命令总共经历了将近8s)
4 R 902 schedule //进程4执行
4 W 903 interruptible_sleep_on //shell等待用户输入命令
0 R 903 schedule //因为无事可做,进程0再次执行
4 J 953 wake_up //用户接着输入命令,唤醒shell(shell等待了0.5s)
4 R 953 schedule //进程4执行
4 W 953 interruptible_sleep_on //shell等待用户输入命令
0 R 953 schedule //因为无事可做,进程0再次执行
4 J 1003 wake_up //用户接着输入命令,唤醒shell(shell等待了0.5s)
4 R 1003 schedule //shell执行
4 W 1003 interruptible_sleep_on //shell等待用户输入
0 R 1003 schedule //因为无事可做,进程0再次执行
4 J 1056 wake_up //用户接着输入命令,唤醒shell(shell等待了0.53s)
4 R 1056 schedule //shell执行
4 W 1056 interruptible_sleep_on //shell等待用户输入
0 R 1056 schedule //因为无事可做,进程0再次执行
4 J 1182 wake_up //用户接着输入命令,唤醒shell(shell等待了1.26s),该命令输入完毕(用户输入该命令总共经历了将近1.8s)
4 R 1182 schedule //shell执行
6 N 1184 copy_process /* --- 进程6是shell建立的不知道做什么的进程
6 J 1185 copy_process 进程6进入就绪队列 --- */
4 W 1185 sys_waitpid //shell开始等待(等待进程6退出)
6 R 1185 schedule //进程6开始执行
6 E 1191 do_exit //进程6很快退出
4 J 1191 schedule //shell此前在等待进程6退出,被阻塞。进程6退出后,重新进入就绪队列
4 R 1191 schedule //shell执行
7 N 1192 copy_process /* --- 进程7是shell建立的不知道做什么的进程
7 J 1192 copy_process 进程7进入就绪队列 --- */
4 J 1193 schedule /* --- shell从运行->就绪,让出CPU。
7 R 1193 schedule 进程7开始运行 --- */
7 E 1195 do_exit //进程7很快退出
4 R 1195 schedule //shell执行
4 W 1196 interruptible_sleep_on //shell等待用户输入命令
0 R 1196 schedule //因为无事可做,进程0再次执行
4 J 1251 wake_up //用户开始输入命令,唤醒了shell(从等待用户输入命令到用户开始输入命令,总共经历了0.55s)
4 R 1251 schedule //shell执行
4 W 1251 interruptible_sleep_on //shell等待用户继续输入命令
0 R 1252 schedule //因为无事可做,进程0再次执行
4 J 2345 wake_up //用户接着输入命令,唤醒shell(shell等待了0.93s)
4 R 2345 schedule //shell执行
4 W 2345 interruptible_sleep_on //shell等待用户输入
0 R 2345 schedule //因为无事可做,进程0再次执行
4 J 2386 wake_up //用户接着输入命令,唤醒shell(shell等待了0.41s)
4 R 2386 schedule //shell执行
4 W 2386 interruptible_sleep_on //shell等待用户输入
0 R 2386 schedule //因为无事可做,进程0再次执行
4 J 2823 wake_up //用户接着输入命令,唤醒shell(shell等待了4.37s)
4 R 2823 schedule //shell执行
4 W 2823 interruptible_sleep_on //shell等待用户输入
0 R 2823 schedule //因为无事可做,进程0再次执行
4 J 2864 wake_up //用户接着输入命令,唤醒shell(shell等待了0.41s)
4 R 2864 schedule //shell执行
4 W 2864 interruptible_sleep_on //shell等待用户输入
0 R 2864 schedule //因为无事可做,进程0再次执行
4 J 2898 wake_up //用户接着输入命令,唤醒shell(shell等待了0.34s)
4 R 2898 schedule //shell执行
4 W 2898 interruptible_sleep_on //shell等待用户继续输入
0 R 2898 schedule //因为无事可做,进程0再次执行
4 J 2936 wake_up //用户接着输入命令,唤醒shell(shell等待了0.38s)
4 R 2936 schedule //shell执行
4 W 2936 interruptible_sleep_on //shell等待用户输入
0 R 2936 schedule //因为无事可做,进程0再次执行
4 J 3046 wake_up //用户接着输入命令,唤醒shell(shell等待了1.1s)
4 R 3046 schedule //shell执行
4 W 3046 interruptible_sleep_on //shell等待用户输入
0 R 3046 schedule //因为无事可做,进程0再次执行
3 J 3074 schedule /* --- 进程3从睡眠->就绪。
3 R 3074 schedule 进程3运行(进程3由于长时间睡觉,其优先级变高,进入就绪队列后立即得到调度执行) --- */
3 W 3074 sys_pause //进程3主动睡觉,让出CPU
0 R 3074 schedule //因为无事可做,进程0再次执行
4 J 3099 wake_up //用户接着输入命令,唤醒shell(shell等待了0.53s)
4 R 3100 schedule //shell执行
4 W 3100 interruptible_sleep_on //shell等待用户输入
0 R 3100 schedule //因为无事可做,进程0再次执行
4 J 3416 wake_up //用户接着输入命令,唤醒shell(shell等待了3.16s)
4 R 3417 schedule //shell执行
4 W 3417 interruptible_sleep_on //shell等待用户输入
0 R 3417 schedule //因为无事可做,进程0再次执行
4 J 3457 wake_up //用户接着输入命令,唤醒shell(shell等待了0.4s)
4 R 3457 schedule //shell执行
4 W 3457 interruptible_sleep_on //shell等待用户输入
0 R 3457 schedule //因为无事可做,进程0再次执行
4 J 3623 wake_up //用户接着输入命令,唤醒shell(shell等待了1.66s)
4 R 3623 schedule //shell执行
4 W 3623 interruptible_sleep_on //shell等待用户输入
0 R 3623 schedule //因为无事可做,进程0再次执行
4 J 3687 wake_up //用户接着输入命令,唤醒shell(shell等待了0.64s)
4 R 3687 schedule //shell执行
4 W 3687 interruptible_sleep_on //shell等待用户输入
0 R 3687 schedule
....
0 R 7494 schedule //因为无事可做,进程0再次运行
4 J 7560 wake_up //用户继续输入命令,唤醒shell(shell等待了0.66s)
4 R 7560 schedule //shell执行
4 W 7560 interruptible_sleep_on //shell等待用户输入
0 R 7561 schedule //因为无事可做,进程0再次执行
4 J 7617 wake_up //用户继续输入命令,唤醒shell(shell等待了0.56s)
4 R 7617 schedule //shell执行
4 W 7617 interruptible_sleep_on //shell等待用户输入
0 R 7617 schedule //因为无事可做,进程0再次执行
4 J 8090 wake_up //用户继续输入命令,唤醒shell(shell等待了3.73s),该命令输入完毕(用户输入该命令总共经历了67.39s,一分钟的时间,用户输入不可能这么长!那么这个命令是什么?全部是由用户输入?)
4 R 8090 schedule //shell执行该命令
8 N 8092 copy_process /* --- shell新建进程8(估计是gcc编译process.c生成可执行文件process的命令)。
8 J 8093 copy_process 进程8新建后进入就绪队列 --- */
4 W 8093 sys_waitpid //shell开始等待进程8退出
8 R 8093 schedule //进程8开始运行
9 N 8098 copy_process /* --- 进程8新建进程9(估计是gcc用于编译process.c的预处理子进程?)。
9 J 8098 copy_process 进程9新建后进入就绪队列 --- */
8 W 8099 sys_waitpid //进程8开始等待进程9退出
9 R 8099 schedule //进程9开始运行
9 E 8159 do_exit //进程9退出(运行了0.6s)
8 J 8159 schedule /* --- 进程8此前在等待进程9退出,被阻塞。进程9退出后,重新进入就绪队列。
8 R 8159 schedule 进程8运行 --- */
10 N 8160 copy_process /* --- 进程8新建进程10(估计是gcc用于编译process.c的编译子进程,生成汇编代码?)。
10 J 8161 copy_process 进程10新建后进入就绪队列 --- */
8 W 8161 sys_waitpid //进程8开始等待进程10退出
10 R 8161 schedule //进程10开始运行
10 E 8295 do_exit //进程10退出(运行了1.34s)
8 J 8295 schedule /* --- 进程8此前在等待进程10退出,被阻塞。进程10退出后,重新进入就绪队列。
8 R 8295 schedule 进程8继续运行 --- */
11 N 8295 copy_process /* --- 进程8新建进程11(估计是gcc用于编译process.c的汇编子进程,生成二进制目标文件?)。
11 J 8296 copy_process 进程11新建后进入就绪队列 --- */
8 W 8296 sys_waitpid //进程8开始等待进程11退出
11 R 8296 schedule //进程11开始运行
11 E 8341 do_exit //进程11退出(运行了0.45s)
8 J 8341 schedule /* --- 进程8此前在等待进程11退出,被阻塞。进程11退出后,重新进入就绪队列。
8 R 8341 schedule 进程8继续运行 --- */
12 N 8343 copy_process /* --- 进程8新建进程12(估计是gcc用于编译process.c的链接子进程,生成可执行文件process?)。
12 J 8344 copy_process 进程12新建后进入就绪队列 --- */
8 W 8344 sys_waitpid //进程8开始等待进程12退出
12 R 8344 schedule //进程12开始运行
12 E 8425 do_exit //进程12退出(运行了0.81s)
8 J 8425 schedule /* --- 进程8此前在等待进程12退出,被阻塞。进程12退出后,重新进入就绪队列。
8 R 8425 schedule 进程8继续运行 --- */
8 E 8427 do_exit //进程8退出(gcc编译process.c生成process可执行文件经历了3.34s)
4 J 8427 schedule /* --- shell此前在等待进程8退出,被阻塞。进程8退出后,重新进入就绪队列
4 R 8427 schedule shell运行 --- */
13 N 8427 copy_process /* --- shell新建进程13(不知道是做什么的进程)。
13 J 8428 copy_process 进程13新建后进入就绪队列 --- */
4 W 8428 sleep_on //进程4进入不可中断睡眠状态
13 R 8428 schedule //进程13开始运行
4 J 8430 wake_up //进程13唤醒睡觉的进程4,进程4进入就绪队列
13 E 8431 do_exit //进程13很快退出(运行了0.03s)
4 R 8431 schedule //shell运行
4 W 8432 interruptible_sleep_on //shell等待用户输入命令
0 R 8432 schedule //因为无事可做,进程0再次执行
4 J 9737 wake_up //用户开始输入命令,唤醒了shell(从等待用户输入命令到用户开始输入命令,总共经历了13s!为什么会这么长时间?)
4 R 9737 schedule //shell运行
4 W 9737 interruptible_sleep_on //shell等待用户输入命令
0 R 9737 schedule //因为无事可做,进程0再次执行
4 J 9790 wake_up //用户接着输入命令,唤醒了shell(shell等待了0.53s)
4 R 9790 schedule //shell运行
4 W 9790 interruptible_sleep_on //shell等待用户输入命令
......
0 R 11136 schedule //因为无事可做,进程0再次执行
4 J 11195 wake_up //用户接着输入命令,唤醒了shell(shell等待了0.59s)
4 R 11195 schedule //shell运行
4 W 11195 interruptible_sleep_on //shell等待用户输入命令
0 R 11196 schedule //因为无事可做,进程0再次执行
4 J 12048 wake_up //用户接着输入命令,唤醒了shell(shell等待了8.52s),该命令输入完毕(用户输入该命令总共经历了231s!输入不可能需要这么长时间!说明计时单位并不是10ms???)
4 R 12048 schedule //shell执行该命令(即我们自己编写的样本程序process可执行文件)
14 N 12049 copy_process /* --- shell新建进程14(即process.c的main()函数)。
14 J 12049 copy_process 进程14新建后进入就绪队列 --- */
4 W 12049 sys_waitpid //shell开始等待进程14(process.c的main()函数)退出
14 R 12050 schedule //进程14(process.c的main()函数)开始运行
15 N 12051 copy_process /* --- main()函数新建进程15(N1 node)。
15 J 12051 copy_process 进程15(N1 node)新建后进入就绪队列 --- */
16 N 12052 copy_process /* --- main()函数新建进程16(N2 node)。
16 J 12052 copy_process 进程16(N2 node)新建后进入就绪队列 --- */
14 W 12053 sys_waitpid //进程14(现在为process.c的main()函数中的Root)开始等待进程15(N1 node)和进程16(N2 node)退出
16 R 12053 schedule //进程16(N2 node)开始运行
17 N 12053 copy_process /* --- 进程16新建进程17(N5 node)。
17 J 12053 copy_process 进程17(N5 node)新建后进入就绪队列 --- */
16 W 12054 sys_pause //进程16(N2 node)主动睡觉
17 R 12054 schedule //进程17(N5 node)开始运行
17 J 12069 schedule /* --- 进程17(n5 node)时间片(15个滴答)耗完,从运行->就绪(运行了0.15s),让出CPU。
15 R 12069 schedule 进程15(N1 node)开始运行 --- */
18 N 12069 copy_process /* --- 进程15(N1 node)新建进程18(N3 node)。
18 J 12069 copy_process 进程18(N3 node)新建后进入就绪队列 --- */
19 N 12070 copy_process /* --- 进程15(N1 node)新建进程19(N4 node)。
19 J 12070 copy_process 进程19(N4 node)新建后进入就绪队列 --- */
15 J 12084 schedule /* --- 进程15(N1 node)时间片耗完,从运行->就绪(运行了0.15s),让出CPU。
19 R 12084 schedule 进程19(N4 node)开始运行 --- */
19 J 12099 schedule /* --- 进程19(N4 node)时间片耗完,从运行->就绪(运行了0.15s),让出CPU。
18 R 12099 schedule 进程18(N3 node)开始运行 --- */
18 J 12114 schedule /* --- 进程18(N3 node)时间片耗完,从运行->就绪(运行了0.15s),让出CPU。
19 R 12114 schedule 进程19(N4 node)运行 --- */
19 J 12129 schedule /* --- 进程19(N4 node)时间片耗完,从运行->就绪(运行了0.15s),让出CPU。
18 R 12129 schedule 进程18(N3 node)开始运行 --- */
18 J 12144 schedule /* --- 进程18(N3 node)时间片耗完,从运行->就绪(运行了0.15s),让出CPU。
17 R 12144 schedule 进程17(N5 node)运行 --- */
16 J 12159 schedule /* --- 进程16(N2 node)被唤醒。
17 J 12159 schedule 进程17(N5 node)时间片耗完,从运行->就绪(运行了0.15s),让出CPU。
16 R 12159 schedule 进程16(N2 node)运行 --- */