进程状态

进程的5种状态:创建,就绪,运行,阻塞,完成

在用户编写程序使用fork()函数可以创建子进程

1
2
pid_t pid;
pid = fork();

进程状态转移图

用户是调用了内核中fork.c中的函数(linux-0.11/kernel/fork.c),其中主要copy_process()函数起作用(复制进程)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
/*
* OK,下面是主要的fork 子程序。它复制系统进程信息(task[n])并且设置必要的寄存器。
* 它还整个地复制数据段。
*/
// 复制进程。
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;
struct i387_struct *p_i387;
//**开始创建进程**
p = (struct task_struct *) get_free_page (); // 为新任务数据结构分配内存。
if (!p) // 如果内存分配出错,则返回出错码并退出。
return -EAGAIN;
task[nr] = p; // 将新任务结构指针放入任务数组中。
// 其中nr 为任务号,由前面find_empty_process()返回。
*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;
p->signal = 0; // 信号位图置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; // 当前滴答数时间。
// 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;
// 如果当前任务使用了协处理器,就保存其上下文。
p_i387 = &p->tss.i387;
if (last_task_used_math == current)
_asm{
mov ebx, p_i387
clts
fnsave [p_i387]
}
// __asm__ ("clts ; fnsave %0"::"m" (p->tss.i387));
// 设置新任务的代码和数据段基址、限长并复制页表。如果出错(返回值不是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; /* do this last, just in case */
/* 最后再将新任务设置成可运行状态,以防万一 */
return last_pid; // 返回新进程号(与任务号是不同的)。
}

创建 ——> 就绪

现在进程在就绪队列中等待分配CPU资源运行。

当一个进程在CPU结束运行后如何在就绪队列中选择进程运行呢?

  1. 根据等待的顺序来选择
  2. 根据优先级高低来选择
  3. 根据短任务优先(分配的时间片短)

在Linux0.11中 linux-0.11/kernel/sched.c调度算法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/*
* 'schedule()'是调度函数。这是个很好的代码!没有任何理由对它进行修改,因为它可以在所有的
* 环境下工作(比如能够对IO-边界处理很好的响应等)。只有一件事值得留意,那就是这里的信号
* 处理代码。
* 注意!!任务0 是个闲置('idle')任务,只有当没有其它任务可以运行时才调用它。它不能被杀
* 死,也不能睡眠。任务0 中的状态信息'state'是从来不用的。
*/
void schedule (void)
{
int i, next, c;
struct task_struct **p; // 任务结构指针的指针。

/* 检测alarm(进程的报警定时值),唤醒任何已得到信号的可中断任务 */

// 从任务数组中最后一个任务开始检测alarm。
for (p = &LAST_TASK; p > &FIRST_TASK; --p)
if (*p)
{
// 如果任务的alarm 时间已经过期(alarm<jiffies),则在信号位图中置SIGALRM 信号,然后清alarm。
// jiffies 是系统从开机开始算起的滴答数(10ms/滴答)。定义在sched.h 第139 行。
if ((*p)->alarm && (*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;//进程的数量 64
p = &task[NR_TASKS];
// 这段代码也是从任务数组的最后一个任务开始循环处理,并跳过不含任务的数组槽。比较每个就绪
// 状态任务的counter(任务运行时间的递减滴答计数)值,哪一个值大,运行时间还不长,next 就
// 指向哪个的任务号。
while (--i)
{
if (!*--p)
continue;
//**counter是时间片也是优先等级,时间片越大优先级就越高
//找出counter最大的一个进程,最为下一个运行的程序**
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 的任务,并运行之。
}

注意到这里的counter和priority在Linux0.11设置为15,15的

就绪态 ——> 运行态

当时间片使用完后,该进程还没有执行完需要再分配时间片。

也就是上述代码所示:目前的进程在与下一个进程切换,目前的进程只在时钟中断、int0x80中断和进程结束才会切换到下一个进程。而在schedule()函数中只涉及时间片,则判断当前进程的时间片使用完了。如果当前的进程还需要运行则把运行态 ——> 就绪态,等待下一次的调用。而下一个进程从就绪态 ——> 运行态。

一个进程除了在CPU在执行任务,也会读取磁盘上的信息,这个时候进程就会释放CPU的资源,让给其他的进程。这个时候该进程就阻塞了,相对与CPU来说。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//// **pause()系统调用**。转换当前任务的状态为可中断的等待状态,并重新调度。
// 该系统调用将导致进程进入睡眠状态,直到收到一个信号。该信号用于终止进程或者使进程调用
// 一个信号捕获函数。只有当捕获了一个信号,并且信号捕获处理函数返回,pause()才会返回。
// 此时pause()返回值应该是-1,并且errno 被置为EINTR。这里还没有完全实现(直到0.95 版)。
int sys_pause(void)
{
//把当前的进程的状态设置为阻塞态
current->state = TASK_INTERRUPTIBLE;
if(current->pid != 0) {
//fprintk(3,"%d\tW\t%d\n",current->pid,jiffies);
}
//切换进程使用CPU
schedule();
return 0;
}

当一个运行的进程可能发生不可中断睡眠状态和可中断睡眠状态时,会释放CPU资源,则进程状态:运行态 ——> 阻塞态。之后调用shedule()函数把CPU资源给下一个进程使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// 把当前任务置为不可中断的等待状态,并让睡眠队列头的指针指向当前任务。
// 只有明确地唤醒时才会返回。该函数提供了进程与中断处理程序之间的同步机制。
// sleep_on()自愿地放弃CPU的使用权
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;// 让tmp 指向已经在等待队列上的任务(如果有的话)
*p = current;// 将睡眠队列头的等待指针指向当前任务。
//不可中断睡眠 (通常是在IO操作) 收到信号不唤醒和不可运行,
//不可中断,指的并不是CPU不响应外部硬件的中断,而是指进程不响应异步信号。
//只有被使用wake_up()函数明确唤醒时才能转换到可运行的就绪状态**
current->state = TASK_UNINTERRUPTIBLE;
//fprintk(3,"%d\tW\t%d\n",current->pid,jiffies);
schedule();
// 唤醒队列中的上一个(tmp)睡眠进程。0 换作 TASK_RUNNING 更好
/* 依次唤醒,所以不可中断的睡眠进程一定是按严格从“队列”(一个依靠
* 放在进程内核栈中的指针变量tmp维护的队列)的首部进行唤醒。而对于可
* 中断的进程,除了用wake_up唤醒以外,也可以用信号(给进程发送一个信
* 号,实际上就是将进程PCB中维护的一个向量的某一位置位,进程需要在合
* 适的时候处理这一位。感兴趣的实验者可以阅读有关代码)来唤醒
*/**
if (tmp) {
tmp->state=0;
//fprintk(3,"%d\tJ\t%d\n",tmp->pid,jiffies);
}
}

// 将当前任务置为可中断的等待状态,并放入*p 指定的等待队列中。参见列表后对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;
schedule ();
// 如果等待队列中还有等待任务,并且队列头指针所指向的任务不是当前任务时,则将该等待任务置为
// 可运行的就绪状态,并重新执行调度程序。当指针*p 所指向的不是当前任务时,表示在当前任务被放
// 入队列后,又有新的任务被插入等待队列中,因此,既然本任务是可中断的,就应该首先执行所有
// 其它的等待任务。
if (*p && *p != current)
{
(**p).state = 0;
goto repeat;
}
// 下面一句代码有误,应该是*p = tmp,让队列头指针指向其余等待任务,否则在当前任务之前插入
// 等待队列的任务均被抹掉了。参见图4.3。
// 唤醒队列中的上一个(tmp)睡眠进程。0 换作 TASK_RUNNING 更好**
// 在记录进程被唤醒时一定要考虑到这种情况,实验者一定要注意!!!
*p = NULL;
if (tmp)
tmp->state = 0;
}

指针变化

在阻塞态的进程等待除CPU资源外的资源,资源到了后需要唤醒进程

1
2
3
4
5
6
7
8
9
// 唤醒指定任务*p。
void wake_up (struct task_struct **p)
{
if (p && *p)
{
(**p).state = 0; // 置为就绪(可运行)状态。
*p = NULL;
}
}

这时就把阻塞态 ——> 就绪态

比如父进程等待子进程都结束后才开始运行,该父进程就存在调用系统等待

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
int sys_waitpid(pid_t pid,unsigned long * stat_addr, int options)
{
int flag, code;
struct task_struct ** p;

verify_area(stat_addr,4);
repeat:
flag=0;
for(p = &LAST_TASK ; p > &FIRST_TASK ; --p) {
if (!*p || *p == current)
continue;
if ((*p)->father != current->pid)
continue;
if (pid>0) {
if ((*p)->pid != pid)
continue;
} else if (!pid) {
if ((*p)->pgrp != current->pgrp)
continue;
} else if (pid != -1) {
if ((*p)->pgrp != -pid)
continue;
}
switch ((*p)->state) {
case TASK_STOPPED:
if (!(options & WUNTRACED))
continue;
put_fs_long(0x7f,stat_addr);
return (*p)->pid;
case TASK_ZOMBIE:
current->cutime += (*p)->utime;
current->cstime += (*p)->stime;
flag = (*p)->pid;
code = (*p)->exit_code;
release(*p);
put_fs_long(code,stat_addr);
return flag;
default:
flag=1;
continue;
}
}
if (flag) {
if (options & WNOHANG)
return 0;
//设置成可中断睡眠状态
// 父进程受到子进程执行完后的信号这就变成就绪态**
current->state=TASK_INTERRUPTIBLE;
fprintk(3,"%d\tW\t%d\n",current->pid,jiffies);
schedule();
if (!(current->signal &= ~(1<<(SIGCHLD-1))))
goto repeat;
else
return -EINTR;
}
return -ECHILD;
}

运行态 ——> 结束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
int do_exit(long code)
{
int i;
free_page_tables(get_base(current->ldt[1]),get_limit(0x0f));
free_page_tables(get_base(current->ldt[2]),get_limit(0x17));
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);
}
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;
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();
**current->state = TASK_ZOMBIE;**
fprintk(3,"%d\tE\t%d\n",current->pid,jiffies);
current->exit_code = code;
tell_father(current->father);
schedule();
return (-1); /* just to suppress warnings */
}

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!