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。这些基础设施一般可以完成特定的任务。既然是用来完成任务的,就必须参与调度。而调度的单位只能是内核线程。所以这些机制虽然对用户来说是一些拿来即用的调用接口,但其执行却是通过特定的内核守护线程执行的。

软中断、tasklet与workqueue

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锁与可抢占锁

资源被抢占的情况有两种:SMP系统下多个CPU的并发访问和一个CPU下的可抢占访问。大部分应用做开发时都是用的一样的锁来锁数据。然而这两种情况有不一样的特点,很多情况下,一个CPU的可抢占锁可以做的更轻巧。

preempt_enable()、preempt_disable()、preempt_enable_no_resched()、preempt_count()、preempt_check_resched()通过这几个函数可以在可抢占单CPU情况下完成锁的工作,就不需要其他种类的锁了。

优先级锁

futex是用户端使用锁的一个很好的选择,然而用户的进程具有不同的优先级,而锁无视所有优先级,信号量可以实现同步概念,但锁没有。然而有些时候希望获得锁在进程上有优先级的区别,这是pi-futex锁提供的功能,叫做优先级继承。是使用futex锁实现的,但是增加了判断进程优先级来确定解锁的优先级。打开这个机能之后效率会显著下降。

自旋锁的SMP处理

当一个自旋锁上很多进程在自旋等待,就可以判断在自旋锁上非常忙。判断的方式是自旋的过程中发现自旋锁的所有者发生了改变,但变成的却不是自己。此时,应该睡眠而不是继续自旋。

lg_local_lock、lg_global_lock

多进程(线程)

由Linux内核对线程和进程没有区别,如果要实现具有单独调度单位的线程,在内核中必须用进程来对应。众所周知的是,在内核看来,每个进程能访问的资源通常是其他进程不知道的,而用户态要求多线程编程需要可以共享内核,Linux内核中解决这个问题的方式是使用一个机制,使得一个进程在创建时可以指定哪些资源可以与其他进程共享。如此模拟实现多线程环境。较新的内核不但可以共享资源,还可以使用unshare系统调用取消共享,也就是说内核从底层让用户端线程脱离进程独立运行成为了可能。

进程资源限制

有一大类需求是限制进程可用的资源。可以限制CPU、内存、文件、行为等。甚至系统调用。

系统调用限制:seccomp_filter

限制进程的可见的系统调用使用seccomp_filter功能。

内核与用户程序通信

netlink

proc

设备节点的read write

ioctrl

系统调用

应用程序间通信

writev/readv

System V IPC

管道

fifo

dbus

unix domain

信号

POSIX IPC

mailbox

模拟现实的邮箱应用。一个进程能给所有其他进程发送邮件,但是只有本进程可以接收发送给自己的邮件。每个进程只有一个邮箱地址,邮件的处理顺序是FIFO。

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-10-26 23:30:24

Linux内核工程导论——进程的相关文章

Linux内核工程导论——进程:ELF文件执行原理(2)

ELF 强符号与弱符号(本小节是转别人的) 我们经常在编程中碰到一种情况叫符号重复定义.多个目标文件中含有相同名字全局符号的定义,那么这些目标文件链接的时候将会出现符号重复定义的错误.比如我们在目标文件A和目标文件B都定义了一个全局整形变量global,并将它们都初始化,那么链接器将A和B进行链接时会报错: 1 b.o:(.data+0x0): multiple definition of `global'2 a.o:(.data+0x0): first defined here 这种符号的定义

Linux内核工程导论——基础架构

基础功能元素 workqueue linux下的工作队列时一种将工作推后执行的方式,其可以被睡眠.调度,与内核线程表现基本一致,但又比内核线程使用简单,一般用来处理任务内容比较动态的任务链.workqueue有个特点是自动的根据CPU不同生成不同数目的队列.每个workqueue都可以添加多个work(使用queue_work函数). 模块支持 模块概述 可访问地址空间,可使用资源, 模块参数 用户空间通过"echo-n ${value} > /sys/module/${modulenam

Linux内核工程导论——用户空间设备管理

用户空间设备管理 用户空间所能见到的所有设备都放在/dev目录下(当然,只是一个目录,是可以变化的),文件系统所在的分区被当成一个单独的设备也放在该目录下.以前的2.4版本的曾经出现过devfs,这个思路非常好,在内核态实现对磁盘设备的动态管理.可以做到当用户访问一个设备的设备的时候,devfs驱动才会去加载该设备的驱动.甚至每个节点的设备号都是动态获得的.但是该机制的作者不再维护他的代码,linux成员经过讨论,使用用户态的udev代替内核态的devfs,所以现在的devfs已经废弃了.用户态

Linux内核工程导论——网络:Netfilter概览

简介 最早的内核包过滤机制是ipfwadm,后来是ipchains,再后来就是iptables/netfilter了.再往后,也就是现在是nftables.不过nftables与iptables还处于争雄阶段,谁能胜出目前还没有定论.但是他们都属于netfilter项目的子成员. 钩子 netfilter基于钩子,在内核网络协议栈的几个固定的位置由netfilter的钩子.我们知道数据包有两种流向,一种是给本机的:驱动接收-->路由表-->本机协议栈-->驱动发送.一种是要转发给别人的:

Linux内核工程导论——内存管理(一)

Linux内存管理 概要 物理地址管理 很多小型操作系统,例如eCos,vxworks等嵌入式系统,程序中所采用的地址就是实际的物理地址.这里所说的物理地址是CPU所能见到的地址,至于这个地址如何映射到CPU的物理空间的,映射到哪里的,这取决于CPU的种类(例如mips或arm),一般是由硬件完成的.对于软件来说,启动时CPU就能看到一片物理地址.但是一般比嵌入式大一点的系统,刚启动时看到的已经映射到CPU空间的地址并不是全部的可用地址,需要用软件去想办法映射可用的物理存储资源到CPU地址空间.

Linux内核工程导论——用户空间进程使用内核资源

本文大部分转载和组装,只是觉得这些知识应该放到一起比较好. 进程系统资源的使用原理 大部分进程通过glibc申请使用内存,但是glibc也是一个应用程序库,它最终也是要调用操作系统的内存管理接口来使用内存.大部分情况下,glibc对用户和操作系统是透明的,所以直接观察操作系统记录的进程对内存的使用情况有很大的帮助.但是glibc自己的实现也是有问题的,所以太特殊情况下追究进程的内存使用也要考虑glibc的因素.其他操作系统资源使用情况则可以直接通过proc文件系统查看. 进程所需要的系统资源种类

Linux内核工程导论——网络:Filter(LSF、BPF)

数据包过滤 LSF(Linux socket filter)起源于BPF(Berkeley Packet Filter),基础从架构一致,但使用更简单.其核心原理是对用户提供了一种SOCKET选项:SO_ATTACH_FILTER.允许用户在某个sokcet上添加一个自定义的filter,只有满足该filter指定条件的数据包才会上发到用户空间.因为sokket有很多种,你可以在各个维度的socket添加这种filter,如果添加在raw socket,就可以实现基于全部IP数据包的过滤(tcp

Linux内核工程导论——内核为何使用C语言

C与C++的对比无数人说过,都说C效率高,但很多人做过实验如果C++不使用RTTI,C++的效率也不会低太多(25%左右).还有人说C++强大的STL,但是对效率讲究点的话那个真的不能用,具体我后面说.一般大部分人的心态是,学C++出身的,就经常吐槽linux的C代码乱的一塌糊涂,各种敏捷,面向对象原则,代码不如C++精简,连个STL或者boost都用不上,等软件工程相关问题都是被他们吐槽的重灾区,这些人肯定没有给内核贡献过代码.做嵌入式一直用C的,接触内核后的反应是:啊,内核真大,啊,内核真难

Linux内核工程导论——UIO

要开启hugepages文件系统,这个文件系统要使用mmap来映射页,可以显著的减少缺页中断. UIO介绍 UIO是一个在用户端实现内核驱动的机制.其在内核中有一个模块支持uio模块.现在这个模块只支持字符设备.用户可以添加多个uio设备(用户端的设备驱动),每个设备在/dev/uioX,X为数字,第一个为0,依次类推.我们知道设备都是靠中断来响应的,响应uio设备中断的方法是读取/dev/uioX文件,没有中断的时候读取会阻塞,来中断的时候会读取到整数值,代表已经发生的中断的次数. 但是这只是