Linux
下的多进程编程技术;process.c
:
#include
#include
#include
#include
#include
#define HZ 100
void cpuio_bound(int last, int cpu_time, int io_time);
int main(int argc, char * argv[])
{
pid_t fd_1, fd_2;
int fd;
printf("The Parent Pid = [%d].\n", getpid());
fd_1 = fork();
if(fd_1 == 0)
{
printf("[%d] Is Running Now.\n", getpid());
cpuio_bound(10, 1, 0);
exit(0);
}
fd_2 = fork();
if(fd_2 == 0)
{
printf("[%d] Is Running Now.\n", getpid());
cpuio_bound(10, 1, 0);
exit(0);
}
fd = wait(NULL);
printf("[%d] Have Exited.\n",fd);
fd = wait(NULL);
printf("[%d] Have Exited.\n",fd);
return 0;
}
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)
{
times(&start_time);
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;
sleep_time=0;
while (sleep_time < io_time)
{
sleep(1);
sleep_time++;
}
last -= sleep_time;
}
}
首先修改init
文件夹下的main.c
中的main()
函数,应当在内核启动时就打开log
文件。
打开log
文件的参数指定为只写模式,如果文件已经存在,则清空其现有内容。文件权限设置为所有人可读可写。
在kernel/printk.c
中添加fprintk
函数。
#include
#include
#include
#include "linux/sched.h"
#include "sys/stat.h"
static char logbuf[1024];
static char buf[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;
}
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)
{
__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
{
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;
}
jiffies
在kernel/sched.c
文件中定义为一个全局变量:
long volatile jiffies=0;
其记录了从系统开机到当前时间的时钟中断发生次数,也被称为“滴答数”。
在kernel/sched.c
文件中的sched_init()
函数中,时钟中断处理函数被设置为:
set_intr_gate(0x20,&timer_interrupt);
这表明jiffies
表示了从系统开机时到目前为止的时钟中断次数,即“滴答数”。
此外,在sched_init()
函数中,以下代码用于设置每次时钟中断的间隔,即LATCH
:
outb_p(0x36, 0x43);
outb_p(LATCH&0xff, 0x40);
outb_p(LATCH>>8, 0x40);
三条语句的目的是配置8253定时芯片的工作模式,并设置时钟中断的触发频率。其中,LATCH
是在kernel/sched.c
文件中定义的一个宏:
kernel/sched.c
#define LATCH (1193180/HZ)
include/linux/sched.h
#define HZ 100
需要在所有发生进程状态切换的代码点添加适当的代码,以记录进程状态变化的情况并输出到log文件中。
总体而言,Linux 0.11支持四种主要的进程状态转移:从就绪到运行、从运行到就绪、从运行到睡眠以及从睡眠到就绪。此外,还存在新建和退出两种情况。其中,就绪到运行的状态转移通过schedule()
函数实现(该函数同时涵盖调度算法);运行到睡眠的转移则依赖于sleep_on()
和interruptible_sleep_on()
,以及进程主动休眠的系统调用如sys_pause()
和sys_waitpid()
;而从睡眠到就绪的转移则依赖于wake_up()
。通过在这些函数的适当位置插入处理语句,可以实现对进程运行轨迹的全面跟踪。
在kernel/fork.c
文件中的copy_process()
函数中,修改如下,以在新建态N时输出新建进程信息到文件:
p->start_time = jiffies;
fprintk(3, "%ld\t%c\t%ld\n", p->pid, 'N', jiffies);
kernel/sched.c
:
schedule()
函数:
if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) &&
(*p)->state==TASK_INTERRUPTIBLE)
{
(*p)->state=TASK_RUNNING;
fprintk(3, "%ld\t%c\t%ld\n", (*p)->pid, 'J', jiffies);
}
由于中断信号影响,由可中断的阻塞态W变为就绪态J
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);
sys_pause()
函数:
int sys_pause(void)
{
if(current->state!=TASK_INTERRUPTIBLE )
fprintk(3, "%ld\t%c\t%ld\n", current->pid, 'W', jiffies);
current->state = TASK_INTERRUPTIBLE;
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, "%ld\t%c\t%ld\n", current->pid, 'W', jiffies);
schedule();
if (tmp)
tmp->state=0;
fprintk(3, "%ld\t%c\t%ld\n", tmp->pid, 'J', 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;
fprintk(3, "%ld\t%c\t%ld\n", current->pid, 'W', jiffies);
schedule();
if (*p && *p != current) {
(**p).state=0;
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", tmp->pid, 'J', jiffies);
}
wake_up()
函数:
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;
}
}
修改kernel/exit.c
:
do_exit()
函数:
fprintk(3, "%ld\t%c\t%ld\n", current->pid, 'E', jiffies);
sys_waitpid()
函数:
fprintk(3, "%ld\t%c\t%ld\n", current->pid, 'W', jiffies);
schedule();
在Linux-0.11
下
make all
sudo ./mount-hdc
sudo umount hdc
./run
chmod +x stat_log.py
./stat_log.py process.log 12 13 14 15 -g | more
linux-0.11
进程调度的时间片Linux 0.11
所采用的调度算法是一种综合考虑进程优先级并具有动态反馈机制,能够调整时间片的轮转调度算法。该算法为每个进程分配一个称为时间片的时间段,即该进程被允许运行的时间。如果在时间片结束时进程仍在运行,CPU
将会被剥夺,并分配给另一个进程;而如果进程在时间片结束前阻塞或结束,则CPU
会立即进行切换。调度程序的主要任务是维护一个就绪进程列表,当进程用完它的时间片后,它将被移到队列的末尾。那么综合考虑进程优先级又是什么呢?这意味着一个进程在阻塞队列中停留的时间越长,其优先级就越高,因此下次将被分配更大的时间片。
进程之间的切换是需要时间的。如果时间片设定得太小,将导致频繁的进程切换,从而浪费大量时间在进程切换上,影响系统效率;相反,如果时间片设定得足够大,虽然不会浪费时间在进程切换上,但会降低系统的利用率,且可能对用户交互性产生不良影响。因此,时间片的大小需要在保持CPU利用率和用户交互性之间取得平衡。
为了调整每个进程的时间片,可以通过修改INIT_TASK
宏中的counter
来实现。在这里,counter
代表进程的时间片。通过增加它,可以延长进程被分配CPU的时间。下图中,平均等待时间以及平均完成时间随着时间片的切片数增多而减少 。
在单进程编程中,程序是按照顺序执行的,一个任务完成后才会执行下一个任务。这种方式不涉及到多个任务之间的切换和数据保存,但是在等待I/O时,CPU可能处于空闲状态,导致CPU利用率相对较低。
相比之下,多进程编程允许多个进程交替执行。当一个进程等待I/O时,系统可以切换到另一个就绪的进程执行,从而提高了CPU的利用率。然而,多进程编程涉及到进程切换时的数据保存和复杂的调度机制,这可能导致一些额外的开销和复杂性。
执行方式
数据是否同步
CPU利用率
适用场景
2.你是如何修改时间片的?仅针对样本程序建立的进程,在修改时间片前后, log
文件的统计结果(不包括Graphic)都是什么样?结合你的修改分析一下为什么会这样变化,或者为什么没变化?
通过在sched.h
文件中的INIT_TASK
宏进行时间片的修改。观察到在样本程序建立的进程中,随着时间片的较大幅度修改,并没有引起log
文件统计结果的显著变化。
系统处理能力不变: 关键在于系统处理的进程数量并没有发生改变。即使时间片较大,系统在单位时间内仍能处理相同数量的进程。这导致了吞吐量在一定范围内没有明显的变化。
平均等待时间和平均完成时间的变化: 在一定范围内,随着时间片的增大,平均等待时间和平均完成时间呈下降趋势。这是因为在较小的时间片情况下,CPU
花费更多时间在调度切换上,导致平均等待时间增加。随着时间片的增大,这种等待时间减小。然而,当时间片足够大时,进程调度可能变得类似于先来先服务(FCFS
),平均等待时间和平均完成时间趋于稳定。
调度策略影响: 随着时间片的修改,RR
轮转调度逐渐转变为FCFS
先来先服务。这导致了吞吐量的相对稳定性,因为在单位时间内完成的进程数量受到系统处理能力的限制。