记录一些学习哈工大操作系统实验的学习笔记和心得
从实验材料中我们可以发现,process.c
模板程序已经提供了cupio_bound
函数来模拟各个子程序的运行时间,包括CPU时间、IO时间等
因此,需要实现的就是在process.c
中的main
函数使用fork
和wait
系统调用来创建多个子程序,然后每个子程序执行各自的cpuio_bound
函数来模拟实际情况,父进程等待所有子进程之后再向stdout
来输出所有的子进程id
.
这里给出两版代码,一版是广义情况下调用库函数的代码,另外一版是调用系统调用的一版
process.c :
#include
#include
#include
#include
#include
#include
#include
#include
#define HZ 100
void cpuio_bound(int last, int cpu_time, int io_time);
/*
+ 所有子进程都并行运行,每个子进程的实际运行时间一般不超过 30 秒;
+ 父进程向标准输出打印所有子进程的 id,并在所有子进程都退出后才退出;
fork();
wait();
*/
int main(int argc, char *argv[])
{
/*
*/
int pid_list[5]; // pid list
int pid_index = 0; // pid index/number
int fork_numer = atoi(argv[1]); // 参数1, 参数0为./process
srand(time(NULL)); // init time to get random.
if (argc < 2)
{
printf("argment count is to less!!!\n");
return 0;
}
for (int i = 0; i < fork_numer; i++)
{
int pid = fork();
if (pid == -1)
printf("create fork errr!\n");
if (!pid)
{
// int total_time = rand() % 10 + 1;
int total_time = getpid() % 29 + 1; // time <= 30
printf("I'am %d children fork. total time is %d\n", getpid(), total_time);
cpuio_bound(total_time, i, total_time - i); // total_time cpu_time io_time
exit(EXIT_SUCCESS);
}
else
{
pid_list[pid_index++] = pid;
}
}
/*
wait all children fork exit;
wait() 只会等待任意一个子进程退出,因此需要for循环来等待子进程结束
*/
for (int i = 0; i < fork_numer; i++)
{
int pid = wait(NULL);
printf("I'm father process and I'm end wait, children_pid = %d\n", pid);
}
// output all children fork pid;
for (int i = 0; i < pid_index; i++)
printf("process %d pid is %d.\n", i, pid_list[i]);
return 0;
}
/*
* 此函数按照参数占用CPU和I/O时间
* last: 函数实际占用CPU和I/O的总时间,不含在就绪队列中的时间,>=0是必须的
* cpu_time: 一次连续占用CPU的时间,>=0是必须的
* io_time: 一次I/O消耗的时间,>=0是必须的
* 如果last > cpu_time + io_time,则往复多次占用CPU和I/O
* 所有时间的单位为秒
*/
void cpuio_bound(int last, int cpu_time, int io_time)
{
struct tms start_time, current_time;
clock_t utime, stime;
int sleep_time;
while (last > 0)
{
/* CPU Burst */
times(&start_time);
/* 其实只有t.tms_utime才是真正的CPU时间。但我们是在模拟一个
* 只在用户状态运行的CPU大户,就像“for(;;);”。所以把t.tms_stime
* 加上很合理。*/
do
{
times(¤t_time);
utime = current_time.tms_utime - start_time.tms_utime;
stime = current_time.tms_stime - start_time.tms_stime;
} while (((utime + stime) / HZ) < cpu_time);
last -= cpu_time;
if (last <= 0)
break;
/* IO Burst */
/* 用sleep(1)模拟1秒钟的I/O操作 */
sleep_time = 0;
while (sleep_time < io_time)
{
sleep(1);
sleep_time++;
}
last -= sleep_time;
}
}
process2.c
#include
#include
int main(int argc, char * argv[])
{
int id = fork();
if(!id) {
printf("id == %d\n", id);
printf("I am child process, my id is [%d] and my parent process is [%d].\n", getpid(), getppid());
}
return 0;
}
观察init/main.c
代码可以发现,main
函数在执行完所有的初始化后使用fork()
来创建进程1,并在进程1中执行init
函数,而原本的父进程0则是不断执行pause()
系统调用
在init
函数中,进程首先会初始化文件描述符,并分别将0,1,2绑定至stdin stdout stderror
,因此我们可以在这里将文件描述符3绑定至我们的log
文件
void init(void)
{
int pid, i;
setup((void *)&drive_info);
(void)open("/dev/tty0", O_RDWR, 0); // stdin 0
(void)dup(0); // stdout 1
(void)dup(0); // stderror 2
(void)open("/var/process.log", O_CREAT | O_TRUNC | O_WRONLY, 0666); // log 3
// ....
}
接下来为了让log
文件开启的时间提前,记录所有进程的状态,我们将上述代码移动至内核开启代码后,即move_to_user
之后
move_to_user_mode();
// 加载文件系统
setup((void *)&drive_info);
(void)open("/dev/tty0", O_RDWR, 0); // stdin 0
(void)dup(0); // stdout 1
(void)dup(0); // stderror 2
(void)open("/var/process.log", O_CREAT | O_TRUNC | O_WRONLY, 0666); // log 3
if (!fork())
{ /* we count on this going ok */
init();
}
至此,log
文件就已经开启成功
实验楼环境为我们提供了写log
文件的函数,直接将它复制到kernel/printk.c
中即可,以后我们使用fprink()
函数并加上对应的文件描述符即可实现对log
文件日志的写入
#include
#include
#include
#include "linux/sched.h"
#include "sys/stat.h"
static char buf[1024];
static char logbuf[1024];
extern int
vsprintf(char *buf, const char *fmt, va_list args);
int printk(const char *fmt, ...)
{
va_list args;
int i;
va_start(args, fmt);
i = vsprintf(buf, fmt, args);
va_end(args);
__asm__("push %%fs\n\t"
"push %%ds\n\t"
"pop %%fs\n\t"
"pushl %0\n\t"
"pushl $buf\n\t"
"pushl $0\n\t"
"call tty_write\n\t"
"addl $8,%%esp\n\t"
"popl %0\n\t"
"pop %%fs" ::"r"(i) : "ax", "cx", "dx");
return i;
}
// write()实现
// 写入Log
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;
}
/*
// 向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
是系统滴答数,它代表了系统从开机到现在为止经过的滴答数,在sched_init()
中我们也可以发现时钟处理函数被初始化为time_interuupt
,其中每次执行该函数都会让jiffies
的值加一
下面这部分代码用于设置每次时钟中断的间隔LATCH
// 设置8253模式
outb_p(0x36, 0x43);
outb_p(LATCH&0xff, 0x40);
outb_p(LATCH>>8, 0x40);
linux0.11环境下的jiffies
为10ms
为了在合适的地方记录状态的变化,并将其写入日志之中,我们需要考虑一下几种情况
了解到可能存在的状态变化之后,我们只需要在相对应的代码位置进行记录即可,主要修改的函数包括:
schedule()
sleep_on()
和interruptible_sleep_on()
sys_pause()
和sys_waitpid()
wake_up()
这里给出一部分参考代码,具体的可以查看dev3
分支
/*
schedule()部分
*/
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;
}
if (c) // 找到counter最大的进程并且是就绪态
break;
for (p = &LAST_TASK; p > &FIRST_TASK; --p)
if (*p)
(*p)->counter = ((*p)->counter >> 1) +
(*p)->priority;
}
// switch_to 切换进程,但是由于这里是通过汇编进行切换,因此需要提前记录
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);
在上面我们需要关注的点是,每次记录之前需要使用if()
进行判断,是否状态真的发生了改变,避免重复的记录
其中进程0即父进程在系统无事可做的时候,会不断调用pause()
系统调用,以激活调度算法,此时它可以是等待态,也可以是运行态,因为它是唯一一个在CPU上运行的程序
bochs
之前记得使用sync
刷新缓存mount
来挂载文件,将process.log
文件拷贝到主机环境,方便处理阅读这里给出进行测试的步骤
process.c
程序上传到linux0.11 root/
中linux0.11
中编译运行process.c
程序linux0.11
中的process.log
拷贝至主机环境stat_log.py
对process.log
进行测试chmod +x ./stat_log.py
./stat_log.py process.log 1 2 3 4 # 只统计pid为1 2 3 4的进程
./stat_log.py process.log # 统计所有进程
根据实验指导,我们就可以发现,nice
系统调用不会执行,只有scdule.h
中的INIT_TASK
宏会修改state counter priority
,因此我们直接在这里修改即可完成对时间片的修改
/*scdele.h*/
#define INIT_TASK \
{ 0,15,15,
// 上述三个值分别对应 state、counter 和 priority;
当就绪进程counter
为0的时候,会被更新成初始priority
的值
结合自己的体会,谈谈从程序设计者的角度看,单进程编程和多进程编程最大的区别是什么?
你是如何修改时间片的?仅针对样本程序建立的进程,在修改时间片前后,log 文件的统计结果(不包括 Graphic)都是什么样?结合你的修改分析一下为什么会这样变化,或者为什么没变化?