标签:linux kernel 内核 linux
linux是个多进程的环境,不但用户空间可以有多个进程,而且内核内部也可以有内核进程。linux内核中线程与进程没有区别,因此叫线程和进程都是一样的。调度器调度的是CPU资源,按照特定的规则分配给特定的进程。然后占有CPU资源的资源去申请或使用硬件或资源。因此这里面涉及到的几个问题:
对于调度器来说:
l 调度程序在运行时,如何确定哪一个程序将被调度来使用CPU资源?
n 如何不让任何一个进程饥饿?
n 如何更快的定位和响应交互式进程?
l 单个CPU只有一个流水线,但能否一次调度多个进程同时使用多个CPU的物理资源?
l 调度来的CPU如何让其释放资源?是任其自己释放还是有相关回收机制?
对于希望被调度的进程来说:
l 如何定义自己被调度的概率?
l 如何在等待被调度的同时接收信号?
l 如何避免自己希望占有的资源在自己没有被调度时不被别的进程占用?或者SMP环境下没有与其同时使用同一资源的进程?
分为分时系统和实时系统两种。linux本身不是实时系统,但是本着兼容并包的原则,linux也实现了实时系统的接口。
对于整个内核来说,调度策略包括:SCHED_NORMAL、SCHED_FIFO、SCHED_RR、SCHED_BATCH四种。而标准的调度策略还有两种linux没有实现:SCHED_IDLE、SCHED_DEADLINE。SCHED_NORMAL就是默认的我们最常说的分时的调度策略。
SCHED_IDLE的进程将会在没有任何非SCHED_IDLE进程存在的情况下执行。该等级通常用于类似磁盘整理等不能影响用户的后台时间不敏感操作。但是linux内核并没有实现。
SCHED_NORMAL有完全公平和针对用户交互优化调整优先级两种情况。一般我们常用的是要针对用户做动态优先级调整的。
无论是实时的还是普通的,优先级都是由数值表示的。普通的静态优先级全部为0,区别普通调度程序可以用动态优先级。实时调度的程序优先级为1-99,也就是说任何一个实时程序的优先级都高于普通程序。
当使用SCHED_RR时,时间片流转的,虽然也有优先级的数字,但是即使是最高优先级的进程在时间片用完的时候也会释放CPU。而SCHED_FIFO,除非主动释放,否则具有最高优先级的进程永远不会释放CPU(等待IO完成除外)。两者当存在更高优先级进程时都会被抢占。
前面说了SCHED_IDLE调度方式没有实现,那么linux如何实现后台磁盘整理等操作?答案是功能类似的SCHED_BATCH调度方式。这种调度方式并不会在有正常程序的时候完全不执行,但是其会保证正常程序的执行和交互程序的响应。也即适合GCC等编译操作。
你可以通过内核提供的API设置调度调度办法,也可以通过命令行。命令是chrt。你还可以配置实时进程的最大时间占用情况,因为如果实时进程出现bug,最高优先级的进程几乎不可能释放CPU,导致系统卡死。通过sysctl调用可以设置kernel.sched_rt_period_us等参数可以配置最大的实时调度进程占用的CPU情况。
通过搭配cgroup和进程调度,还可以实现按照cgroup进行CPU资源的配置方式。这也是通过cgroup文件系统完成的。
内核中很多操作都是使用一些内核基础设施完成的。例如workqueue、tasklet、softirq。这些基础设施一般可以完成特定的任务。既然是用来完成任务的,就必须参与调度。而调度的单位只能是内核线程。所以这些机制虽然对用户来说是一些拿来即用的调用接口,但其执行却是通过特定的内核守护线程执行的。
linux中中断分为上下两部分,下半部分可以关中断,产生上半部分的中断任务。上半部分不需要关中断,可以调度执行。这样的原因是系统中关中断的时间必须短,否则就会失去响应。产生的软中断被加入内核守护线程ksoftirqd的执行队列。这个线程后续会调度执行相关软中断。tasklet与软中断类似,只是在SMP系统中,软中断是可以被多个CPU一起执行的,是可重入的,而tasklet一次只允许一个CPU执行,是不可重入的。用户可以根据软中断是否允许重入决定是否使用tasklet或softirq。
特殊的,softirq和tasklet不能睡眠,所以不能使用信号量或其他阻塞函数。因为他们对应的都是由一个内核线程执行的(ksoftirqd),如果阻塞了,系统将无法响应其他软中断。而工作队列workqueue本身就是作为一个可用的单元提供给用户,一个workqueue就是一个内核线程。内核模块可以生成一个workqueue,然后添加自己的任务进去。也可以使用内核已经有的workqueue,向其中添加任务。workqueue是一个容器,内核模块可以向已有的workqueue中添加任务。该workqueue就会调度执行自己的子任务。可以说是进程中的进程。
内核中的资源锁有:自旋锁、信号量、互斥锁、读写锁rwlock、顺序锁、RCU锁、Futex锁。
这些锁分别用来解决不同类型的问题:
l 软中断中多个CPU同时访问同一资源。由于软中断不能睡眠,因此在多个CPU抢用统一资源时不能用其他锁,只能忙等,这就是自旋锁。
l 普通进程竞争资源时,该资源无论读写,在同一时间只能有一个或几个进程获得。这就是互斥锁和信号量(信号量为1时就是互斥锁)
l 当互斥不是很频繁的时候,希望不必每次都进入内核。就有Futex锁
l 同一个资源希望读和写分开处理。就是读写锁和顺序锁和RCU
不同的锁服务于不同的目的和场景。实际上linux只是应用资源锁思想的一部分,操作系统原理是一门学科,其有多种方式用于处理资源锁问题。
资源锁本质上是同步和互斥问题。从上面可以看出,大部分是处理同时写的问题。所以只要能保证比较并写的操作是原子的,线程就可以是无锁的。Intel已经实现了类似的指令,如cmpxchg8,在一个周期内完成比较和写操作就可以保证不发生并发写冲突。
同样的思想,linux也提供了两组原子操作,一组针对整数,一组针对位。合理的利用原子操作就可以避免大部分的锁应用场景。自旋锁看起来代价很大,一个运行时需要两外的CPU空转等待,但是在要锁住的代码量很少的时候,由于自旋锁的轻量级,就比使用信号量代价小很多。所以,自旋锁不仅用于软中断,还可以用于加锁很小的一段代码的时候。
除了自旋锁,还有一种锁需要忙等,就是顺序锁。严格的说这不是忙等,而是使用了一个巧妙而又非常简单的思想,在读之前看锁值,在读之后看锁值,如果不变化,就表明读的过程中,读的值没有被写,就重读。写的时候就会改变锁值。原理相当于自旋锁,但是可以允许多个写,读操作在多个写操作全部完成后才能读得正确的值。
但是当要加锁的是大块的逻辑时,就的需要信号量这种重量级的锁。但是,一般的逻辑都应该尽量避免大块锁。现实中,也可以通过精细的设计来避免大块锁。
RCU锁直接不阻塞写,前面的顺序锁已经是改进的读写锁了,但同时也只能有一个写。但RCU锁允许不阻塞写操作,多个写的时候不是写到同一个地方,而是拷贝一份新的数据写。读还继续读旧的,如此以内存的使用增多为代价换来读写都不阻塞。
还有一种仅由用户空间进程使用的锁futex。使用这个锁可以完全取代用户空间的各种锁。因为其高效,行为又符合要求。futex的原理其实是考虑到用户态之前使用的信号量等锁都是内核中的一个变量,每次查询的时候都要进入内核态,还要再出来。fitex的思想就是直接将内核态的这个锁变量mmap映射到用户进程空间,如此,各个用户进程就可以在自己的空间直接查询这个值而不用进入内核就可以知道有没有人在用。读取虽然是大家都随便读,但是写入考虑到多个进程操作一个变量的可能冲突,linux是提供的API陷入内核来加锁写入的。虽然最后还是要陷入内核,但是其判断部分可以不进入内核完成,而大部分进入的情况判断资源是没有并发访问的。特殊应用场景除外。
信号量有个问题是,如果多个CPU获得读锁,则信号量本身会在各个cpu的cache中不断的刷新,造成效率的下降。解决的方式内核定义了一个新型的信号量:percpu-rw-semaphore。
互斥概念与同步概念必须要区别开。互斥只同一时间只有一个进程可以访问资源,没有时序概念,而同步包含了多个访问该资源的进程的访问的先后顺序,有你结束了轮到我的意思。互斥只是你还没结束我就没法开始。信号量是同步概念的,因为未得到资源的进程会睡眠等待。其他的内核锁是互斥概念的(自旋、顺序),因为得不到就阻塞,或者是让其永远可以得到(RCU)。
资源被抢占的情况有两种:SMP系统下多个CPU的并发访问和一个CPU下的可抢占访问。大部分应用做开发时都是用的一样的锁来锁数据。然而这两种情况有不一样的特点,很多情况下,一个CPU的可抢占锁可以做的更轻巧。
preempt_enable()、preempt_disable()、preempt_enable_no_resched()、preempt_count()、preempt_check_resched()通过这几个函数可以在可抢占单CPU情况下完成锁的工作,就不需要其他种类的锁了。
futex是用户端使用锁的一个很好的选择,然而用户的进程具有不同的优先级,而锁无视所有优先级,信号量可以实现同步概念,但锁没有。然而有些时候希望获得锁在进程上有优先级的区别,这是pi-futex锁提供的功能,叫做优先级继承。是使用futex锁实现的,但是增加了判断进程优先级来确定解锁的优先级。打开这个机能之后效率会显著下降。
当一个自旋锁上很多进程在自旋等待,就可以判断在自旋锁上非常忙。判断的方式是自旋的过程中发现自旋锁的所有者发生了改变,但变成的却不是自己。此时,应该睡眠而不是继续自旋。
lg_local_lock、lg_global_lock
由Linux内核对线程和进程没有区别,如果要实现具有单独调度单位的线程,在内核中必须用进程来对应。众所周知的是,在内核看来,每个进程能访问的资源通常是其他进程不知道的,而用户态要求多线程编程需要可以共享内核,Linux内核中解决这个问题的方式是使用一个机制,使得一个进程在创建时可以指定哪些资源可以与其他进程共享。如此模拟实现多线程环境。较新的内核不但可以共享资源,还可以使用unshare系统调用取消共享,也就是说内核从底层让用户端线程脱离进程独立运行成为了可能。
有一大类需求是限制进程可用的资源。可以限制CPU、内存、文件、行为等。甚至系统调用。
限制进程的可见的系统调用使用seccomp_filter功能。
proc
设备节点的read write
ioctrl
系统调用
writev/readv
System V IPC
管道
fifo
dbus
unix domain
信号
POSIX IPC
模拟现实的邮箱应用。一个进程能给所有其他进程发送邮件,但是只有本进程可以接收发送给自己的邮件。每个进程只有一个邮箱地址,邮件的处理顺序是FIFO。
版权声明:本文为博主原创文章,未经博主允许不得转载。
标签:linux kernel 内核 linux
原文地址:http://blog.csdn.net/ljy1988123/article/details/48064267