前言
本文讲解系统的进程管理相关内容,系统的进程管理是有关系统的所有进程的调度、排序、分配资源、创建、销毁等,是比较重要的内容。
一、系统的进程的运转方式
1、系统时间:(jiffies 系统滴答)
- CPU 内部有一个 RTC,会在上电的时候调用 mktime 函数算出从 1970 年的 1 月 1 日 0 时开始到当前开机点所过的秒数,给 mktime 函数传来的时间结构体的赋值是由初始化时从 RTC(CMOS)读出的参数,转换为时间存入全局变量中,并且会为 JIFFIES 所用
- JIFFIES 是一个系统的时钟滴答,一个系统滴答是 10ms,定时器
- 10ms 一个系统滴答 ----> 每隔 10ms 会引发一个定时器中断(中断服务函数中进行了 JIFFIES 的自加,在 System_call.s 的 timer_interrupt 中进行自加)
- 调用 do_timer 函数
if (cpl) // CPL 变量是内核中用来指示被中断程序的特权 0:内核进程 3:被中断的是用户进程 current->utime++; // utime:用户程序的运行时间 else current->stime++; // stime:内核程序的运行时间
- next_timer 是嫁接于 jiffies 变量的所有定时器的事件链表
- current->counter -----> 进程的时间片
- task_struct ----> 一个进程;task_struct[] ----> 进程向量表;每个进程都有一个 counter
- counter 在哪里用?
- 进程的调度就是 task_struct[] 进程链表的检索,找时间片最大的那个进程对象(task_struct),然后进行调用,直到时间片为 0,退出,之后再进行新一轮的调用
- counter 在哪里被设置?
- 当全部的 task_struct[] (task[])所有的进程 counter 都为 0,就进行新一轮的时间片分配
- 优先级分配
(*p)->counter = ((*p)->counter >> 1) + (*p)->priority;
- 优先级时间片轮转调度算法
具体详细内容参考Linux内核完全注释:基于0.11内核(修正版V3.0).pdf P299~P300
链接:Linux内核完全注释:基于0.11内核(修正版V3.0).pdf
提取码:ygz8
2、task_struct
- 进程的状态
- 分时技术进行多进程调度
一个进程中由以下几部分组成:
二、如何创建一个新的进程(重要)
- 进程是如何创建的?
- Linux 在初始化的过程中会进行 0 号进程的创建,fork
- main.c
- sched.c—>sched_init—>gdt
- linux系统级别 GDT
- sched_init(); 做了什么事情
- 内核态:不可抢占
- 用户态:可抢占
- move_to_user_mode():把内核状态从内核态切换到用户态
- 在内核初始化的过程中,会手动创建 0 号进程,0 号进程是所有进程的父进程
- 进程的初始化
- 在 0 号进程中:
- 打开标准输入、输出、错误的控制台句柄
- 创建 1 号进程,如果创建成功,则在 1 号进程中
- 首先打开了 “/etc/rc” 文件
- 执行 SHELL 程序 “/bin/sh”
- 0 号进程不可能结束,它会在没有其他进程调用的时候调用,只会执行
for(;;) pause();
- 进程创建
- fork
- 在 task 链表中找一个任务进程空位存放当前的进程
- 创建一个 task_struct
- 设置 task_struct
- 进程的创建就是对 0 号进程或者当前进程的复制
- 0 号进程复制就是结构体的赋值,把 task[0] 对应的 task_struct 复制给新创建的 task_struct
- 对于堆栈的拷贝,当进程做创建的时候要保持复制原有的堆栈
- 进程的创建是系统调用
.align 2 _sys_fork: call _find_empty_process testl %eax,%eax js 1f push %gs pushl %esi pushl %edi pushl %ebp pushl %eax call _copy_process addl $20,%esp 1: ret
- 给当前要创建的进程分配一个进程号。find_empty_process
- 创建一个子进程的 task_struct 结构体:
struct task_struct *p; p = (struct task_struct *) get_free_page();
- 将当前的子进程放入到整体进程链表中
task[nr] = p;
- 设置创建的 task_struct 结构体
p->pid = last_pid; p->father = current->pid; p->counter = p->priority; p->signal = 0; p->alarm = 0; p->leader = 0; /* process leadership doesn't inherit */ p->utime = p->stime = 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; 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); p->tss.trace_bitmap = 0x80000000; 如果当前进程使用了协处理器,那就设置当前创建进程的协处理器 if (last_task_used_math == current) __asm__("clts ; fnsave %0"::"m" (p->tss.i387)); 进行老进程向新进程代码段、数据段(LDT)的拷贝 int copy_mem(int nr,struct task_struct * p) 如果父进程打开了某个文件,那么子进程也同样打开这个文件,所以将文件的打开计数+1 for (i=0; i<NR_OPEN;i++) if (f=p->filp[i]) f->f_count++; 继承父进程相关属性 if (current->pwd) 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)); set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt)); 给程序的状态标志为可运行状态 p->state = TASK_RUNNING; /* do this last, just in case */ 返回新创建进程的 pid return last_pid;
具体详细内容参考Linux内核完全注释:基于0.11内核(修正版V3.0).pdf P242~P252 和 P325~P333
链接:Linux内核完全注释:基于0.11内核(修正版V3.0).pdf
提取码:ygz8
进程被创建到了链表中,如何再进行进一步的调佣和调度?请看下面进程调度!!!
三、进程调度
①、主要函数
void schedule(void)
-------> 进程调度函数
- 进程状态
- 运行状态 ------> 可以被运行,就绪状态,进程切换只有在运行状态
- 可中断睡眠状态 ------> 可以被信号中断,使其变成 RUNNING
- 不可中断睡眠状态 ------> 只能被 wakeup 所唤醒变为 RUNNING
- 暂停状态 ------> 收到 SIGSTOP、SIGTSTP、SIGTTIN
- 僵死状态 ------> 进程停止运行了,但是父进程还没有将其清空
- #define TASK_RUNNING 0
- #define TASK_INTERRUPTIBLE 1
- #define TASK_UNINTERRUPTIBLE 2
- #define TASK_ZOMBIE 3
- #define TASK_STOPPED 4
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; } // 如何该进程为可中断睡眠状态,则如果该进程有非屏蔽信号出现就将进程的状态设置为就绪状态 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) break; // 如果 c 不为空,则当前进程链表中,还有一些进程的时间片没有用完。 // 如果 c 为空,则所有进程的时间片都已经用完了 // 进行时间片的重分配 for(p = &LAST_TASK ; p > &FIRST_TASK ; --p) if (*p) (*p)->counter = ((*p)->counter >> 1) + // 时间片的分配:counter = counter/2 + priority (*p)->priority; }
switch_to()
------> 进程切换函数
- 把进程切换为当前进程
- 1)将需要切换的进程赋值给当前进程指针
- 2)进行进程的上下文切换
- 上下文:程序运行是 CPU 的特殊寄存器、通用寄存器(TSS)等信息+当前堆栈中的信息
- void sleep_on(struct task_struct **p)
- 当某个进程想要访问CPU的资源的时候,碰巧 CPU 资源被占用,那么就会调用SLEEPON 函数,把进程休眠
if (current == &(init_task.task)) panic("task[0] trying to sleep");
②、辅助函数
void show_task(int nr,struct task_struct * p)
------> 打印 p->pid,p->state 打印栈的空闲大小
具体详细内容参考Linux内核完全注释:基于0.11内核(修正版V3.0).pdf P281~P302
链接:Linux内核完全注释:基于0.11内核(修正版V3.0).pdf
提取码:ygz8
四、进程的退出
linux内核代码中以 syscall_、do_xxx 开头的基本上都是中断调用的函数
内核的销毁
- exit 是销毁函数 ------> 一个系统调用 ------> do_exit
- 首先该函数会释放进程的代码段和数据段占用的内存
- 关闭进程打开的所有文件,对当前的目录和 i 节点进行同步(文件操作)
- 如果当前要销毁的进程有子进程,那么就让 1 号进程作为新的父进程(init 进程)
- 如果当前进程是一个会话头进程,则会终止会话中的所有进程
- 改变当前进程的运行状态,变成 TASK_ZOMBIE 僵死状态,并且向其父进程发送 SIGCHLD
- void release(struct task_struct * p)
- 完成清空了任务描述表中的对应进程表项,释放对应的内存页(代码段 数据段 堆栈)
- static inline int send_sig(long sig,struct task_struct * p,int priv)
- 给指定的 p 进程发送对应的 sig 信号
- static void kill_session(void)
- 终止会话,终止当前进程的会话给其发送 SIGHUP
- int sys_kill(int pid,int sig)
- kill- 不是杀死的意思,向对应的进程号或者进程组号发送任何信号
- pid
- pid > 0,给对应的 pid 发送 sig
else if (pid>0) while (--p > &FIRST_TASK) { if (*p && (*p)->pid == pid) if (err=send_sig(sig,*p,0)) retval = err; }
- pid = 0,给当前进程的进程组发送 sig
if (!pid) while (--p > &FIRST_TASK) { if (*p && (*p)->pgrp == current->pid) if (err=send_sig(sig,*p,1)) retval = err; }
- pid = -1,给任何进程发送
else if (pid == -1) while (--p > &FIRST_TASK) if (err = send_sig(sig,*p,0)) retval = err;
- pid < -1,给进程组号为 -pid 的进程组发送信号
else while (--p > &FIRST_TASK) if (*p && (*p)->pgrp == -pid) if (err = send_sig(sig,*p,0)) retval = err;
- static void tell_father(int pid)
- 子进程向父进程发送SIGCHLD信号
- int do_exit(long code)
- current->pid 就是当前关闭的进程
- 首先该函数会释放进程的代码段和数据段占用的内存
free_page_tables(get_base(current->ldt[1]),get_limit(0x0f)); free_page_tables(get_base(current->ldt[2]),get_limit(0x17));
- 关闭进程打开的所有文件,对当前的目录和 i 节点进行同步(文件操作)
for (i=0 ; i<NR_OPEN ; i++) if (current->filp[i]) sys_close(i); iput(current->pwd); current->pwd=NULL; iput(current->root); current->root=NULL; iput(current->executable); current->executable=NULL;
- 如果当前要销毁的进程有子进程,那么就让 1 号进程作为新的父进程(init 进程)
for (i=0 ; i<NR_TASKS ; i++) if (task[i] && task[i]->father == current->pid) { task[i]->father = 1; if (task[i]->state == TASK_ZOMBIE) /* assumption task[1] is always init */ (void) send_sig(SIGCHLD, task[1], 1); }
- 如果使用了终端或者协处理器,那么将终端关闭 协处理器关闭
if (current->leader && current->tty >= 0) tty_table[current->tty].pgrp = 0; if (last_task_used_math == current) last_task_used_math = NULL;
- 如果当前进程是一个会话头进程,则会终止会话中的所有进程
if (current->leader) kill_session();
- 改变当前进程的运行状态,变成TASK ZOMBIE僵死状态,并且向其父进程发送SIGCHLD信号
current->state = TASK_ZOMBIE; tell_father(current->father);
- 重新调度进程
schedule();
- int sys_waitpid(pid_t pid,unsigned long * stat_addr, int options)
- 父进程在运行子进程的时候一般都会运行 wait waitpid 这两个函数(父进程等待某个子进程终止),当父进程收到 SIGCHLD 信号时父进程会终止僵死状态的子进程
- 首先父进程会把子进程的运行时间累加到自己的进程变量中
current->cutime += (*p)->utime; current->cstime += (*p)->stime;
- 把对应的子进程的进程描述结构体进行释放,置空任务数组中的空槽
release(*p);
具体详细内容参考Linux内核完全注释:基于0.11内核(修正版V3.0).pdf P319~P325
链接:Linux内核完全注释:基于0.11内核(修正版V3.0).pdf
提取码:ygz8