内核轻探,锁,虚拟地址空间

Posted chary

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了内核轻探,锁,虚拟地址空间相关的知识,希望对你有一定的参考价值。

可重入函数

可重入函数:当前进程已经处于该函数中, 这时程序会允许当前进程的 某个执行流程再次进入该函数, 而不会引发问题。
可重入函数一定是线程安全的,而线程安全函数则不一定是可重入函 数,很难说出哪些函数是可重入函数,但是可以很明显看出哪些函数是不可以重入的函数。
例子:
当函数使用锁的时候,尤其是互斥锁的时候,该函数是不可重入的,否则会造成死锁。
若函数使用了静态变量(存储在全局区),该函数也是不可重入的,否则会造成该函数工作不正常

死锁与互斥量

死锁即死循环,一种简单的情况就是互相申请各自已经拿到的互斥量
如:线程1已经成功拿到了互斥量1, 正在申请互斥量2, 而同时在另一个CPU上, 线程2已经拿到了互斥量2, 正在申请互斥量1。 彼此占有对方正在申请的互斥量, 结局就是谁也没办法拿到想要的互斥量, 于是死锁就发生了。
下图为复杂的死锁环境,如果存在多个互斥量, 一定要小心防范死锁的形成。

解决方法:
1.存在多个互斥量的情况下,避免死锁最简单的方法就是总是按照一定的先后顺序申请这些互斥量。
如果每个线程都按照先申请互斥量1,再申请互斥量2的顺序执行,死锁就不会发生。 如果有一些互斥量原本就没有特定的层级关系,可以人为干预,让所有的线程必须遵循同样的顺序来申请互斥量
2.要加锁前尝试获取锁

int pthread_mutex_trylock(pthread_mutex_t *mutex);  
int pthread_mutex_timedlock(pthread_mutex_t?*restrict mutex, const struct timespec *restrict abs_timeout);  

不过trylock不行就回退的思想有可能会引发活锁(live lock) 。
生活中也经常遇到两个人迎面走来,双方都想给对方让路, 但是让的方向却不协调, 反而互相堵住的情况。活锁现象与这种场景有点类似

原子操作

即使有了volatile 也要用锁

volatile 的作用有三,一是外部设备寄存器映射后的内存,二是由于一可以防止编译器对其优化,其三是多线程或异步访问的全局变量

static volatile int counter = 0;
void add_counter(void)
{
++counter;
}

其反汇编代码为:

add_counter:
pushl %ebp
movl %esp, %ebp
movl counter, %eax
addl $1, %eax
movl %eax, counter
popl %ebp
ret

可以看出,++counter就绝不可能是原子操作了, 必须使用锁保护。
加了volatile 之后有什么用了,加了后每次都需要从内存读取修饰的变量的值,再放到寄存器,同时对该变量的修改, volatile并不提供原子性的保证。
综上,volatile即不能保证原子性,也不能保证顺序性,不能用来进行多线程同步

读写锁

对于共享变量的访问,读请求之间是无需同步的, 它们之间的并发访问是安全的。 然而写请求必须锁住读请求和其他写请求。
读写锁的行为

| 当前锁状态 | 读锁请求 | 写锁请求 | | – | – | – | | 无锁 | OK | OK | | 读锁 | OK | 阻塞 | | 写锁 | 阻塞 | 阻塞 |

虚拟地址空间的一些理解

Linux系统虚拟地址空间的总结及一些理解,文章以32位系统为例

虚拟地址空间:创建一个进程时,操作系统会为该进程分配一个 4GB 大小的虚拟 进程地址空间。4 字节指针的寻址能力为 4GB 大小的容量。
1.虚拟地址空间分为用户空间和内核空间,在虚拟内存中,我们的所使用的地址是逻辑地址
2.每个进程只能访问自己虚拟地址空间中的数据,无法访问别的进程中的数据,通过这种方法实现了进程间的地址隔离。

用户空间与内核空间

用户空间是通过页表进行映射到物理地址,而内核空间0-896M是通过线性一一映射到物理地址,即用虚拟地址减去一个偏移量0xc0000000。
32位系统用户进程最大可以访问3GB,内核代码可以访问所有物理内存。
64位系统用户进程最大可以访问超过512GB,内核代码可以访问所有物理内存。

为什么需要高端内存区

众所周知,内核空间上,大于896M的为高端内存区,若机器安装的物理内存超过内核地址空间范围,就会存在高端内存
为什么需要高端内存呢?因为内核空间只有1G,如果一一映射的话,只能访问1G物理内存,所以在大于896M的高端内存区上,可以实现随时映射来表示剩余的空间,包括动态映射,永久内核映射,临时映射
(目前现实中,64位Linux内核不存在高端内存,因为64位内核可以支持超过512GB内存。)

内核空间地址

技术图片

虚拟内存管理的最基本的管理单元应该是struct vm_area_struct了,它描述的是一段连续的、具有相同访问属性的虚存空间,该虚存空间的大小为物理内存页面的整数倍。

用户态与内核态

接触Linux内核会接触到最多的是一些概念,如一个程序运行时,什么时候是用户态,什么时候是内核态,上下文切换,中断处理,多任务处理,用户态切换,IO切换等

用户态和内核态

现在操作系统都是采用虚拟存储器,为了保证用户进程不能直接操作内核,保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。针对linux操作系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。

技术图片
1.如上图所示,从宏观上来看,Linux操作系统的体系架构分为用户态和内核态(或者用户空间和内核)。 内核从本质上看是一种软件——控制计算机的硬件资源,并提供上层应用程序运行的环境。为了使上层应用能够访问到CPU资源、存储资源、I/O这些资源,内核必须为上层应用提供访问的接口:即系统调用。
用户态的应用程序可以通过三种方式来访问内核态的资源:
1)系统调用
2)库函数
3)Shell脚本

2.发生从用户态到内核态的切换,一般存在以下三种情况:
1)系统调用
2)异常事件: 当CPU正在执行运行在用户态的程序时,突然发生某些预先不可知的异常事件,这个时候就会触发从当前用户态执行的进程转向内核态执行相关的异常事件,典型的如缺页异常。
3)外围设备的中断:当外围设备完成用户的请求操作后,会像CPU发出中断信号,此时,CPU就会暂停执行下一条即将要执行的指令,转而去执行中断信号对应的处理程序,如果先前执行的指令是在用户态下,则自然就发生从用户态到内核态的转换。
注意:系统调用的本质其实也是中断,相对于外围设备的硬中断,这种中断称为软中断
例子:整数除以零 - 异常事件
read 系统调用
如sin()函数不是系统函数,不会发生切换
另外,Spinlock没有昂贵的系统调用,一直处于用户态,执行速度快。

3.用户态和内核态的切换
Unix/Linux对不同的操作赋予不同的执行等级,就是所谓特权的概念。Linux0级供内核使用,3级供用户程序使用。很多程序开始时运行于用户态,但在执行的过程中,一些操作需要在内核权限下才能执行,这就涉及到一个从用户态切换到内核态的过程。
比如C函数库中的内存分配函数malloc(),它具体是使用sbrk()系统调用来分配内存,当malloc调用sbrk()的时候就涉及一次从用户态到内核态的切换,类似的函数还有printf(),调用的是wirte()系统调用来输出字符串,等等。
技术图片

进程上下文与中断上下文

引例:传送门
> 当一个程序执行了系统调用或者触发某个异常(软中断),此时就会陷入内核空间,内核此时代表进程执行,并处于进程上下文中 – 《linux内核设计与实现》

程序在执行过程中通常有用户态和内核态两种状态,CPU对处于内核态根据上下文环境进一步细分,因此有了下面三种状态:
(1)内核态,运行于进程上下文,内核代表进程运行于内核空间。
(2)内核态,运行于中断上下文,内核代表硬件运行于内核空间。
(3)用户态,运行于用户空间。
所谓的“进程上下文”,可以看作是用户进程传递给内核的这些参数以及内核要保存的那一整套的变量和寄存器值和当时的环境等。
当发生进程调度时,进行进程切换就是上下文切换(context switch).操作系统必须对上面提到的全部信息进行切换,新调度的进程才能运行。而系统调用进行的模式切换(mode switch)。模式切换与进程切换比较起来,容易很多,而且节省时间,因为模式切换最主要的任务只是切换进程寄存器上下文的切换。

硬件通过触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的 一些变量和参数也要传递给内核,内核通过这些参数进行中断处理。所谓的“ 中断上下文”,其实也可以看作就是硬件传递过来的这些参数和内核需要保存的一些其他环境(主要是当前被打断执行的进程环境)。中断时,内核不代表任何进程运行,它一般只访问系统空间,而不会访问进程空间,内核在中断上下文中执行时一般不会阻塞。

总结:
当一个进程在执行时,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容被称为该进程的上下文。当内核需要切换到另一个进程时,它需要保存当前进程的所有状态,即保存当前进程的上下文,以便在再次执行该进程时,能够必得到切换时的状态执行下去。在LINUX中,当前进程上下文均保存在进程的任务数据结构中。在发生中断时,内核就在被中断进程的上下文中,在内核态下执行中断服务例程。但同时会保留所有需要用到的资源,以便中继服务结束时能恢复被中断进程的执行。

以上是关于内核轻探,锁,虚拟地址空间的主要内容,如果未能解决你的问题,请参考以下文章

Linux 内核 内存管理虚拟地址空间布局架构 ⑦ ( Linux 内核地址空间布局简介 )

虚拟地址空间布局架构

内核逻辑地址空间、内核虚拟地址空间和用户虚拟地址空间有啥区别?

Linux 内核 内存管理虚拟地址空间布局架构 ② ( 用户虚拟地址空间组成 | 内存描述符 mm_struct 结构体源码 )

Linux 内核 内存管理虚拟地址空间布局架构 ① ( 虚拟地址空间布局架构 | 用户虚拟地址空间划分 )

I/O模型详细解析