前言
本篇基于Linux Kernel 2.6.30 的源代码,源码浏览地址如下:https://elixir.bootlin.com/linux/v2.6.30/source
具体内容分为:
- 操作系统是怎么组织进程的
- 进程状态如何转换(给出进程状态转换图)
- 进程是如何调度的
- 谈谈自己对该操作系统进程模型的看法
一.操作系统是怎么组织进程的
1.进程
1.1进程的引入
多道程序系统中,程序具有:并行、制约以及动态的特征。程序概念难以便是和反映系统中的情况:
①程序是一个静态的概念:程序是完成某个功能的指令集和。系统实际上是出于不断变化的状态中,程序不能反映这种动态性。
②程序概念不能反映系统中的并行特性
例如:两个C语言源程序由一个编译程序完成编译,若用程序概念理解,内存中只有一个编译程序运行(两个源程序看作编译程序的输入数据),但是这样无法说明白内存中运行着两个任务。程序的概念不能表示这种并行情况,反映不了他们活动的规律和状态变化。就像不能用菜谱(程序)代替炒菜(程序执行的过程)一样(这句话我稍微修改了一下,感觉应该是这样表诉才对)
1.2进程的概念
一个进程就是一个正在运行的程序。一个进程应该包含以下内容:
- 程序的代码,既然进程是一个正在运行的程序,自然需要程序的代码
- 程序的数据
- CPU寄存器的值,包括通用寄存器,程序计数器
- 堆(heap)是用来保存进程运行时动态分配的内存空间
- 栈(stack)有两个用途,1保存运行的上下文信息。2在函数调用时保存被调用函数的形参或者局部变量
- 进程所占用的一组系统资源,如打开的文件
2.进程的组织
2.1 进程控制块PCB
Linux中的进程控制块PCB,可以理解为进程属性的集合,该控制块由操作系统创建和管理。每个进程在内核中都有一个进程控制块来维护进程相关的信息,Linux内核的进程控制块是 task_struct 结构体,每个进程都把他的信息放在 task_struct 这个数据结构里,并且可以在include/linux/sched.h 这个目录结构中找到它。所有运行在系统里的进程都以 task_struct 链表的形式存在内核当中。
task_struct 结构体结构如下:
struct task_struct { //说明了该进程是否可以执行,还是可中断等信息 volatile long state; //Flage 是进程号,在调用fork()时给出 unsigned long flags; //进程上是否有待处理的信号 int sigpending; //进程地址空间,区分内核进程与普通进程在内存存放的位置不同 mm_segment_t addr_limit; //0-0xBFFFFFFF for user-thead //0-0xFFFFFFFF for kernel-thread //调度标志,表示该进程是否需要重新调度,若非0,则当从内核态返回到用户态,会发生调度 volatile long need_resched; //锁深度 int lock_depth; //进程的基本时间片 long nice; //进程的调度策略,有三种,实时进程:SCHED_FIFO,SCHED_RR, 分时进程:SCHED_OTHER unsigned long policy; //进程内存管理信息 struct mm_struct *mm; int processor; //若进程不在任何CPU上运行, cpus_runnable 的值是0,否则是1 这个值在运行队列被锁时更新 unsigned long cpus_runnable, cpus_allowed; //指向运行队列的指针 struct list_head run_list; //进程的睡眠时间 unsigned long sleep_time; //用于将系统中所有的进程连成一个双向循环链表, 其根是init_task struct task_struct *next_task, *prev_task; struct mm_struct *active_mm; struct list_head local_pages; //指向本地页面 unsigned int allocation_order, nr_local_pages; struct linux_binfmt *binfmt; //进程所运行的可执行文件的格式 int exit_code, exit_signal; int pdeath_signal; //父进程终止是向子进程发送的信号 unsigned long personality; //Linux可以运行由其他UNIX操作系统生成的符合iBCS2标准的程序 int did_exec:1; pid_t pid; //进程标识符,用来代表一个进程 pid_t pgrp; //进程组标识,表示进程所属的进程组 pid_t tty_old_pgrp; //进程控制终端所在的组标识 pid_t session; //进程的会话标识 pid_t tgid; int leader; //表示进程是否为会话主管 struct task_struct *p_opptr,*p_pptr,*p_cptr,*p_ysptr,*p_osptr; struct list_head thread_group; //线程链表 struct task_struct *pidhash_next; //用于将进程链入HASH表 struct task_struct **pidhash_pprev; wait_queue_head_t wait_chldexit; //供wait4()使用 struct completion *vfork_done; //供vfork() 使用 unsigned long rt_priority; //实时优先级,用它计算实时进程调度时的weight值 //it_real_value,it_real_incr用于REAL定时器,单位为jiffies, 系统根据it_real_value //设置定时器的第一个终止时间. 在定时器到期时,向进程发送SIGALRM信号,同时根据 //it_real_incr重置终止时间,it_prof_value,it_prof_incr用于Profile定时器,单位为jiffies。 //当进程运行时,不管在何种状态下,每个tick都使it_prof_value值减一,当减到0时,向进程发送 //信号SIGPROF,并根据it_prof_incr重置时间. //it_virt_value,it_virt_value用于Virtual定时器,单位为jiffies。当进程运行时,不管在何种 //状态下,每个tick都使it_virt_value值减一当减到0时,向进程发送信号SIGVTALRM,根据 //it_virt_incr重置初值。 unsigned long it_real_value, it_prof_value, it_virt_value; unsigned long it_real_incr, it_prof_incr, it_virt_value; struct timer_list real_timer; //指向实时定时器的指针 struct tms times; //记录进程消耗的时间 unsigned long start_time; //进程创建的时间 //记录进程在每个CPU上所消耗的用户态时间和核心态时间 long per_cpu_utime[NR_CPUS], per_cpu_stime[NR_CPUS]; //内存缺页和交换信息: //min_flt, maj_flt累计进程的次缺页数(Copy on Write页和匿名页)和主缺页数(从映射文件或交换 //设备读入的页面数); nswap记录进程累计换出的页面数,即写到交换设备上的页面数。 //cmin_flt, cmaj_flt, cnswap记录本进程为祖先的所有子孙进程的累计次缺页数,主缺页数和换出页面数。 //在父进程回收终止的子进程时,父进程会将子进程的这些信息累计到自己结构的这些域中 unsigned long min_flt, maj_flt, nswap, cmin_flt, cmaj_flt, cnswap; int swappable:1; //表示进程的虚拟地址空间是否允许换出 //进程认证信息 //uid,gid为运行该进程的用户的用户标识符和组标识符,通常是进程创建者的uid,gid //euid,egid为有效uid,gid //fsuid,fsgid为文件系统uid,gid,这两个ID号通常与有效uid,gid相等,在检查对于文件 //系统的访问权限时使用他们。 //suid,sgid为备份uid,gid uid_t uid,euid,suid,fsuid; gid_t gid,egid,sgid,fsgid; int ngroups; //记录进程在多少个用户组中 gid_t groups[NGROUPS]; //记录进程所在的组 //进程的权能,分别是有效位集合,继承位集合,允许位集合 kernel_cap_t cap_effective, cap_inheritable, cap_permitted; int keep_capabilities:1; struct user_struct *user; struct rlimit rlim[RLIM_NLIMITS]; //与进程相关的资源限制信息 unsigned short used_math; //是否使用FPU char comm[16]; //进程正在运行的可执行文件名 //文件系统信息 int link_count, total_link_count; //NULL if no tty 进程所在的控制终端,如果不需要控制终端,则该指针为空 struct tty_struct *tty; unsigned int locks; //进程间通信信息 struct sem_undo *semundo; //进程在信号灯上的所有undo操作 struct sem_queue *semsleeping; //当进程因为信号灯操作而挂起时,他在该队列中记录等待的操作 //进程的CPU状态,切换时,要保存到停止进程的task_struct中 struct thread_struct thread; //文件系统信息 struct fs_struct *fs; //打开文件信息 struct files_struct *files; //信号处理函数 spinlock_t sigmask_lock; struct signal_struct *sig; //信号处理函数 sigset_t blocked; //进程当前要阻塞的信号,每个信号对应一位 struct sigpending pending; //进程上是否有待处理的信号 unsigned long sas_ss_sp; size_t sas_ss_size; int (*notifier)(void *priv); void *notifier_data; sigset_t *notifier_mask; u32 parent_exec_id; u32 self_exec_id; spinlock_t alloc_lock; void *journal_info; };
PID一列代表了各进程的进程ID,也就是说,PID就是各进程的身份标识。只要运行一程序,系统会自动分配一个标识,不过PID只是暂时唯一的,在进程中止后,这个号码就会被回收,并可能被分配给另一个新进程。
下图为电脑中部分进程的PID:
除了进程ID,每个进程还有一些其他的标识符。
- #include <unistd.h>
- pid_t getpid(void);
返回调用进程的进程ID
- pid_t getppid(void);
返回调用进程的父进程ID
- uid_t getuid(void);
返回调用进程的实际用户ID
- uid_t geteuid(void);
返回调用进程的有效用户ID
- uid_t getgid(void);
返回调用进程的实际组ID
- uid_t getegid(void);
返回调用进程的有效组ID
注意,这些函数都没有出错返回。
二.进程状态如何转换(给出进程状态转换图)
1.进程状态
状态 | 描述 |
TASK_RUNNING | 这个进程状态就是所说的运行状态,它包括两种类型的进程,一个是正在运行的进程,一个是可以被运行但是没有被调度的进程.这二者都是TASK_RUNNING; |
TASK_INTERRUPTIBLE | 这个状态是处于休眠但是可以被打断.在这种情况下会发生,比如当进程等待一个socket的连接或者信号量时,进程会进行休眠但是,这种休眠是可以打断的 |
TASK_UNINTERRUPIBLE | 这种状态是不允许被异步信号打断的,即使是kill -9也不行.这个状态被应用在内核中某些场景中,比如当进程需要对磁盘进行读写,而此刻正在DMA中进行着数据到内存的拷贝,如果这时进程休眠被打断(比如强制退出信号)那么很可能会出现问题,所以这时进程就会处于不可被打断的状态下; |
TASK_STOPPED/TASK_TRACED | 都是进程暂停的状态. 比如当进程收到SIGSTOP信号时会进入到stopped状态.而traced状态是当进程被跟踪时处于的状态, 其中TASK_STOPPED状态也可以通过SIGCONT信号再次运行.而跟踪状态的进程则需要ptrace系统调用才能进一步向下进行. |
TASK_ZOMBIE/TASK_DEAD | 当父进程存在而子进程退出时子进程会处于僵尸状态 |
在 include/linux/sched.h
中被定义:
#define TASK_RUNNING 0 #define TASK_INTERRUPTIBLE 1 #define TASK_UNINTERRUPTIBLE 2 #define TASK_STOPPED 4 #define EXIT_ZOMBIE 16 #define EXIT_DEAD 32
2.进程状态转换
进程是操作系统为了控制多个程序而创建的数据,操作系统是通过修改进程的状态来完成对相应程序的控制,用户程序的一些操作也可以修改一些进程的状态。具体转换如下图:
三.进程是如何调度的
3.1进程调度功能
调度程序负责决定将哪个进程投入运行,何时运行以及运行多长时间。
多任务系统可分为非抢占式多任务和抢占式多任务。Linux提供了抢占式的多任务模式。在此模式下,由调度程序来决定什么时候停止一个进程的运行,以便其他进程能够提到得到执行机会。
3.2调度器
进程调度器的任务就是合理分配CPU时间给运行的进程,创造一种所有进程并行运行的错觉。这就对调度器提出了要求:
1、调度器分配的CPU时间不能太长,否则会导致其他的程序响应延迟,难以保证公平性。
2、调度器分配的时间也不能太短,每次调度会导致上下文切换,这种切换开销很大。
而调度器的任务就是:1、分配时间给进程 2、上下文切换
3.3 O(1)调度算法
O(1)调度算法将系统中的可运行进程分为两类:
- 活动进程: 那些还没有用完时间片的进程
- 过期进程: 那些已经用完时间片的进
调度程序每次都是选取优先级最高而且还有剩余时间片的进程来运行.在选取最高优先级的进程时,首先利用优先级位图从高到低找到第一个被设置的位,该位对应着一条进程链表,这个链表中的进程是当前系统所有可运行进程中优先级最高的.在该优先级链表中选取头一个进程,它拥有最高的优先级,即为调度程序马上要执行的进程.
struct task_struct *prev, *next; struct list_head *queue; struct prio_array *array; int idx; prev = current; array = rq->active; idx = sehed_find_first_bit(array->bitmap); //找到位图中第一个不为0的位的序号 queue = array->queue + idx; //得到对应的队列链表头 next = list_entry(queue->next, struct task_struct, run_list); //得到进程描述符 if (prev != next) //如果选出的进程和当前进程不是同一个,则交换上下文 context_switch();
四.对该操作系统进程模型的看法
当今网络的服务器有90%上的服务器是假设在linux服务平台上的。linux系统相对于其他操作系统具有安全性高,硬件要求低,系统性能稳定等一系列优势;而相对linux而言,其本身属于开源软件,只需要用户具有很一定的学习能力便可学习该操作系统的使用,而且操作系统是免费使用的。如此看来linux还是有很大前景的。
五.参考资料
https://www.cnblogs.com/tianlangshu/p/5224178.html
https://blog.csdn.net/woshixuye/article/details/53931303
https://wenda.so.com/q/1371609273068731?src=140
原文地址:https://www.cnblogs.com/xiaoxian-123/p/8964809.html