您的位置:首页 > 运维架构 > Linux

内核笔记2-Linux内核进程控制

2016-05-22 18:13 609 查看
本文主要来自于赵炯先生编著的《Linux内核完全注释》一书

Linux内核进程控制

程序是一个可执行文件,而进程是程序执行的一个实例。

进程由以下三部分组成:

1.可执行的代码

2.数据

3.堆栈区

利用分时技术,Linux系统可以同时运行多个进程。同一时刻,一颗CPU只能运行一个进程,内核通过调度程序分时调度运行各个进程。

对于0.11内核,最多同时存在64个进程。除了第一个进程是由OS手动创建,其他的进程都是由父进程调用fork()函数创建出来。进程由PID来标示。每个进程只能执行自己的代码和访问自己的数据,进程间通信需要通过系统调用来执行。

同一个进程可以运行在内核态或用户态,所以Linux的内核态堆栈和用户态堆栈是分开的。

进程控制块PCB(任务数据结构)

在Linux系统中,内核通过进程表管理进程,而进程表中的每一项都是一个指向task_struct类型的指针,即任务数据结构。

任务数据结构task_struct被定义在头文件sched.h中,下面是task_struct的代码段:

struct task_struct {
/* these are hardcoded - don't touch */
long state; /* -1 unrunnable, 0 runnable, >0 stopped 进程状态*/
long counter;
long priority;
long signal;
struct sigaction sigaction[32];
long blocked;   /* bitmap of masked signals */
/* various fields */
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;
/* file system info */
int tty;        /* -1 if no tty, so it must be signed */
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];
/* ldt for this task 0 - zero 1 - cs 2 - ds&ss */
struct desc_struct ldt[3];
/* tss for this task */
struct tss_struct tss;
};


task_struct主要包括进程当前运行的状态信息、信号、PID、父进程PID、运行时间累计值、正在使用的文件和任务的局部描述符以及任务状态段信息

进程上下文:当一个进程运行时,cpu中寄存器的值、堆栈内容和进程状态都被称之为进程上下文。当内核需要切换到另一个进程时,必须保存前进程的上下文来确保下次返回时该进程能够执行。

进程运行状态

Linux中进程在其生命周期中可处于一组不同的状态:



1.运行状态running(可以在用户态运行也可在内核态运行)

正在被cpu执行或准备就绪随时可由调度程序执行


2. 可中断睡眠状态interruptible

发生以下情况时,进程可由interruptible转到就绪态:
-系统产生了一个中断
- 进程正在等待的资源被释放
- 进程收到一个信号


3.不可中断睡眠状

处于该状态的进程只有在被wake_up()函数明确唤醒才能转换为就绪态。


4.暂停状态

当进程收到信号SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU时就会进入暂停状态。可向其发送SIGCONT信号让进程转换到可运行状态。


5.僵死状态

进程已经停止,父进程依旧没有询问其状态。


当需要时,一个进程可以主动调用sleep_on()或interruptible_sleep_on()函数来释放cpu使用权,进入睡眠状态。

在内核态运行的进程不能被抢占,只能自己释放cpu使用权。

进程初始化

当boot/目录中的引导程序把内核从磁盘加载到内存之后,就开始执行系统初始化程序init/main.c。该程序首先确定如何分配物理内存,然后调用内核各部分初始化函数来对内存管理、块设备、字符设备、中断处理、进程调度以及软硬盘进行初始化。 (关于main.c程序后续应该写篇博文好好研究一下)

完成这些操作之后系统就可以运行了,程序首先把自己手动放到进程0(任务0)当中运行,然后调用fork()函数来创建进程1。在进程1中继续初始化各自应用的环境并且启用shell登录程序。进程0会在cpu空闲时被执行,此时任务0仅执行pause()函数。

需要注意的是宏move_to_user_mode在这个过程中的作用:

宏move_to_user_mode实现了程序把自己手动放到进程0中这一功能,并且把运行特权级从内核态0级改为用户态3级,仍然执行原先的代码指令流。

创建新进程

Linux中所有进程都通过fork()系统调用,所有进程都是通过复制进程0得到的,都是进程0的子进程。

创建新进程的过程:

在任务数组中找出一个还没有被任何进程使用的空项,0.11版本的内核的任务数组只有64个项,现在的Linux内核可存放的进程数已经远远大于这个量级。

在主内存区中为新建进程申请一页内存来存放任务数据结构信息,并将当前进程的任务数据结构(PCB)复制过来

将新进程状态置为不可中断睡眠状态来防止新建进程被调度函数执行

修改复制过来的任务数据结构,把当前进程设置为新进程的父进程,清除信号位,复位新进程各统计值,设置新进程的初始运行时间片为15个系统滴答

根据当前进程设置任务状态段(TSS)中各寄存器的值。Intel80386程序员参考手册-任务状态段

设置新进程的代码和数据段基址、限长

复制当前进程内存分页管理的页表

如果父进程有打开的文件,则将对应文件的打开次数增1

最后将新进程设置成可运行状态并返回其PID

进程调度

Linux进程是抢占式的,被强占的进程仍然处于running态,只不过没有被cpu执行,进程在内核态是不会被抢占的。

一、调度策略

1.schedule()函数首先扫描任务数组,通过比较每个就绪态(TASK_RUNNING)任务的运行时间来确定当前哪个进程运行的时间最少。哪个的值最大就表示那个进程运行的时间不长,于是就选中该进程,并使用任务转换函数转换至该进程。

2.每个任务的需要运行的时间片值counter = counter/2 + priority(优先权值)。

3. 如果没有进程可运行,系统就会选择进程0运行,进程0调度pause()把自己置为可中断睡眠状态并再次调用schedule(),其实schedule()并不在意进程0当前的状态,只要系统空闲就调度进程0。

二、进程切换

切换任务由switch_to()宏定义的一段汇编代码完成。

其功能主要是先检测需要切换的是否为本进程,如果是则不做任何操作,如果不是则把内核全局变量current置为新进程的指针,然后长跳转到新任务(进程)的任务状态段TSS的地址处,造成cpu执行任务的切换操作,所以它的执行速度会非常快,然后要对新旧进程的TSS做保存和恢复工作,如下图所示:



终止进程

当一个进程结束了运行或在半途中终止了运行,那么内核就需要释放该进程所占有的系统资源。

当一个用户程式调用exit()系统调用后,就会执行内核函数d0_exit(),并做一系列的资源释放工作,再最后并调用schedule函数去执行其他进程。

在进程终止时,他的任务数据结构仍然保留着,因为其父进程还需要使用其中的信息。

在子进程执行期间,父进程会使用wait()或waitpid()函数等待其子进程的结束。当等待的子进程被终止并处于僵死状态时,父进程就会把子进程运行所使用的时间累加到自己的进程中,最终释放已终止的子进程任务数据结构所占用的内存页面,并置空子进程在任务数组中占用的指针项。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: