Linux内核基于Minix编写,现在的都是Linux发行版,是内核的自定义。
现代操作系统允许一个进程有多个执行流,即在相同的地址空间中可执行多个指令序列。每个执行流用一个线程表示,一个进程可以有多个线程。
Linux具体实现比较特别,Linux中严格来说是没有线程的,而是使用轻量级进程实现对多线程应用程序的支持,一个轻量级进程就是一个线程。
本章内容:
进程有两种状态,用户态和核心态。
这两种状态的根本区别在于切换了段地址。用户态的时候用的是用户栈,而核心态用的是核心栈。
状态的切换以及进程的切换都需要保存信息,其中就用到进程描述符
task_struct结构如下,图中仅列出关键的几个部分,实际上有更多:
每个进程都在内存中有一个进程核心栈,其和thread_info是放在一起的,总共占2页空间,核心栈在一端,thread_info在另一端。
task_struct通过thread_info指针访问核心栈,核心栈通过thread_info反向访问task_struct,是双向的。
具体到操作系统,操作系统通过esp获取thread_info地址,然后通过thread_info获取task_struct(PCB),即操作系统通过esp获取PCB信息。尤其是进程刚从用户态切换到核心态时,其核心栈为空,只要将栈顶指针减去8k,就能得到thread_info结构的地址。
Linux的链表都是list_head类型列表
全局的链表有:
这两个链表,每一个PCB都有:
pid实际上是一种哈希(TODO)
Linux有三个创建进程的函数:
要想实现共享+并行,需要用fork+共享内存区+PV操作实现。
进程撤销:
如果只将主进程exit,子进程保留,子进程就会变成孤儿进程。
此时会给孤儿分配养父,即init进程(1号,监控用户态进程),init进程定期会处理僵死的子进程。
前面是用户空间进程的创建,内核有另一套函数。
内核空间的线程创建create_ kthread () / kthread_run()。内核里只有线程,因为内核本身就是root权限,不需要进行数据隔离。出于好听的原因,人们又喜欢把内核线程称作进程。
内核线程的任务一般是执行周期性执行的任务,如刷新磁盘高速缓存;交换出不用的页;维护网络连接等任务。相对的,用户线程高度自定义,不具有周期性。
使用ps查看进程信息。-eo自定义显示信息,显示pid,ppid,command信息。
可以看到,没有显示0号进程,而1号进程负责init任务与用户态进程创建,其父进程是0号进程,2号进程负责内核线程创建。这里显示的其他进程都是内核线程,任务各不相同,其父进程都是2号进程。
用户态进程之间可以切换。用户态进程到核心态也需要切换。这两种切换执行的思路类似:
这里需要注意,进程切换只能发生在核心态,毕竟用户怎么能掌握计算机运行的调度权呢?那问题来了,从核心态切到用户态很自然,那用户态怎么切到核心态呢?这就需要触发中断,发送一个信号给核心,然后核心再进行切换。
所以一个切换操作分如下流程:
Linux,无论是用户态还是核心态,都是可抢占的。
为了保证抢占的合理,需要搭配动态优先级。
进程调度类型:
实时进程调度时机:
进程调度需要用到特殊的数据结构,其实就是前面的进程链表,是一维数组runqueues(TODO,TODO到底是一维数组还是链表?)。
一个CPU有140级全局可运行进程链表。
在时间片轮转系统中,还可以再分两类,一类是活动进程链表,一类是过期进程链表,各有140个队列。当活动进程都过期后,过期进程才可运行。避免低优先级进程没有机会运行(进程饥饿)
刚开始优先级动态计算复杂度比较高,所以引入了公平调度算法。
不同内核线程使用一个内核,所以对于互斥资源就需要进行同步控制。核心思路就是保证临界区的同一时间只对应一个内核控制路径(?TODO)
同步方法很多,不止有信号量。
32位机器的进程寻址空间为4G。
进程的私有空间是3G,剩下1G是内核虚空间。
1G虚拟空间的前896M对应物理内存的前896M。前896MB的物理地址等于内核虚地址减去0xc0000000(3G)后128MB的虚拟空间比较特殊,是固定的分区,与用户。
注意,这里针对的是3G的内存空间,是进程管理。而核心都是线程,不存在下面的管理机制。
针对3G的用户空间,Linux的管理机制如下图:
看着比较庞大,我们大致捋一下,你可能现在看不懂,但是看完后面两个结构的具体描述再回来看也是OK的:
struct vm_area_struct {
struct mm_struct * vm_mm; 虚拟内存描述符,指向其对应的mm_struct
unsigned long vm_start; /*起始地址*/
unsigned long vm_end; /*结束地址*/
struct vm_area_struct *vm_next; /*单链表*/
struct rb_node vm_rb; /*红-黑树*/
struct file * vm_file; 映射文件时指向文件对象
……
}
结构里同时有红黑树指针和链表指针,说明vm area struct是同时具有两种组织方式。
红黑树是一种特殊的平衡二叉树,满足红黑树规则的n节点树,高度最多为 2 × l o g ( n + 1 ) 2\times log(n+1) 2×log(n+1)
内核线程不拥有mm_struct(本身就是线程,mm struct是针对进程的) 。
struct task_struct
{ // PCB
…
struct mm_struct *mm;
…
}
struct mm_struct {
struct vm_area_struct *mmap; /*单向链*/
struct rb_root mm_rb; /*指向红-黑树的根*/
pgd_t *pgd; /*指向页目录表*/
atomic_t mm_users; /*次使用计数器*/
atomic_t mm_count; /*主使用计数器*/
struct list_head mmlist; //双向链表
unsigned long start_code, end_code; /*可执行代码所占用的地址区间*/
……};
结构里有指向vm area struct链表的头指针,也有指向红黑树结构的指针。
说一下mm_users mm_count这两个计数器。
进程结束,mm_users和mm_count都为0时,这个mm_struct才能被释放。
物理内存是连续的(假设4G)
页框0给bios用,从0x000a0000到0x000fffff(1M空间)给BIOS例程用。
Linux管理页框的时候,会跳过前1M的空间(BIOS空间,对应RAM)。剩下的页框,大小为4KB,每一个页框都有一个页框描述符,struct page。这些struct page以结构数组的形式放在mem_map数组中。
这个结构,mapcount是页框号。其余的字段,要注意private字段,和伙伴系统有关(后面)。还有mapping字段,和页高速缓存的核心数据结构,与文件的inode有关。
struct page {
unsigned long flags; /*页框状态标志P175*/
atomic_t _count; /*页框的引用计数*/
atomic_t _mapcount; /*页框号,可以索引到物理页框*/
unsigned long private; /*空闲时由伙伴系统使用*/
struct address_space *mapping;用于页高速缓存
pgoff_t index; 在页高速缓存中以页为单位偏移
struct list_head lru; 链入活动页框链表或非活动..
void *virtual; /*页框所映射的内核虚地址*/
};
整个4G内存被分成3块zone:
也就是说,大部分内存区域仅仅被一个zone结构管理:
这个结构里又出现了伙伴系统。
struct zone {
unsigned long free_pages; 空闲页框数
struct per_cpu_pageset pageset[NR_CPUS];
/*每CPU页框高速缓存,以满足CPU对单个页框的请求p177*/
struct free_area free_area[11];
/*伙伴系统中的11个空闲页框链表*/
struct list_head active_list; /*活动页框链表,存放最近正被访问的页框*/
struct list_head inactive_list; /*非活动页框链表,存放最近未被访问的页框*/
…….};
free_area是11个长度的数组。对应 2 0 − 2 10 2^0-2^{10} 20−210长度。有的程序会需要连续的物理空间,而这个数组就负责统计整个zone里的连续物理空间,记录在数组中。
具体来说,比如数组中的8号位,实际上是一个链表头指针。指向一个链表,每一个链表节点都是起始页框描述符,这意味着这个链表对应着n段长度为8的连续物理空间。
而这里也揭示了private有什么用,private表示的是2的幂,如果是4就是16长度,3就是8长度。
伙伴系统就是基于分区页框分配器的。
假设要请求一个具有8个连续页框的块,该算法先在8个连续页框块的链表中检查是否有,如果没有,就在16个连续页框块的链表中找,如果找到,就把这16个连续页框分成两等份,一份用来满足请求,另一份插入到具有8个连续页框块的链表中;
如果在16个连续页框块的链表中没有找到,就在更大的块链表中查找,比如32找到了,就先切16,插入到16链表中,然后再把剩下的再切,把8插入到8链表中,最后剩下的8个连续页框就分配出去。
回收的时候,会检查长度,如果长度从8变成16,就合并,插入16中。
伙伴系统以页框为单位,分配大块内存。有一些file对象或者各种描述符需要大量的小内存,这就是slab分配器的作用。
slab分配器先批发一些连续页框,常驻内存,构成高速缓冲区,缓冲区内部进行细粒度分配。常驻内存以空间换时间,减少了内存分配初始化销毁释放的代价。
slab分配器生成的每个高速缓存存储一种类型的对象。高速缓存由一连串的slab构成,每个slab包含了若干个同类型的对象。这种细粒度的切分也是slab的特点。
从一个虚拟地址到最后的物理地址,要经过多级转换。
首先,通过段基址+段偏移,获得32位线性地址,然后将32位线性地址通过分页部件转换成物理地址。
无论是页目录表项还是页表项,结构都是一样的。一个页表项,不仅仅包含地址,还有很多字段:
传统的页表项字段且不说,虚拟内存的页表项增加了一些字段:
Linux中,有一个磁盘交换区。可以理解为内存与磁盘之间的一种缓存。交换区可以直接作为一个磁盘分区,这种交换区就只有一个子区(swap分区),也可以以文件形式存在,但是这种就会被切分为多个物理块(文件会被离散储存)。
盘交换区由若干页槽组成,页槽大小和内存的页大小对应,为4K。盘交换区的第一个页槽存放交换区的整体信息,其他页槽用于交换。发生交换的时候,内核尽量把换出去的页放在相邻页槽中,减少后续磁道寻道时间。
缺页中断发生在这些情况下:
发生缺页中断时,会先去交换区找,交换区找不到就会去磁盘去调。
页面置换策略是LFU(Least Frequently Used)。注意这和LRU不同,R是Recently,而这个是Friquently,这个算法会统计最近调用一页的频数,频数最少的就被交换出去。
最开始是Minix文件系统,之后用Ext FS(Extended FileSystem),现在是Ext2,广泛运用。
Linux的物理结构是索引文件结构。
Ext2把磁盘块分组。这是因为一个物理块上的位示图无法描述所有的空间。描述一个块组要用到下面这些元数据,总的来说,两个描述块,两个位图块,剩下的就是两类文件块。
这是整体的架构图,磁盘分区下有块组,块组下有6个区,索引区有一系列索引节点inode,inode本身具有复杂的数据结构,描述了一个文件。
同时,为了描述大文件,inode中设有15个索引,分为0-3级索引。但是需要注意的是,多级索引使用一些储存索引值的物理块存索引,与inode不一样,储存索引值的块没有复杂的数据结构(本身是物理块,里面全是物理块号,每个块号长4B,仅此而已)
超级块描述文件系统整体信息,所有块组的超级块都是一模一样的。
块组描述符描述一个块组的信息。
//超级块
struct ext2_super_block {
__le32 s_inodes_count; 索引节点的总数
__le32 s_blocks_count; 盘块的总数
__le32 s_free_blocks_count; 空闲块计数
__le32 s_free_inodes_count; 空闲索引节点数
__le32 s_log_block_size; 盘块的大小
__le32 s_blocks_per_group; 每组中的盘块数
__le32 s_inodes_per_group; 每组索引节点数
__le16 s_inode_size; 磁盘上索引节点结构的大小
……
};
//一个块组描述符
struct ext2_group_desc {
__le32 bg_block_bitmap; 盘块位图的块号
__le32 bg_inode_bitmap; 索引节点位图的块号
__le32 bg_inode_table; 索引节点区的第一个盘块块号
__le16 bg_free_blocks_count;组中空闲块的个数
__le16 bg_free_inodes_count;组中空闲索引节点的个数
__le16 bg_used_dirs_count; 组中目录的个数
……};
一个物理块用一个bit对应,与内存中的位示图一致。
储存数据本身。
其中需要注意的是,目录文件也是存在这里的,一个目录文件就是一张文件目录表:
文件目录项描述一个文件,一个简单的文件目录项包含文件名以及索引节点号结构。
间接索引表也在数据区。
通过索引节点号就可以锁定一个inode索引节点,就可以找到文件的所有物理块。
struct ext2_dir_entry_2{
__le32 inode; 索引节点号
__le16 rec_len; 目录项长度
__u8 name_len; 实际文件名长度
__u8 file_type; 文件类型
char name[255]; 文件名是4B的整数倍,变长数组
}
目录项长度,固定长度为4(inode)+2(rec_len)+1(name_len)+1(file_type)=8
变化的长度为名字,为4n个长度。
如果要删除文件,就把索引节点号置零,前一个文件的rec_len就会变长,这使得我们在扫描文件目录项的时候,可以跳过那个被删除的文件。
m个索引节点块共同构成一个大表,叫inode table。
inode table中是一个一个的inode,一个inode有128B长,所以一个索引节点块可以存放 4 K 128 B = 32 \dfrac{4K}{128B}=32 128B4K=32个inode。
一个索引节点inode,对应一个实体的文件,有如下重要信息:
inode与文件一一对应,但是n个文件目录项可以对应一个inode,此时inode的硬链接计数为n,每删除一个文件目录项,硬链接计数就减一,当n=0的时候也就可以删除inode了。
宏观来说,inode是文件系统角度的文件,而文件目录项是用户角度的文件,用户角度看到的多个硬链接文件,指向的是同一个inode。
15个元素的数组,其实就是索引表。我们捋一下多级索引,假设一个盘块的大小为b,则满打满算,只储存块索引,可以存 b 4 \dfrac{b}{4} 4b个索引。
有趣的是,这三级索引对应的区间是连续的,所以三级索引可以表示出小文件以及很大的文件,大小可以连续变化,这里计算一下,假设b=4KB:
物理块号是32位的,对应 4 G × 4 K = 16 T 4G\times4K=16T 4G×4K=16T的磁盘寻块空间,如果使用四级索引,文件大小的上限(4P+4T+4G+4M+48KB)就会超出物理磁盘寻址空间。
所以三级索引与物理块号位数是匹配的。
一个普通文件,是通过inode索引的标准模式,但是其他特殊类型的文件并不如此。
符号链接文件,本身就是一个文件,理论上应该配n个物理块+1个inode+1个文件目录项。不过这么做成本可能是有点大,更简洁的方式是只用inode即可。
如果路径名小于60B,那么可以直接存i_block数组里,如果超出,那就只能用额外的物理块了,i_block也回归了最初的作用——物理块索引。
至于其他的文件,因为路径不会太长(比如设备端口长度是很短的),所以一定可以直接存inode。
与磁盘是一一对应的,有超级块和inode。
至于其他的块组成分,不做考虑。
struct ext2_inode_info { /*内存索引节点*/
……
struct inode vfs_inode; /*索引节点对象*/
……
}; //P196
磁盘空间管理负责 磁盘块和索引节点的分配和回收,让文件放置遵循一定原则,使得寻道成本降低。
都是尽量,不能保证在一个组块中。
Linux内置Ext2文件系统,但是文件系统有很多,Minix,Ext2,FAT,NTFS,各种设备等等,要兼容这些设备,需要建立一个接口。对用户来说,接口提供统一的操作方式,之后将操作转换到各自的文件系统中。
这个接口就是VFS(virtual file system)
这种机制如何实现呢?磁盘已经是Ext2了,不能改了,所以使用灵活性较强的内存来在运行时生成虚拟文件数据结构。
注意,除了磁盘上存的数据以外,这些VFS数据结构都在内存(Ext2的实际的文件系统,这些数据结构存在磁盘)。
总的来说,从索引节点到目录项对象到文件对象,是一个树结构。
当进程打开文件的时候,下面的对象全部是在内存中动态生成的。这里宏观地总结一下,要具体说还得从下面仔细看。
其实还用到ext2_inode,只不过这个是在磁盘中。
struct super_block { //P205
struct list_head s_list; 系统超级块双向链
struct file_system_type *s_type;
struct super_operations *s_op;
struct dentry *s_root 根目录的目录项对象
struct list_head s_inode; 索引节点链表
struct list_head s_files; 文件对象链表
void *s_fs_info; 指向一个具体文件系统的超级块结构
……
}
super_blok在磁盘上有映像,s_fs_info指向磁盘中具体的超级块,对应磁盘中具体的一个文件系统。
一个超级块对应一个文件系统,file_system_type描述了文件系统类型,超级块之间通过双向链表链接。
在把一个虚拟文件系统载入到Linux中的时候,可以指定其根目录,使用dentry字段描述。
struct inode { //P207
unsigned long i_ino; 磁盘索引节点号
atomic_t i_count; 该对象的引用计数
nlink_t i_nlink; 硬链接计数
struct inode_operations *i_op; //P209
struct address_space *i_mapping;
…
}
struct ext2_inode_info { /*内存索引节点*/
……;
struct inode vfs_inode;
……
}; // P196
inode对象在磁盘上有映像,其i_ino字段储存了磁盘上的索引号。
注意区分:
struct file { //P212
struct list_head f_list; 文件对象链表
struct dentry *f_dentry; 指向目录项对象
atomic_t f_count; 该对象的引用计数
loff_t f_pos; 文件的当前读写位置
struct file_operations *f_op; 操作类型
struct address_space *f_mapping; 内存映射
……
}
仅在内存中,没有磁盘映像。
struct dentry { //P212
atomic_t d_count; 文件对象的引用计数
struct inode *d_inode; 指向inode对象
struct dentry *d_parent; 指向父目录项对象
struct list_head d_alias; 属于同一inode的dentry链表(同目标的硬链接链表)
struct dentry_operations *d_op; 方法
……}
Struct tast_struct{
……
struct fs_struct *fs; //指向文件系统
struct files_struct *files; 指向进程打开文件信息
…}
在PCB中,有指向文件系统的指针,有指向files_struct的指针,这个结构储存了当前进程打开的所有文件的信息。
struct files_struct { P213
struct file **fd; 指向文件对象指针数组
struct file *fd_array[ ]; 文件对象指针数组
……}
一般情况下,只需要fd_array,这个指针数组长一般为32,也可以扩展64。如果打开的文件超出64,就会在内存中新开一个指针数组,其地址用fd表示。这个指针数组能存的比较多,加起来够1024。
一个进程最多开1024个文件。
这个时候你再回来看下面这个图:
注册:register_filesystem(),可以理解为在VFS中把一种文件系统对应的超级块写入物理块。
安装:mount
mount –t ntfs /dev/hda2 /mnt/ntfs
在挂载了文件系统后,会在安装表里添加一个描述符。这个描述符分别指向被安装的根目录对象(来源)与安装点目录对象(去向)
struct vfsmount {
struct dentry *mnt_mountpoint;
/*指向安装点的目录项对象*/
struct dentry *mnt_root;
/*指向被安装文件系统的根目录*/
……
}
文件打开与关闭:open(), close()
文件的读写:read(), write()
Windows与Linux最大的区别在于,Windows是私有的,Linux内核是开源的。
看到这种图不必害怕,从上往下读即可:
这个图更加细节,红色的大圈为执行体,其功能是分块的,而下面的kernel是一整块,kernel下面是HAL,HAL下面是硬件。
至于侧面的东西,略过即可。
核心态的组件就这两个东西,上层是执行体,下面是内核。
执行体和内核是概念上的东西,具体在操作系统中实现是以对象的形式存在的:
HAL是一个可加载的核心态模块hal.dll,为Windows运行在硬件平台上提供低级接口。
设备驱动程序和执行体的其他部分隐藏各种与硬件有关的细节,HAL使上层免受特殊硬件平台的影响, 系统可移植性好。
总的来说,这是个分层模型,同时也是客户/服务机模型。
客户进程和服务器进程通过执行体中的消息传递工具进行通信。
核心态组件中使用了面向对象的设计原则,但是整体来说,Windows不能说是一个面向对象的操作系统。
中断有两种:
中断只是陷阱调度的一部分,陷阱调度整体上就是通过各种信号来实现异步。
中断的执行是有优先级的,如何在硬件层面维持优先级呢?或者说,对于一个CPU来说,当你运行的过程中收到一个中断,你会不会响应?
响不响应取决于CPU优先级和中断优先级哪个更高。
CPU哪来的优先级?CPU在执行一个中断的时候,CPU的优先级就是正在执行的中断的优先级,执行完中断以后优先级会恢复。
如果CPU优先级高于新来的中断,那么就屏蔽,如果新来的中断优先级更高,就去处理更重要的中断。这就是所谓的中断屏蔽,这是很自然的思路。从这张优先级表来说,用户线程最低,软件中断其次,硬件中断最重要。硬件中断内部也有高下之分,电源是最重要的,处理器次重要。
TODO,如果一个中断被打断,回来以后是否还会处理
DPC软件中断中,优先级最高的。
通常,一个任务会有重要的部分和不重要的部分,重要的部分会产生硬件中断,优先执行。耗时的,不那么重要的(但是仍然很重要),会产生DPC软件中断,等硬件中断都处理完了,再处理DPC任务。这就是延迟调用。
具体怎么实现呢?
Asyncroneus Procedure Call。每个线程都有自己的APC队列,对应一些线程要处理的信息,就好比你微信里的红点一样。
APC队列优先级高于用户线程,所以当一个线程被调度时,它的APC过程会首先被执行。这种机制使得APC很适合实现异步IO通知(回顾操作系统的进程间消息传递)
举例:文件操作有“同步”和“异步”之分。如一个进程调用写文件,同步,阻塞直到写完,该进程被唤醒;异步,调用写之后可以去干别的事,写完之后产生一个通知用APC放到该进程中
下图给出IO请求的中断处理,分为同步IO和异步IO两种方式。
对象管理器是执行体的一个部分,负责从上而下地管理操作系统核心态的各种对象,所谓对象,就是一种数据结构,任何一个有结构的系统都可以看做一个对象,不论大小:
对象由两部分组成:
需要注意的是,对象头里面的对象类型实际上是一个指针,指向一个类型对象。之所以这么干,是因为个一字段难以完美地描述一类对象,所以干脆就整个类型对象出来。
类型对象描述了一类对象,有哪些公共的属性,公共的方法。
每一个具体的对象都会指向其所属的类型对象。
TODO对于特殊的进程对象,还有一些额外的链接:对象之间通过链表链接,而其类型对象指向链表的头结点。
前面只是说了,对象与其类型对象的关系,还没涉及到进程。一个进程是如何管理其创建的对象的呢?
从这个图整体看,进程句柄表指向已经打开的对象,打开的对象又指向其类型对象。
有一些特殊的对象,可以被多个进程同时打开,比如信号量对象。
系统组件的对象分两种,所以同步也分两种
内核引入自旋锁实现多CPU互斥访问内核临界区
自旋锁的实现是testset,这是硬件层面的指令实现,因此自旋锁线程具有绝对优先级,不可能被剥夺,所以要十分小心,不要占用太久,更不要卡死锁。
执行体之间要同步,执行体还提供了用户层面的同步。而且这个同步机制是统一的:等待调度程序对象为有信号状态,WaitForSingleObject( ) 。
回忆一下调度程序对象,核心组件对象分为执行体对象和内核对象,内核对象分为控制对象和调度程序对象,调度程序对象会影响进程调度。调度程序对象有:进程、线程、事件、信号量、互斥体、可等待的定时器、I/O完成端口或文件等同步对象
每个同步对象有两种状态:“有信号”,“无信号”,线程、进程终止时有信号。所以当一个进程终止时,就会产生一个信号。
进程的特点:
线程是进程内的执行实体。一个进程有一个主线程,以线程为单位调度执行。核心级线程。
KPROCESS
struct KPROCESS{
DISPATCHER_Header; 调度头
DirectoryTablebase; 页目录表的基地址
BasePriority; 基本优先级
…… }
E:execute,对应执行体。
其中有一个KPROCESS,K对应内核,PCB是内核进程块。
struct EPROCESS{ P285
KPROCESS Pcb; 内核进程块,位于内核层
ObjectTable; 进程的句柄表
PageDirectoryPte; 页目录表项
ImageFileName; 进程的可执行映像文件名
UniqueProcessId ; 进程的ID
SectionObject;指向可执行映像文件的区域对象
SectionBaseAddress; 该区域的基地址
VadRoot; 平衡二叉树的根,代表虚拟地址空间
WorkingSetPage; 进程工作集页面
Peb;位于进程私有地址空间的环境块
Win32Process; 指向由Windows子系统管理的进程区域,此值不为空,说明是GUI进程。
PriorityClass;进程的优先级
ThreadListHead; 线程链表
ActiveProcessLinks; 所有活动进程连接在一起
……
}
一个进程对象,核心的数据字段就是:
操作系统通过给出函数接口,提供进程对象的服务。
类似于EPROCESS,也有ETHREAD和KTHREAD,同样是ETHREAD指向KTHREAD。
每一个KTHREAD都指向自己的核心栈,虽然说线程共用数据,但只是公用数据而已,执行信息还是要保存的,所以核心栈是万万不可公用的。
注意,APC队列是一个线程就有一个。
大致上和Linux一样,但是Windows更多地考虑了多处理器系统。Windows采用,基于优先级的抢先式的多处理器调度系统,优先级相同时按时间片轮转
Windows调度以线程为单位,线程调度时,不考虑线程属于哪个进程,但是还是会考虑其进程的优先级。
线程属于进程,所以其优先级继承了进程基本优先级。而线程本身有当前优先级,代表相对的优先级。
系统调度的调度要考虑基本优先级和当前优先级,具体比较略过,结果就是会有32个优先级队列:
相比起来,Linux有140个优先级队列,控制的更加细节(这就是windows容易卡的原因吗?)
就绪态,运行态,阻塞态是进程的基本状态。对于线程,增加了4个更加细化的状态:备用态。
一个电脑上,处理器可能是不同种类的,因为线程对不同的处理器有不同的适应性,所以线程和处理器有亲和关系(Affinity)
当线程对处理器有偏好的时候,高优先级的就绪态线程可能不能变成运行状态,比如线程1喜欢A处理器,但是有个更高优先级的线程2占用A处理器,即使B处理器没线程用,线程1还是会因为偏好而不去选择B处理器。
在有亲和关系的情况下,线程调度使用了更多的数据结构:
对于前面说的偏好情况,在就绪位图中,高优先级的线程1所属队列位图是1,表示正在等待,此时,处理器空闲位图也有空闲,这种情况下就会采取一些行动防止线程1饥饿。
线程优先级提升,用于防止线程饥饿问题。本质上来说,优先级提升就是三种情况:
具体分为如下情况:
前面说的执行体同步和内核同步,都是核心态级别的同步,这里的线程同步是用户空间的同步。