1. 前言
该博客内容分析自操作系统linux-2.6.11,为个人学习分享笔记,若有不足之处请指出。
linux-2.6.11源码下载地址:https://mirrors.edge.kernel.org/pub/linux/kernel/v2.6/
2. Linux进程:
2.1 Linux进程的作用
①它是一个独立且可调度的活动。
②它是一个抽象实体,执行任务时分配和释放资源。
③它是一个能够并发运行的单位。
2.2 Linux进程关系
如果一个进程由另一个进程创建,则它们之间具有父子关系。
如果一个进程创建多个子进程,则子进程之间有兄弟关系。
如下图所示:
2.3 Linux进程的几种主要状态:
①可运行状态 ---TASK_RUNNING
②可中断睡眠状态 ---TASK_INTERRUPTIBLE
③不可中断睡眠状态 ---TASK_UNINTERRUPTIBLE
④退出(僵死)状态 ---TASK_ZOMBIE / EXIT_ZOMBIE
⑤退出(销毁)状态 ---TASK_DEAD / EXIT_DEAD
⑥暂停(跟踪)状态 ---TASK_STOPPED
可以总的归类为以下三种状态:
运行态:进程占在CPU上运行,占用CPU。
就绪态:进程具备运行条件,但未分配CPU。
阻塞态:进程因等待某件事发生暂时不能运行。
2.3.1 状态分析:
①可运行状态 ---TASK_RUNNING
程序正在运行或者具备了运行条件准备运行,处于运行态或就绪态。
②可中断睡眠状态 ---TASK_INTERRUPTIBLE
该状态也称浅度睡眠状态,处于阻塞态,在等待资源(可以被中断唤醒),如果有了资源则会被唤醒进入就绪态,准备好运行。
③不可中断睡眠状态 ---TASK_UNINTERRUPTIBLE
该状态也称深度睡眠状态,处于阻塞态,在等待资源(不可被中断唤醒),如果有了资源则会被唤醒进入就绪态,准备好运行。
④退出(僵死)状态 ---TASK_ZOMBIE / EXIT_ZOMBIE
进程已经结束,进程资源用户空间被释放,但还未释放进程控制块PCB,在等待父进程回收,处于阻塞态。
⑤退出(销毁)状态 ---TASK_DEAD / EXIT_DEAD
进程运行完毕或者因为触发系统异常而退出,释放掉进程用户态使用的相关的物理内存,处于阻塞态。
<linux-2.6.11\kernel\exit.c>
if(state == EXIT_DEAD) release_task(tsk);
state = xchg(&p->exit_state, EXIT_DEAD); if (state != EXIT_ZOMBIE) { BUG_ON(state != EXIT_DEAD); return 0;
}
父进程如果是一个EXIT_ZOMBIE状态的进程。则会尝试把它exit_state设置为EXIT_DEAD,所以父进程会把子进程由EXIT_ZOMBIE设置为EXIT_DEAD。
⑥暂停(跟踪)状态 ---TASK_STOPPED
该状态处于就绪态,进程被外部程序暂停接受某种处理,当再次允许时继续执行进入运行态。
2.4 进程状态的转换
2.4.1 主要状态转换
2.4.2 总的状态转换
2.5 进程的组织
2.5.1 0号进程
首先,在整个系统启动期间,会初始化系统的第一个进程init_task(进程0),以下是init_task的核心代码:
<linux-2.6.11/arch/x86_64/kernel/init_task.c>
static struct fs_struct init_fs = INIT_FS; static struct files_struct init_files = INIT_FILES; static struct signal_struct init_signals = INIT_SIGNALS(init_signals); static struct sighand_struct init_sighand = INIT_SIGHAND(init_sighand); struct mm_struct init_mm = INIT_MM(init_mm); EXPORT_SYMBOL(init_mm); union thread_union init_thread_union __attribute__((__section__(".data.init_task"))) = { INIT_THREAD_INFO(init_task) }; struct task_struct init_task = INIT_TASK(init_task); EXPORT_SYMBOL(init_task); DEFINE_PER_CPU(struct tss_struct, init_tss) ____cacheline_maxaligned_in_smp; #define ALIGN_TO_4K __attribute__((section(".data.init_task")))
init_task进程属于内核中的一个进程,是所有进程的祖先,属于一个比较特殊的进程,它是内核开发者人为制造出来的,init_task对象的初始化在内核代码中由该代码来完成:
struct task_struct init_task = INIT_TASK(init_task);
init_task内核栈用静态方式分配,其代码为:
union thread_union init_thread_union __attribute__((__section__(".data.init_task"))) = { INIT_THREAD_INFO(init_task) };
2.5.2 内核栈
每个进程都有自己的内核栈,当进程从用户态进入内核态时,CPU 就自动地设置该进程的内核栈。
<linux-2.6.11/include/linux/sched.h>
struct task_struct { ...... volatile long state; //进程状态 void *stack; //指向内核栈 Struct list_head tasks; //用于加入进程的链表 pid_t pid; //进程的ID pid_t gpid; //线程组的ID unsigned long timestamp; //时间戳 unsigned long rt_priority; //任务的实时优先级 struct mm_struct *mm, *active_mm //指向该进程的内存区描述符 struct task_struct __rcu *real_parent; //指向创建其的父进程如果其父进程不存在,则指向init进程 struct task_struct __rcu *parent; //指向当前的父进程 ...... }
其中pid是一个数字,按从小到大顺序进行编号,表示进程标识号,一次只能标识一个进程,所以每个进程的pid都不同,因此内核可以通过这个标识来识别不同的进程,用户程序可以通过pid对进程发号施令,ppid则是父进程标识号。
2.5.3 宏
<linux-2.6.11\include\asm-i386\current.h>
struct task_struct; static inline struct task_struct * get_current(void) { return current_thread_info()->task; } #define current get_current() #endif /* !(_I386_CURRENT_H) */
该段代码中定义了current 宏,这是一段与体系结构相关的代码,当一个进程在某个CPU 上正在执行时,内核从此处获得指向它的task_struct 的指针。
2.5.4 哈希表
哈希表可以根据进程的pid 可以快速地找到对应的进程。
Linux 在进程中引入的哈希表叫做pidhash,定义如下:
<linux-2.6.11/include/linux/sched.h>
#define PIDHASH_SZ (4096 >> 2) struct list_head uidhash_list; struct pid pids[PIDTYPE_MAX]; extern struct task_struct *pidhash[PIDHASH_SZ]; #define pid_hashfn(x) ((((x) >> 8) ^ (x)) & (PIDHASH_SZ - 1))
PIDHASH_SZ 表示表中元素的个数,表中的元素是指向task_struct 结构的指针。
pid_hashfn 为哈希函数,把进程中的的PID 转换为表的索引。
通过这个函数,可以把进程的PID均匀地散列在它们的域(0 到 PID_MAX-1)中。
Linux 操作系统中利用链地址法来处理冲突的PID,每一表项是由冲突的PID 组成的双向链表,这种链表是由task_struct 结构中的pidhash_next 和 pidhash_pprev 域实现的,同一链表中pid 的大小由小到大排列。
2.5.5 双向循环链表
#define LIST_HEAD_INIT(name) { &(name), &(name) } #define LIST_HEAD(name) /struct list_head name = LIST_HEAD_INIT(name)
struct list_head { struct list_head *next, *prev; };
哈希表的主要作用是根据进程的pid 可以快速地找到对应的进程,但没有反映进程创建的顺序,也没有反映进程之间的亲属关系,因此引入双向循环链表,用每个进程的task_struct结构中的prev_task 和next_task 域来实现这种链表。
链表的头和尾都为init_task,它对应的是进程0(pid 为0),也就是所谓的空进程。
2.5.6 运行队列
struct work_struct { atomic_long_t data; //运行处理函数func的参数* #define WORK_STRUCT_PENDING 0 #define WORK_STRUCT_STATIC 1 #define WORK_STRUCT_FLAG_MASK (3UL) #define WORK_STRUCT_WQ_DATA_MASK (~WORK_STRUCT_FLAG_MASK) struct list_head entry; //连接运行的指针 work_func_t func; //运行处理函数 #ifdef CONFIG_LOCKDEP struct lockdep_map lockdep_map; #endif };
当内核要寻找一个新的进程在CPU 上运行时,必须只考虑处于可运行状态的进程,因为扫描整个进程链表是相当低效的,因而引入了可运行状态进程的双向循环链表,即运行队列。
运行队列通过task_struct 结构中的两个指针work_list 链表来维持。
2.5.7 等待队列
主要例子:sleep_on函数
void sleep_on (wait_queue_head_t *wq) //wq是某个等待队列的队列头 { wait_queue_t wait; //定义一个等待队列结点 init_waitqueue_entry (&wait, current); // 用当前进程初始化这个等待队列结点 current->state = TASK_UNINTERRUPTIBLE; //设置进程状态为不可中断睡眠状态 add_wait_queue (wq, &wait); //将代表当前进程的等待队列结点加入到wq等待队列 schedule (); //请求调度器进行调度,执行完schedule后进程会被移除CPU运行队列,只有等待队列唤醒后才会重新回到CPU运行队列 remove_wait_queue (wq, &wait); //进程已被唤醒,重新移到CPU运行队列 }
进程必须经常等待某些事件的发生,例如,等待一个磁盘操作的终止,等待释放系统资源或等待时间走过固定的间隔。
等待队列实现在事件上的条件等待,也就是说,等待特定事件的进程把自己放进合适的等待队列,并放弃控制权。因此,等待队列表示一组睡眠的进程,当某一条件变为真时,由内核唤醒它们,由循环链表实现。
2.5.8 内核线程
struct task_struct { ...... struct mm_struct *mm; struct mm_struct *avtive_mm; //指向该进程的内存区描述符 ...... };
内核线程主要用于实现系统后台操作,如页面对换,刷新磁盘缓存,网络连接等系统工作。
它所执行的是内核中的函数,而普通进程只有通过系统调用才能执行内核中的函数。
2.6 进程的调度
2.6.1 调度器
Linux调度器的一般原理是按所需分配的CPU计算能力, 向系统中每个进程提供最大的公正性。
调度器由两个调度函数来实现,一个为主调度函数,一个为周期性调度函数。
①主调度器负责将CPU使用权限从一个进程切换到另一个进程。
②周期性调度器由中断实现,在中断过程中执行scheduler_tick()函数,执行完毕后返还CPU权限,可用于更新统计信息。
调度器部分代码:
<linux-2.6.11\kernel/sched.c>
asmlinkage void __sched schedule(void) { struct task_struct *tsk = current; sched_submit_work(tsk); __schedule(); }
先将当前进程加入运行队列,然后选择下个可调度进程,之后调用进程上下文切换,再开始执行下个进程。
static void __sched __schedule(void) { struct task_struct *prev, *next; unsigned long *switch_count; struct rq *rq; int cpu; cpu = smp_processor_id(); rq = cpu_rq(cpu); rcu_note_context_switch(cpu); prev = rq->curr; //当前队列的当前进程 schedule_debug(prev);
... if (unlikely(!rq->nr_running))//当前rq无进程运行时,从其他cpu上取一个task执行 idle_balance(cpu, rq); //选择下个可调度进程 next = pick_next_task(rq); ... //上下文切换 context_switch(rq, prev, next);
... }
2.6.2 调度算法(CFS)
CFS是完全公平调度算法,将所有的进程都统一对待。其中的schedule() 函数它会先抢占当前运行任务,当它开始确定下一个要调度的任务时,它会调用 pick_next_task 函数,通过调度器类调用 CFS 调度器,返回相关 的sched_entity。
pick_next_task的具体实现:
<linux-2.6.11/kernel/sched_fair.c>
static inline struct task_struct * pick_next_task(struct rq *rq) { const struct sched_class *class; struct task_struct *p;
/*使用的数据结构*/ if (likely(rq->nr_running == rq->cfs.nr_running)) { //如果nr_running==cfs.nr_running,则说明当前rq队列中是没有rt任务的, //rt任务不在cfs队列中,它的优先级的设置和cfs不一样。
p = fair_sched_class.pick_next_task(rq); //在cfs队列中挑选即将被切换进去的进程 if (likely(p)) return p; } for_each_class(class) { p = class->pick_next_task(rq); if (p) return p; } }
3.对Linux操作系统进程模型的看法:
Linux操作系统的进程模型是开源的,能够表示这是一个足够安全的系统,对比于windows而言,windows则需要不断的打补丁。
它能够并发的执行任务,合理的分配CPU资源,而且整合性很强,整个操作系统的规模非常的小,但是却能够完成各方各面的任务。
其中有的代码很简明易懂,但是也有的比较深奥,比如算法方面的内容,还需要自己加倍努力,多加学习,努力提高自己,保证自己的能力有所进步。
4.参考资料:
https://blog.csdn.net/jnu_simba/article/details/11724277
https://www.cnblogs.com/tolimit/p/4530370.html
https://blog.csdn.net/u014089131/article/details/54585805