栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Python

Linux0.11中关于进程管理的部分

Python 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Linux0.11中关于进程管理的部分

Linux0.11中关于进程管理的部分

GDT初始化
在setup.s 132行:

lgdt	gdt_48		

lgdt m指令:m源操作数指定了6字节的内存位置,它包含全局描述表(GDT)的基址与限长,将m所在内存位置的内容加载到gdtr寄存器中。

gdtr寄存器的结构如图所示:

其中:

gdt_48:
	.word	0x800		
	.word	512+gdt,0x9	

gdt_48中的内容:
0x800表示gdt的限长为2048(256个表项,每个表项8字节)
512+gdt,0x9表示gdt表的基址(0x0009<<16 + 0x200 + gdt),即0x90200 + gdt(即在本程序段中的偏移)

gdt:
	.word	0,0,0,0		! dummy

	.word	0x07FF		! 8Mb - limit=2047 (2048*4096=8Mb)
	.word	0x0000		! base address=0
	.word	0x9A00		! code read/exec
	.word	0x00C0		! granularity=4096, 386

	.word	0x07FF		! 8Mb - limit=2047 (2048*4096=8Mb)
	.word	0x0000		! base address=0
	.word	0x9200		! data read/write
	.word	0x00C0		! granularity=4096, 386

gdt中存储的段描述符结构如图所示:

gdt中的内容初始化为:

第一个表项:
0x0000 0000
0x0000 0000
不使用

第二个表项:
0x00C0 9A00
0x0000 07FF
对应的base为 0x0000 0000
第三个表项:
0x00C0 9200
0x0000 07FF

这里的GDT只是为了暂时讲程序跳转到地址为0x0000 0000的地方执行而建立的,之后的jmpi 0,8指令就是通过查询临时建立的gdt来执行跳转的。

当程序跳转到0x0000 0000时,即head.s在内存中的位置,通过以下代码设置全新的全局描述符表:

call setup_gdt

....

setup_gdt:
	lgdt gdt_descr
	ret

...

gdt_descr:
	.word 256*8-1		
	.long gdt

...

gdt:.quad 0x0000000000000000	
	.quad 0x00c09a0000000fff	
	.quad 0x00c0920000000fff	
	.quad 0x0000000000000000	
	.fill 252,8,0				

这里的分析与上面一样。

此时gdt的初始化已经完成。

TASK0的初始化
关于进程0的定义,可以看一下:task0的定义

task_struct的结构:

struct task_struct
{

  long state;			
  long counter;
  long priority;
  long signal;
  struct sigaction sigaction[32];
  long blocked;			

  int exit_code;
  unsigned long start_code, end_code, end_data, brk, start_stack;
  long pid, father, pgrp, session, leader;
  unsigned short uid, euid, suid;
  unsigned short gid, egid, sgid;
  long alarm;
  long utime, stime, cutime, cstime, start_time;
  unsigned short used_math;

  int tty;			
  unsigned short umask;
  struct m_inode *pwd;
  struct m_inode *root;
  struct m_inode *executable;
  unsigned long close_on_exec;
  struct file *filp[NR_OPEN];

  struct desc_struct ldt[3];

  struct tss_struct tss;
};

在kernel/sched.c中的void sched_init(void) 中完成进程0的初始化,将进程0的tss与ldt加入gdt中:

void
sched_init (void)
{

  // 设置初始任务(任务0)的任务状态段描述符和局部数据表描述符(include/asm/system.h,65)。
  set_tss_desc (gdt + FIRST_TSS_ENTRY, &(init_task.task.tss));
  set_ldt_desc (gdt + FIRST_LDT_ENTRY, &(init_task.task.ldt));
  
	...
	
  ltr (0);			// 将任务0 的TSS 加载到任务寄存器tr。
  lldt (0);			// 将局部描述符表加载到局部描述符表寄存器。
  // 注意!!是将GDT 中相应LDT 描述符的选择符加载到ldtr。只明确加载这一次,以后新任务
  // LDT 的加载,是CPU 根据TSS 中的LDT 项自动加载。
}

 在全局表中设置任务状态段描述符。
// n - 是该描述符的指针;addr - 是描述符中的基地址值。任务状态段描述符的类型是0x89。
#define set_tss_desc(n,addr) _set_tssldt_desc(((char *) (n)),addr, "0x89")
 在全局表中设置局部表描述符。
// n - 是该描述符的指针;addr - 是描述符中的基地址值。局部表描述符的类型是0x82。
#define set_ldt_desc(n,addr) _set_tssldt_desc(((char *) (n)),addr, "0x82")

#define _set_tssldt_desc(n,addr,type) 
__asm__ ( "movw $104,%1nt" 	// 将TSS 长度放入描述符长度域(第0-1 字节)。
"movw %%ax,%2nt" 		// 将基地址的低字放入描述符第2-3 字节。
  "rorl $16,%%eaxnt" 	// 将基地址高字移入ax 中。
  "movb %%al,%3nt" 		// 将基地址高字中低字节移入描述符第4 字节。
  "movb $" type ",%4nt" 	// 将标志类型字节移入描述符的第5 字节。
  "movb $0x00,%5nt" 		// 描述符的第6 字节置0。
  "movb %%ah,%6nt" 		// 将基地址高字中高字节移入描述符第7 字节。
  "rorl $16,%%eax" 		// eax 清零。
  ::"a" (addr), "m" (*(n)), "m" (*(n + 2)), "m" (*(n + 4)),
  "m" (*(n + 5)), "m" (*(n + 6)), "m" (*(n + 7)))

_set_tssldt_desc()中,
n --在全局表中描述符项n 所对应的地址;
addr – 状态段/局部表所在内存的基地址。
type - 描述符中的标志类型字节。
%0 - eax(地址addr);
%1 - (描述符项n 的地址);
%2 - (描述符项n 的地址偏移2 处);
%3 - (描述符项n 的地址偏移4 处);
%4 - (描述符项n 的地址偏移5 处);
%5 - (描述符项n 的地址偏移6 处);
%6 - (描述符项n 的地址偏移7 处);

在GDT中设置完ldt和tss后,将tss描述符选择子加载到tr寄存器中,ldt描述符选择子加载到ldtr中。

// 宏定义,加载第n 个任务的任务寄存器tr。
#define ltr(n) __asm__( "ltr %%ax":: "a" (_TSS(n)))
// 宏定义,加载第n 个任务的局部描述符表寄存器ldtr。
#define lldt(n) __asm__( "lldt %%ax":: "a" (_LDT(n)))		

现在就完成了进程0的加载。

进程的创建
通过fork()来调用系统调用sys_fork:

#### sys_fork()调用,用于创建子进程,是system_call 功能2。原形在include/linux/sys.h 中。
# 首先调用C 函数find_empty_process(),取得一个进程号pid。若返回负数则说明目前任务数组
# 已满。然后调用copy_process()复制进程。
.align 2
sys_fork:
call find_empty_process # 调用find_empty_process()(kernel/fork.c,135)。
testl %eax,%eax
js 1f
push %gs
pushl %esi
pushl %edi
pushl %ebp
pushl %eax
call copy_process # 调用C 函数copy_process()(kernel/fork.c,68)。
addl $20,%esp # 丢弃这里所有压栈内容。
1: ret

然后使用copy_process来复制进程,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)
{
  struct task_struct *p;
  int i;
  struct file *f;

  p = (struct task_struct *) get_free_page ();	// 为新任务数据结构分配内存。
  if (!p)			// 如果内存分配出错,则返回出错码并退出。
    return -EAGAIN;
  task[nr] = p;			// 将新任务结构指针放入任务数组中。
// 其中nr 为任务号,由前面find_empty_process()返回。
  *p = *current;		
 (只复制当前进程内容)。
    p->state = TASK_UNINTERRUPTIBLE;	// 将新进程的状态先置为不可中断等待状态。
  p->pid = last_pid;		// 新进程号。由前面调用find_empty_process()得到。
  p->father = current->pid;	// 设置父进程号。
  p->counter = p->priority;
  p->signal = 0;		// 信号位图置0。
  p->alarm = 0;
  p->leader = 0;		

  p->utime = p->stime = 0;	// 初始化用户态时间和核心态时间。
  p->cutime = p->cstime = 0;	// 初始化子进程用户态和核心态时间。
  p->start_time = jiffies;	// 当前滴答数时间。
// 以下设置任务状态段TSS 所需的数据(参见列表后说明)。
  p->tss.back_link = 0;
  p->tss.esp0 = PAGE_SIZE + (long) p;	// 堆栈指针(由于是给任务结构p 分配了1 页
// 新内存,所以此时esp0 正好指向该页顶端)。
  p->tss.ss0 = 0x10;		// 堆栈段选择符(内核数据段)[??]。
  p->tss.eip = eip;		// 指令代码指针。
  p->tss.eflags = eflags;	// 标志寄存器。
  p->tss.eax = 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;	// 段寄存器仅16 位有效。
  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);	// 该新任务nr 的局部描述符表选择符(LDT 的描述符在GDT 中)。
  p->tss.trace_bitmap = 0x80000000;


// 设置新任务的代码和数据段基址、限长并复制页表。如果出错(返回值不是0),则复位任务数组中
// 相应项并释放为该新任务分配的内存页。
  if (copy_mem (nr, p))
    {				// 返回不为0 表示出错。
      task[nr] = NULL;
      free_page ((long) p);
      return -EAGAIN;
    }
// 如果父进程中有文件是打开的,则将对应文件的打开次数增1。
  for (i = 0; i < NR_OPEN; i++)
    if (f = p->filp[i])
      f->f_count++;
// 将当前进程(父进程)的pwd, root 和executable 引用次数均增1。
  if (current->pwd)
    current->pwd->i_count++;
  if (current->root)
    current->root->i_count++;
  if (current->executable)
    current->executable->i_count++;
// 在GDT 中设置新任务的TSS 和LDT 描述符项,数据从task 结构中取。
// 在任务切换时,任务寄存器tr 由CPU 自动加载。
  set_tss_desc (gdt + (nr << 1) + FIRST_TSS_ENTRY, &(p->tss));
  set_ldt_desc (gdt + (nr << 1) + FIRST_LDT_ENTRY, &(p->ldt));
  p->state = TASK_RUNNING;	

  return last_pid;		// 返回新进程号(与任务号是不同的)。
}

在刚开始复制进程的时候,暂时不需要分配物理内存,因为子进程是从父进程复制过来的,他们指向同样的物理内存,所以可以只建立子进程的页表。

通过copy_mem(int nr, task_struct* p)来实现,具体见 内存管理

在完成了页表的建立以后,就将子进程的TSS和LDT设置到GDT中,
完成子进程建立以后,返回子进程的进程号。

进程的调度
主要通过kernel/sched.c中的schedule(void)函数:

void
schedule (void)
{
  int i, next, c;
  struct task_struct **p;	// 任务结构指针的指针。

  

  // 从任务数组中最后一个任务开始检测alarm。
  for (p = &LAST_TASK; p > &FIRST_TASK; --p)
    if (*p)
      {
	// 如果任务的alarm 时间已经过期(alarmalarm && (*p)->alarm < jiffies)
	  {
	    (*p)->signal |= (1 << (SIGALRM - 1));
	    (*p)->alarm = 0;
	  }
	// 如果信号位图中除被阻塞的信号外还有其它信号,并且任务处于可中断状态,则置任务为就绪状态。
	// 其中'~(_BLOCKABLE & (*p)->blocked)'用于忽略被阻塞的信号,但SIGKILL 和SIGSTOP 不能被阻塞。
	if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) &&
	    (*p)->state == TASK_INTERRUPTIBLE)
	  (*p)->state = TASK_RUNNING;	//置为就绪(可执行)状态。
      }

  
  

  while (1)
    {
      c = -1;
      next = 0;
      i = NR_TASKS;
      p = &task[NR_TASKS];
      // 这段代码也是从任务数组的最后一个任务开始循环处理,并跳过不含任务的数组槽。比较每个就绪
      // 状态任务的counter(任务运行时间的递减滴答计数)值,哪一个值大,运行时间还不长,next 就
      // 指向哪个的任务号。
      while (--i)
	{
	  if (!*--p)
	    continue;
	  if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
	    c = (*p)->counter, next = i;
	}
      // 如果比较得出有counter 值大于0 的结果,则退出124 行开始的循环,执行任务切换(141 行)。
      if (c)
	break;
      // 否则就根据每个任务的优先权值,更新每一个任务的counter 值,然后回到125 行重新比较。
      // counter 值的计算方式为counter = counter /2 + priority。[右边counter=0??]
      for (p = &LAST_TASK; p > &FIRST_TASK; --p)
	if (*p)
	  (*p)->counter = ((*p)->counter >> 1) + (*p)->priority;
    }
  switch_to (next);		// 切换到任务号为next 的任务,并运行之。
}

通过

while(1)
{
...
	while(--i)
	{
	...
	}
}

来找到task[NR_TASK]中时间片count最大的进程进行调度,如果所有时间片都为0,则按照每个进程的优先级,给每一个进程分配时间片:count/2 + priority

在选出时间片count最大的进程以后,通过switch_to(next)来调度进程。.

#define switch_to(n) {
struct {long a,b;} __tmp; 
__asm__( "cmpl %%ecx,_currentnt" 	// 任务n 是当前任务吗?(current ==task[n]?)
  "je 1fnt" 			// 是,则什么都不做,退出。
  "movw %%dx,%1nt" 		// 将新任务的选择符??*&__tmp.b。
  "xchgl %%ecx,_currentnt" 	// current = task[n];ecx = 被切换出的任务。
  "ljmp %0nt" 		// 执行长跳转至*&__tmp,造成任务切换。
// 在任务切换回来后才会继续执行下面的语句。
  "cmpl %%ecx,_last_task_used_mathnt" 	// 新任务上次使用过协处理器吗?
  "jne 1fnt" 		// 没有则跳转,退出。
  "cltsn" 			// 新任务上次使用过协处理器,则清cr0 的TS 标志。
  "1:"::"m" (*&__tmp.a), "m" (*&__tmp.b),
  "d" (_TSS (n)), "c" ((long) task[n]));
}

其中:

#define _TSS(n) ((((unsigned long) n)<<4)+(FIRST_TSS_ENTRY<<3))

GDT表中每个进程对应有1个TSS的描述符和1个LDT的描述符,每个描述符长度是8个字节,所以n*16,即n<<4,又因为第0个进程在GDT表中第四个位置开始,所以FIRST_TSS_ENTRY定义为4,进程0的起始地址为FIRST_TSS_ENTRY << 3,第n个进程的TSS描述符的选择子为((unsigned long) n) << 4) + (FIRST_TSS_ENTRY << 3)。

选择子结构:

将TSS(n)装在到dx寄存器中,将dx的内容放置到结构体tmp中32位长整数b的前16位,现在 64 位 tmp 中的内容是前 32 位为空,这个 32 位数字是段内偏移,接下来的 16 位是 n * 16 + 4 * 8,这个数字是段选择子,再接下来的 16 位也为空。
所以swith_to 的核心实际上就是 ljmp 空, n*16+4*8

ljmp指令实现了:
1.将当前CPU中寄存器的值存入当前TR(任务寄存器)指向的TSS描述符指向的TSS段,
2.将ljmp的操作数,即TSS描述符选择子,指向的目标TSS描述符指向的TSS段的寄存器内容放入CPU,并且将该操作数存入TR寄存器。

如图所示:

这时进程就完成了切换。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/990356.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号