面经 cisco

1. 优先级反转问题及解决方法php

(1)什么是优先级反转 css

简单从字面上来讲,就是低优先级的任务先于高优先级的任务执行了,优先级搞反了。那在什么状况下会生这种状况呢? html

假设三个任务准备执行,A,B,C,优先级依次是A>B>C; vue

首先:C处于运行状态,得到CPU正在执行,同时占有了某种资源; node

其次:A进入就绪状态,由于优先级比C高,因此得到CPU,A转为运行状态;C进入就绪状态; linux

第三:执行过程当中须要使用资源,而这个资源又被等待中的C占有的,因而A进入阻塞状态,C回到运行状态; ios

第四:此时B进入就绪状态,由于优先级比C高,B得到CPU,进入运行状态;C又回到就绪状态; c++

第五:若是这时又出现B2,B3等任务,他们的优先级比C高,但比A低,那么就会出现高优先级任务的A不能执行,反而低优先级的B,B2,B3等任务能够执行的奇怪现象,而这就是优先反转。 git

(2)如何解决优先级反转 程序员

高优先级任务A不能执行的缘由是C霸占了资源,而C若是不能得到CPU,不释放资源,那A也只好一直等在那,因此解决优先级反转的原则确定就是让C尽快执行,尽早把资源释放了。基于这个原则产生了两个方法:

2.1 优先级继承

当发现高优先级的任务由于低优先级任务占用资源而阻塞时,就将低优先级任务的优先级提高到等待它所占有的资源的最高优先级任务的优先级。

2.2 优先级天花板

优先级天花板是指将申请某资源的任务的优先级提高到可能访问该资源的全部任务中最高优先级任务的优先级.(这个优先级称为该资源的优先级天花板)

2.3 二者的区别

优先级继承:只有一个任务访问资源时一切照旧,没有区别,只有当高优先级任务由于资源被低优先级占有而被阻塞时,才会提升占有资源任务的优先级;而优先级天花板,不管是否发生阻塞,都提高,即谁先拿到资源,就将这个任务提高到该资源的天花板优先级。

 

2. 红黑树比通常树好在哪,为何树要平衡

平衡二叉树和红黑树最差状况分析

1.经典平衡二叉树

平衡二叉树(又称AVL树)是带有平衡条件的二叉查找树,使用最多的定理为:一棵平衡二叉树是其每一个节点的左子树和右子树的高度最多差为1的二叉查找树。由于他是二叉树的一种具体应用,因此他一样具备二叉树的性质。例如,一棵满二叉树在第k层最多可拥有个节点(性质1)。一棵树的高度为其从根节点到最底层节点通过的路径数(例如只含一个节点的树的高度为0)(性质2)。而且已被证实,一棵含有N个节点的平衡二叉树的高度最多(粗略来讲)为。下面咱们来尝试总结如何获得一个高度差最大的平衡二叉树(查找性能最差)。

由平衡二叉树的定义可知,左子树和右子树最多能够相差1层高度,那么多个在同一层的子树就能够依次以相差1层的方式来递减子树的高度,以下图所示是一个拥有4棵子树的树的层高最大差情形(图1):

 

图1 拥有4颗子树的平衡二叉树最大高度差

该图虚线框中的子树,最左端的节点树高度为0,最右端的节点树的高度为2,所以该平衡二叉树的内部子树最大高度差为2

利用这样的性质,咱们就能够依次递推,1棵子树最大高度差为0层,23棵子树最大高度差为1层,四、五、六、7棵子树最大高度差为2层,8至15棵子树最大高度差为3层,16至31棵子树最大高度差为4......

假设n为子树的数量,m为最大高度差,可得:

 

进一步分析,假设一棵高为h的平衡二叉树的最大高度差为m(假设最小的子树高度为0),且m的高度差由n(此处只考虑分界点情形,即n为2的幂级数)棵子树达成。由平衡二叉树的性质(性质1和性质2),可得以下式:


化简后最终能够获得一个简单的结论(彷佛这个结论在本文以前没人关注过,此处仅仅考虑了最差状况,不过对于实际应用和性能分析已经彻底够用,详细完整的数学证实感兴趣的读者能够尝试证实):

也就是说,一棵高为h的平衡二叉树,其内部子树的最大高度差能够达到(结果取整,不舍入)。举例来讲,一棵高度为8的平衡二叉树,其内部子树的最大高度差为4,同理,一棵高度为9的平衡二叉树,其内部子树的最大高度差为5,以下示(图2):

图2 高度为 9的平衡二叉树的最大高度差

2.红黑树

历史上AVL树流行的一种变种是红黑树(red black tree)。红黑树也是许多编程语言底层实现采纳较多的数据结构(例如JavaTreeSetTreeMap实现)。红黑树是具备下列着色性质的二叉查找树:

1.每个节点或为黑色或为红色。

2.根节点时黑色的。

3.一个红色节点的儿子节点必须所有是黑色。

4.从任意一个节点到一个null的每一条路径必须包含相同数目的黑色节点。

以上着色法则的一个结论是:红黑树的高度最多为,这个结论彷佛还不如经典平衡二叉树。下面咱们来分析由以上着色法则规定的红黑树的最差情形。

由规则4可知,只要在一个节点的一侧子树尽量多的使用红色节点,而另外一侧尽量少甚至不使用红色节点,就能够拉开左右子树的高度差,以下图所示(图3):

图3 红黑树高度差造成缘由

则咱们能够显而易见的获得一个结论:一棵含有k个红色节点的红黑树,理论上其内部子树最大高度差就能够达到k

既然红黑树理论上缺陷如此大,那为何实际应用中反而采纳较多?深刻研究红黑树的具体实现方式,能够发现,红黑树在实际应用中的实现形式已经超出其本来定义的规则。红黑树的理论阐述不够完善,也是其难于理解和新手难以本身动手实现的缘由之一。(注:本文采纳Mark Allen Weiss的《Data Structures and Algorithm Analysis in Java》一书当中对于红黑树的实现方式,这也是实际中使用最多的实现方式)。

通过个人概括总结,现实中的红黑树在实现过程当中增长了如下限制条件:

5.新插入的节点必须为红色。

6.任意节点其左右子树最多相差2层红节点。

7.插入过程(仅限于插入点那条路径上)中不容许任一节点有2个红色儿子节点。

增长了以上三条限制条件的红黑树甚至都不须要再多加分析了。规则67创建在规则5和红黑树复杂的插入调整的基础之上,规则6恰巧直接阻止了经典平衡二叉树出现最差情形的可能性,规则7甚至是对红黑树到AA树(AA树实现尚不完善,目前实际性能较差,本文不作深刻讨论)的一种过渡。如下图展现了连续向红黑树中插入右节点(依次插入30,40,50,60,70,80,90,100)的变化过程(图4,图5):

图4 依次向红黑树插入 30,40,50,60情形
图5 继续向红黑树插入 70,80,90,100情形

3.性能测试

本文的测试环境为Window7操做系统,代码所有使用Java编写,jdk版本为1.8.0,测试均采用随机数生成器产生的九位数数字数据。为尽量排除编译器优化以及操做系统调度形成的影响,每一项测试均运行100遍取平均时间,例如对于万级测试:生成100次不一样的1000个随机九位数,将每次生成的结果分别对平衡二叉树、红黑树、AA树进行树的构造,构造完成后从新循环这10000个数进行平均查找时间的统计,在查找期间还会生成节点数据量十分之一(此处即1000)个假数据来保证查找不到的状况也被统计进查找时间,也就是说万级测试是在操做1100000次后得出的结果。最终测试结果以下:

注:AA树的各项性能均介于红黑树和平衡二叉树之间。可是AA树的深度受随机生成的数字影像波动较大(最差状况出现次数多,而且最差状况树高最高)。

 

由此总结以下(若是前文都没看懂没有关系,记住下面的结论):

性能:红黑树>平衡二叉树>AA树;

编程实现难度:红黑树>平衡二叉树>AA树。

虽然各类树的实现以及具体应用千差万别,可是没有最好的数据结构只有最合适的数据结构,此处比较的三种查找树在实际应用中性能上只有细微的差异(最差状况出现的几率毕竟很是小),在生产实践中彻底不会带来性能的明显缺陷,所以选择合理的实现方式,保证程序的功能健全性,才是选择数据结构的最重要选择因素。

 

 

红黑树属于平衡二叉树。它不严格是由于它不是严格控制左、右子树高度或节点数之差小于等于1,但红黑树高度依然是平均log(n),且最坏状况高度不会超过2log(n)。

红黑树(red-black tree) 是一棵知足下述性质的二叉查找树:

1. 每个结点要么是红色,要么是黑色。

2. 根结点是黑色的。

3. 全部叶子结点都是黑色的(实际上都是Null指针,下图用NIL表示)。叶子结点不包含任何关键字信息,全部查询关键字都在非终结点上。

4. 每一个红色结点的两个子节点必须是黑色的。换句话说:从每一个叶子到根的全部路径上不能有两个连续的红色结点

5. 从任一结点到其每一个叶子的全部路径都包含相同数目的黑色结点

 

红黑树相关定理

1. 从根到叶子的最长的可能路径很少于最短的可能路径的两倍长。

      根据上面的性质5咱们知道上图的红黑树每条路径上都是3个黑结点。所以最短路径长度为2(没有红结点的路径)。再根据性质4(两个红结点不能相连)和性质1,2(叶子和根必须是黑结点)。那么咱们能够得出:一条具备3个黑结点的路径上最多只能有2个红结点(红黑间隔存在)。也就是说黑深度为2(根结点也是黑色)的红黑树最长路径为4,最短路径为2。从这一点咱们能够看出红黑树是 大体平衡的。 (固然比平衡二叉树要差一些,AVL的平衡因子最多为1)

 

2. 红黑树的树高(h)不大于两倍的红黑树的黑深度(bd),即h<=2bd

      根据定理1,咱们不难说明这一点。bd是红黑树的最短路径长度。而可能的最长路径长度(树高的最大值)就是红黑相间的路径,等于2bd。所以h<=2bd。

 

3. 一棵拥有n个内部结点(不包括叶子结点)的红黑树的树高h<=2log(n+1)

      下面咱们首先证实一颗有n个内部结点的红黑树知足n>=2^bd-1。这能够用数学概括法证实,施概括于树高h。当h=0时,这至关因而一个叶结点,黑高度bd为0,而内部结点数量n为0,此时0>=2^0-1成立。假设树高h<=t时,n>=2^bd-1成立,咱们记一颗树高 为t+1的红黑树的根结点的左子树的内部结点数量为nl,右子树的内部结点数量为nr,记这两颗子树的黑高度为bd'(注意这两颗子树的黑高度必然一 样),显然这两颗子树的树高<=t,因而有nl>=2^bd'-1以及nr>=2^bd'-1,将这两个不等式相加有nl+nr>=2^(bd'+1)-2,将该不等式左右加1,获得n>=2^(bd'+1)-1,很显然bd'+1>=bd,因而前面的不等式能够 变为n>=2^bd-1,这样就证实了一颗有n个内部结点的红黑树知足n>=2^bd-1。

        在根据定理2,h<=2bd。即n>=2^(h/2)-1,那么h<=2log(n+1)

        从这里咱们可以看出,红黑树的查找长度最多不超过2log(n+1),所以其查找时间复杂度也是O(log N)级别的。



红黑树的操做

 

由于每个红黑树也是一个特化的二叉查找树,所以红黑树上的查找操做与普通二叉查找树上的查找操做相同。然而,在红黑树上进行插入操做和删除操做会致使不 再符合红黑树的性质。恢复红黑树的属性须要少许(O(log n))的颜色变动(实际是很是快速的)和不超过三次树旋转(对于插入操做是两次)。 虽然插入和删除很复杂,但操做时间仍能够保持为 O(log n) 次 。

 

红黑树的优点

 

红黑树可以以O(log2(N))的时间复杂度进行搜索、插入、删除操做。此外,任何不平衡都会在3次旋转以内解决。这一点是AVL所不具有的。

并且实际应用中,不少语言都实现了红黑树的数据结构。好比 TreeMap, TreeSet(Java )、 STL(C++)等。

 

 

3. tcp为何要3次握手,若是不是会出什么问题,举例说明

为何不能用两次握手进行链接?

咱们知道,3次握手完成两个重要的功能,既要双方作好发送数据的准备工做(双方都知道彼此已准备好),也要容许双方就初始序列号进行协商,这个序列号在握手过程当中被发送和确认。
    如今把三次握手改为仅须要两次握手,死锁是可能发生的。做为例子,考虑计算机S和C之间的通讯,假定C给S发送一个链接请求分组,S收到了这个分组,并发 送了确认应答分组。按照两次握手的协定,S认为链接已经成功地创建了,能够开始发送数据分组。但是,C在S的应答分组在传输中被丢失的状况下,将不知道S 是否已准备好,不知道S创建什么样的序列号,C甚至怀疑S是否收到本身的链接请求分组。在这种状况下,C认为链接还未创建成功,将忽略S发来的任何数据分 组,只等待链接确认应答分组。而S在发出的分组超时后,重复发送一样的分组。这样就造成了死锁。

 

设想:若是只有两次握手,那么第二次握手后服务器只向客户端发送ACK包,此时客户端与服务器端创建链接。在这种握手规则下: 
        假设:若是发送网络阻塞,因为TCP/IP协议定时重传机制,B向A发送了两次SYN请求,分别是x1和x2,且由于阻塞缘由,致使x1链接请求和x2链接请求的TCP窗口大小和数据报文长度不一致,若是最终x1达到A,x2丢失,此时A同B创建了x1的链接,这个时候,由于AB已经链接,B没法知道是请求x1仍是请求x2同B链接,若是B默认是最近的请求x2同A创建了链接,此时B开始向A发送数据,数据报文长度为x2定义的长度,窗口大小为x2定义的大小,而A创建的链接是x1,其数据包长度大小为x1,TCP窗口大小为x1定义,这就会致使A处理数据时出错。
        很显然,若是A接收到B的请求后,A向B发送SYN请求y3(y3的窗口大小和数据报长度等信息为x1所定义),确认了链接创建的窗口大小和数据报长度为x1所定义,A再次确认回答创建x1链接,而后开始相互传送数据,那么就不会致使数据处理出错了。

 

 

 

4. timewait状态的理解

这里写图片描述

1. time_wait状态如何产生?
由上面的变迁图,首先调用close()发起主动关闭的一方,在发送最后一个ACK以后会进入time_wait的状态,也就说该发送方会保持2MSL时间以后才会回到初始状态。MSL值得是数据包在网络中的最大生存时间。产生这种结果使得这个TCP链接在2MSL链接等待期间,定义这个链接的四元组(客户端IP地址和端口,服务端IP地址和端口号)不能被使用。

2.time_wait状态产生的缘由

1)为实现TCP全双工链接的可靠释放

由TCP状态变迁图可知,假设发起主动关闭的一方(client)最后发送的ACK在网络中丢失,因为TCP协议的重传机制,执行被动关闭的一方(server)将会重发其FIN,在该FIN到达client以前,client必须维护这条链接状态,也就说这条TCP链接所对应的资源(client方的local_ip,local_port)不能被当即释放或从新分配,直到另外一方重发的FIN达到以后,client重发ACK后,通过2MSL时间周期没有再收到另外一方的FIN以后,该TCP链接才能恢复初始的CLOSED状态。若是主动关闭一方不维护这样一个TIME_WAIT状态,那么当被动关闭一方重发的FIN到达时,主动关闭一方的TCP传输层会用RST包响应对方,这会被对方认为是有错误发生,然而这事实上只是正常的关闭链接过程,并不是异常。

2)为使旧的数据包在网络因过时而消失

为说明这个问题,咱们先假设TCP协议中不存在TIME_WAIT状态的限制,再假设当前有一条TCP链接:(local_ip, local_port, remote_ip,remote_port),因某些缘由,咱们先关闭,接着很快以相同的四元组创建一条新链接。本文前面介绍过,TCP链接由四元组惟一标识,所以,在咱们假设的状况中,TCP协议栈是没法区分先后两条TCP链接的不一样的,在它看来,这根本就是同一条链接,中间先释放再创建的过程对其来讲是“感知”不到的。这样就可能发生这样的状况:前一条TCP链接由local peer发送的数据到达remote peer后,会被该remot peer的TCP传输层当作当前TCP链接的正常数据接收并向上传递至应用层(而事实上,在咱们假设的场景下,这些旧数据到达remote peer前,旧链接已断开且一条由相同四元组构成的新TCP链接已创建,所以,这些旧数据是不该该被向上传递至应用层的),从而引发数据错乱进而致使各类没法预知的诡异现象。做为一种可靠的传输协议,TCP必须在协议层面考虑并避免这种状况的发生,这正是TIME_WAIT状态存在的第2个缘由。

3)总结
具体而言,local peer主动调用close后,此时的TCP链接进入TIME_WAIT状态,处于该状态下的TCP链接不能当即以一样的四元组创建新链接,即发起active close的那方占用的local port在TIME_WAIT期间不能再被从新分配。因为TIME_WAIT状态持续时间为2MSL,这样保证了旧TCP链接双工链路中的旧数据包均因过时(超过MSL)而消失,此后,就能够用相同的四元组创建一条新链接而不会发生先后两次链接数据错乱的状况。

3.time_wait状态如何避免

首先服务器能够设置SO_REUSEADDR套接字选项来通知内核,若是端口忙,但TCP链接位于TIME_WAIT状态时能够重用端口。在一个很是有用的场景就是,若是你的服务器程序中止后想当即重启,而新的套接字依旧但愿使用同一端口,此时SO_REUSEADDR选项就能够避免TIME_WAIT状态。

 

关于tcp中time_wait状态的4个问题

time_wait是个常问的问题。tcp网络编程中最不easy理解的也是它的time_wait状态,这也说明了tcp/ip四次挥手中time_wait状态的重要性。
如下经过4个问题来描写叙述它


问题

1.time_wait状态是什么

2.为何会有time_wait状态

3.哪一方会有time_wait状态

4.怎样避免time_wait状态占用资源


1.time_wait状态是什么

简单来讲:time_wait状态是四次挥手中server向client发送FIN终止链接后进入的状态。

下图为tcp四次挥手过程
这里写图片描写叙述
可以看到time_wait状态存在于client收到serverFin并返回ack包时的状态
当处于time_wait状态时,咱们没法建立新的链接,因为port被占用。


2.为何会有time_wait状态

time_wait存在的缘由有两点
1.可靠的终止TCP链接。
2.保证让迟来的TCP报文段有足够的时间被识别并丢弃。

1.可靠的终止TCP链接,若处于time_wait的client发送给server确认报文段丢失的话,server将在此又一次发送FIN报文段,那么client必须处于一个可接收的状态就是time_wait而不是close状态。
2.保证迟来的TCP报文段有足够的时间被识别并丢弃,linux 中一个TCPport不能打开两次或两次以上。当client处于time_wait状态时咱们将没法使用此port创建新链接,假设不存在time_wait状态,新链接可能会收到旧链接的数据。

 

3.哪一方会有time_wait状态

time_wait状态是通常有client的状态。

而且会占用port
有时产生在server端,因为server主动断开链接或者发生异常


4.怎样避免time_wait状态占用资源

假设是client,咱们通常不用操心,因为client通常选用暂时port。再次建立链接会新分配一个port。

除非指定client使用某port,只是通常不需要这么作。

假设是server主动关闭链接后异常终止。则因为它老是使用用一个知名serverport号,因此链接的time_wait状态将致使它不能从新启动。只是咱们可以经过socket的选项SO_REUSEADDR来强制进程立刻使用处于time_wait状态的链接占用的port。
经过socksetopt设置后,即便sock处于time_wait状态,与之绑定的socket地址也可以立刻被重用。

此外也可以经过改动内核參数/proc/sys/net/ipv4/tcp_tw/recycle来高速回收被关闭的socket,从而是tcp链接根本不进入time_wait状态,进而赞成应用程序立刻重用本地的socket地址。

 

 

谈谈TCP中的TIME_WAIT

在服务端可能会常常遇到有不少处于TIMEWAIT状态的TCP链接。若是上网一搜索,能够找到有不少关于处理TIMEWAIT不正确的博文(包括本文),不少文章就放了几个调整参数。至于这些参数有什么用,为何要调整为那个值就没有深刻地介绍了。这就好像生了病不去找医生了解病情,而是随便从别人的药箱里面找点药来吃,看看有没有效果,也无论别人的药是否过时,是否对症。因此,本文也来凑个热闹,来谈谈TIME_WAIT。

为何要有TIME_WAIT?

TIME_WAIT是TCP主动关闭链接一方的一个状态,TCP断开链接的时序图以下:image

当主动断开链接的一方(Initiator)发送FIN包给对方,且对方回复了ACK+FIN,而后Initiator回复了ACK后就进入TIME_WAIT状态,一直将持续2MSL后进入CLOSED状态。

那么,咱们来看若是Initiator不进入TIME_WAIT状态而是直接进入CLOSED状态会有什么问题?

考虑这种状况,服务器运行在80端口,客户端使用的链接端口是12306,数据传输完毕后服务端主动关闭链接,可是没有进入TIME_WAIT,而是直接计入CLOSED了。这时,客户端又经过一样的端口12306与服务端创建了一个新的链接。假如上一个链接过程当中网络出现了异常,致使了某个包重传并延时到达了服务端,这时服务端就没法区分这个包是上一个链接的仍是这个链接的。因此,主动关闭链接一方要等待2MSL,而后才能CLOSE,保证链接中的IP包都要么传输完成,要么被丢弃了。

TIME_WAIT会带来什么问题

系统中TIME_WAIT的链接数不少,会致使什么问题呢?这要分别针对客户端和服务器端来看的。

首先,若是是客户端发起了链接,传输完数据而后主动关闭了链接,这时这个链接在客户端就会处于TIMEWAIT状态,同时占用了一个本地端口。若是客户端使用短链接请求服务端的资源或者服务,客户端上将有大量的链接处于TIMEWAIT状态,占用大量的本地端口。最坏的状况就是,本地端口都被用光了,这时将没法再创建新的链接。

针对这种状况,对应的解决办法有2个:
1. 使用长链接,若是是http,能够使用keepalive
2. 增长本地端口可用的范围,好比Linux中调整内核参数:net.ipv4.ip_local_port_range

对于服务器而已,因为服务器是被动等待客户端创建链接的,所以即便服务器端有不少TIME_WAIT状态的链接,也不存在本地端口耗尽的问题。大量的TIME_WAIT的链接会致使以下问题:
1. 内存占用:由于每个TCP链接都会有占用一些内存。
2. 在某些Linux版本上可能致使性能问题,由于数据包到达服务器的时候,内核须要知道数据包是属于哪一个TCP链接的,在某些Linux版本上可能会遍历全部的TCP链接,因此大量TIME_WAIT的链接将致使性能问题。不过,如今的内核都对此进行了优化(待确认)。

那系统中处于TIME_WAIT状态的TCP链接数有上限吗?有的,这是经过net.ipv4.tcp_max_tw_buckets参数来控制的,默认值为180000。当超过了之后,系统就开始关闭这些链接,同时会在系统日志中打印日志。此时,能够将这个值调大一些,从这个参数的默认值就能够看出,对服务器而已,处于TIME_WAIT状态的TCP链接多点也没有什么关系,只是多占用些内存而已。

常见的TIMEWAIT错误参数

若是用TIME_WAIT做为关键字到网络上搜索,会获得不少关于如何减小TIME_WAIT数量的建议,其中有些建议是有错误或者有风险的,列举以下:

  1. net.ipv4.tcp_syncookies = 1,这个参数表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少许SYN攻击。这个和TIME_WAIT没有什么关系。
  2. net.ipv4.tcp_tw_reuse = 1,这个参数表示重用TIME_WAIT的链接,重用的条件是TCP的4元组(源地址、源端口、目标地址、目标端口)要彻底一致,并且开启了net.ipv4.tcp_timestamps,且新创建链接的使用的timestamp要大于当前链接的timestamp。因此,开启了这个参数对减小TIME_WAIT的TCP链接有点用,但条件太苛刻,因此实际用处不大。
  3. net.ipv4.tcp_tw_recycle = 1,这个参数表示开启TIME_WAIT回收功能,开启了这个参数后,将大大减少TIME_WAIT进入CLOSED状态的时间。可是开启了这个功能了风险很大,可能会致使处于NAT后面的某些客户端没法创建链接。由于,开启这个功能后,它要求来自同一个IP的TCP新链接的timestamp要大于以前链接的timestamp。

TIMEWAIT的“正确”处理方法

简单总结一下我对于TIME_WAIT状态TCP链接的理解和处理方法:
1. TIME_WAIT状态的设计初衷是为了保护咱们的。服务端没必要担忧系统中有几w个处于TIME_WAIT状态的TCP链接。能够调大net.ipv4.tcp_max_tw_buckets这个参数。
2. 使用短链接的客户端,须要关注TIME_WAIT状态的TCP链接,建议是采用长链接,同时调节参数net.ipv4.ip_local_port_range,增长本地可用端口的范围。
3. 能够开启net.ipv4.tcp_tw_reuse这个参数,可是实际用处有限。
4. 不要开启net.ipv4.tcp_tw_recycle这个参数,它带来的问题比用处大。
5. 某些Linux的发行版能够调节TIME_WAIT到CLOSED的等待时间(好比Ali的Linux内核提供了参数net.ipv4.tcp_tw_timeout
),能够稍微调小一点这个参数。

 

 

5. linux内核进程调度

1.    Linux进程和线程如何建立、退出?进程退出的时候,本身没有释放的资源(如内存没有free)会怎样?

解答:

Linux进程经过fork来建立

Linux线程经过pthread_create建立,

 

2.    什么是写时拷贝?

解答:

写时拷贝(copy-on-write, COW)就是等到修改数据时才真正分配内存空间,这是对程序性能的优化,能够延迟甚至是避免内存拷贝,固然目的就是避免没必要要的内存拷贝。

Linux 的fork系统调用就使用了写时拷贝技术,具体细节以下:

如今有一个父进程P1,这是一个主体,那么它是有灵魂也是有身体的。如今在其虚拟地址空间(有相应的数据结构表示)上有:正文段,数据段,堆,栈这四个部分,相应地,内核要为这四个部分分配给自的物理块。即正文段块、数据段块、堆块、栈块。

1)如今P1用fork()函数为进程建立一个子进程P2

内核:

(1) 复制P1的正文段,数据段,堆,栈这四个部分,注意是其内容相同。

(2) 为这四个部分分配物理块,P2的:正文段(为P1的正文段的物理块,其实就是不为P2分配正文段块,让P2的正文段指向P1的正文段块),数据段(P2本身的数据段块,为其分配对应的块),堆(P2本身的堆块),栈(P2本身的栈块)。以下图所示,同左到右大的方向箭头表示复制内容:

2)写时复制技术

写时复制技术:内核只为新生成的子进程建立虚拟空间结构,它们复制于父进程的虚拟空间结构,可是不为这些段分配物理内存,它们共享父进程的物理空间,当父子进程中有更改相应的段的行为发生时,再为子进程相应的段分配物理空间。

3)vfork

vfork的作法更加简单粗暴,内核连子进程的虚拟地址空间也不建立了,直接共享了父进程的虚拟空间,固然了,这种作法就顺水推舟的共享了父进程的物理空间

总结

传统的fork()系统调用直接把全部的资源复制给新建立的进程。这种实现过于简单而且效率低下,由于它拷贝的数据也许并不共享,更糟的状况是,若是新进程打算当即执行一个新的映像,那么全部的拷贝将是无用功。

Linux的fork()使用写时拷贝(copy-on-write)页实现。写时拷贝是一种能够推迟甚至免除拷贝数据的技术。内核此时并不复制整个地址空间,而是让父进程和子进程共享一个拷贝。只有在须要写入的时候,数据才会复制,从而使各个进程拥有各自的拷贝。也就是说,资源的复制只有在须要写入的时候才进行,在此以前,只是以只读方式共享。这种技术使地址空间的页的拷贝被推迟到实际发生写入的时候。

 

3.    Linux的线程如何实现,与进程的本质区别是什么?

解答:

进程是资源分配和管理的单位,线程是调度的基本单位,进程有独立的地址空间,拥有PCB,其中包含进程标识符(非负整数)、进程资源、进程调度信息、进程间通讯相关资源、处理机状态(便于调度后恢复原状态)等,线程具备单独的堆栈和寄存器,保存本身容许的相关上下文,具备TCB。各线程还共享如下进程资源和环境: 

1)文件描述符表 
2)每种信号的处理方式(SIG_IGN,SIG_DFL,用户自定义) 
3)当前工做目录 
4)用户id和组id 
但有些资源是线程独享的: 
1)线程id 
2)上下文,包括各类寄存器的值,程序计数器和栈指针 
3)栈空间 
4)errno变量 
5)信号屏蔽字 
6)调度优先级 

4.    Linux可否知足硬实时的需求?

5.    进程如何睡眠等资源,此后又如何被唤醒?

6.    进程的调度延时是多少?

7.    调度器追求的吞吐率和响应延迟之间是什么关系?CPU消耗型和I/O消耗型进程的诉求?

8.    Linux怎么区分进程优先级?实时的调度策略和普通调度策略有什么区别?

9.    nice值的做用是什么?nice值低有什么优点?

10.  Linux能够被改形成硬实时吗?有什么方案?

11.  多核、多线程的状况下,Linux如何实现进程的负载均衡?

12.  这么多线程,究竟哪一个线程在哪一个CPU核上跑?有没有办法把某个线程固定到某个CPU跑?

13.  多核下如何实现中断、软中断的负载均衡?

14.  如何利用cgroup对进行进程分组,并调控各个group的CPU资源?

15.  CPU利用率和CPU负载之间的关系?CPU负载高必定用户体验差吗?

 

 

带着问题上路

一切的学习都是为了解决问题,而不是为了学习而学习。为了学习而学习,这种行为实在是太傻了,由于最终也学很差。因此咱们要弄清楚进程调度和内存管理究竟能解决什么样的问题。

Linux进程调度以及配套的进程管理回答以下问题:

1.    Linux进程和线程如何建立、退出?进程退出的时候,本身没有释放的资源(如内存没有free)会怎样?

2.    什么是写时拷贝?

3.    Linux的线程如何实现,与进程的本质区别是什么?

4.    Linux可否知足硬实时的需求?

5.    进程如何睡眠等资源,此后又如何被唤醒?

6.    进程的调度延时是多少?

7.    调度器追求的吞吐率和响应延迟之间是什么关系?CPU消耗型和I/O消耗型进程的诉求?

8.    Linux怎么区分进程优先级?实时的调度策略和普通调度策略有什么区别?

9.    nice值的做用是什么?nice值低有什么优点?

10.  Linux能够被改形成硬实时吗?有什么方案?

11.  多核、多线程的状况下,Linux如何实现进程的负载均衡?

12.  这么多线程,究竟哪一个线程在哪一个CPU核上跑?有没有办法把某个线程固定到某个CPU跑?

13.  多核下如何实现中断、软中断的负载均衡?

14.  如何利用cgroup对进行进程分组,并调控各个group的CPU资源?

15.  CPU利用率和CPU负载之间的关系?CPU负载高必定用户体验差吗?

Linux内存管理回答以下问题:

1.    Linux系统的内存用掉了多少,还剩余多少?下面这个free命令每个数字是什么意思?

2.    为何要有DMA、NORMAL、HIGHMEM zone?每一个zone的大小是由谁决定的?

3.    系统的内存是如何被内核和应用瓜分掉的?

4.    底层的内存管理算法buddy是怎么工做的?它和内核里面的slab分配器是什么关系?

5.    频繁的内存申请和释放是否会致使内存的碎片化?它的后果是什么?

6.    Linux内存耗尽后,系统会发生怎样的状况?

7.    应用程序的内存是何时拿到的?malloc()成功后,是否真的拿到了内存?应用程序的malloc()与free()与内核的关系到底是什么?

8.    什么是lazy分配机制?应用的内存为何会延后以最懒惰的方式拿到?

9.    我写的应用究竟耗费了多少内存?进程的vss/rss/pss/uss分别是什么概念?虚拟的,真实的,共享的,独占的,究竟哪一个是哪一个?

10.  内存为何要作文件系统的缓存?如何作?缓存什么时候放弃?

11.  Free命令里面显示的buffers和cached分别是什么?两者有何区别?

12.  交换分区、虚拟内存到底是什么鬼?它们针对的是什么性质的内存?什么是匿名页?

13.  进程耗费的内存、文件系统的缓存什么时候回收?回收的算法是否是相似LRU?

14.  怎样追踪和判决发生了内存泄漏?内存泄漏后如何查找泄漏源?

15.  内存大小这样影响系统的性能?CPU、内存、I/O三角如何互动?它们如何综合决定系统的一些关键性能?

以上问题,若是您都能回答,那么恭喜您,您是一个概念清楚的人,Linux出现吞吐低、延迟大、响应慢等问题的时候,你能够找到一个可能的方向。若是您只能回答低于1/3的问题,那么,Linux对您仍然是一片空白,出现问题,您只会陷入瞎猫子乱抓,而捞不到耗子的困境,或者胡乱地意测问题,陷入不断的低水平重试。

试图回答这些问题

本文的目的不是回答这些问题,由于回答这些问题,须要洋洋洒洒数百页的文档,而本文档不会超过10页。因此,本文的目的是试图给出一个回答这些问题的思考问题的出发点,咱们倡导面对任何问题的时候,先要弄明白系统的设计目标。

吞吐vs.响应

首先咱们在思考调度器的时候,咱们要理解任何操做系统的调度器设计只追求2个目标:吞吐率大和延迟低。这2个目标有点相似零和游戏,由于吞吐率要大,势必要把更多的时间放在作真实的有用功,而不是把时间浪费在频繁的进程上下文切换;而延迟要低,势必要求优先级高的进程能够随时抢占进来,打断别人,强行插队。可是,抢占会引发上下文切换,上下文切换的时间自己对吞吐率来说,是一个消耗,这个消耗能够低到2us或者更低(这看起来没什么?),可是上下文切换更大的消耗不是切换自己,而是切换会引发大量的cache miss。你明明weibo跑的很爽,如今切过去微信,那么CPU的cache是不太容易命中微信的。

不抢确定响应差,抢了吞吐会降低。Linux不是一个彻底照顾吞吐的系统,也不是一个彻底照顾响应的系统,它做为一个软实时的操做系统,其实是想达到某种平衡,同时也提供给用户必定的配置能力,在内核编译的时候,Kernel Features  --->  Preemption Model选项实际上可让咱们编译内核的时候,是倾向于支持吞吐,仍是支持响应:

越往上面选,吞吐越好,越好下面选,响应越好。服务器你一个月也可贵用一次鼠标,而桌面则显然要求必定的响应,这样能够保证UI行为的表现较好。可是Linux即使选择的是最后一个选项“Preemptible Kernel (Low-Latency Desktop)”,它仍然不是硬实时的。由于,在Linux有三类区间是不能够抢占调度的,这三类区间是:

  • 中断
  • 软中断
  • 持有相似spin_lock这样的锁而锁住该CPU核调度的状况
以下图,一个绿色的普通进程在T1时刻持有spin_lock进入一个critical section(该核调度被关),绿色进程T2时刻被中断打断,然后T3时刻IRQ1里面唤醒了红色的RT进程(若是是硬实时RTOS,这个时候RT进程应该能抢入),以后IRQ1后又执行了IRQ2,到T4时刻IRQ1和IRQ2都结束了,红色RT进程仍然不能执行(由于绿色进程还在spin_lock里面),直到T5时刻,普通进程释放spin_lock后,红色RT进程才抢入。从T3到T5要多久, 鬼都不知道,这样就没法知足硬实时系统的“ 可预期”延迟性,所以Linux不是硬实时操做系统。

Linux的preempt-rt补丁试图把中断、软中断线程化,变成能够被抢占的区间,而把会关本核调度器的spin_lock替换为能够调度的mutex,它实现了在T3时刻唤醒RT进程的时刻,RT进程能够当即抢占调度进入的目标,避免了T3-T5之间延迟的非肯定性。

CPU消耗型 vs. I/O消耗型

在Linux运行的进程,分为2类,一类是CPU消耗型(狂算),一类是I/O消耗型(狂睡,等I/O),前者CPU利用率高,后者CPU利用率低。通常而言,I/O消耗型任务对延迟比较敏感,应该被优先调度。好比,你正在疯狂编译安卓,而等鼠标行为的用户界面老不工做(正在狂睡),可是鼠标一点,咱们应该优先打断正在编译的进程,而去响应鼠标这个I/O,这样电脑的用户体验才符合人性。

Linux的进程,对于RT进程而言,按照SCHED_FIFO和SCHED_RR的策略,优先级高先执行;优先级高的睡眠了后优先级的执行;同等优先级的SCHED_FIFO先ready的跑到睡,后ready的接着跑;而同等优先级的RR则进行时间片轮转。好比Linux存在以下4个进程,T1~T4(内核里面优先级数字越低,优先级越高):

那么它们在Linux的跑法就是:

 
RT的进程调度有一点“恶霸”色彩,我高优先级的没睡,低优先级的你就靠边站。可是Linux的绝大多数进程都不是RT的进程,而是采用SCHED_NORMAL策略(这符合蜘蛛侠法则)。NORMAL的人比较善良,咱们通常用nice来形容它们的优先级,nice越高,优先级越低(你越nice,就越喜欢在地铁让座,固然越坐不到座位)。普通进程的跑法,并非nice低的必定堵着nice高的(要否则还说什么“善良”),它是按照以下公式进行:
vruntime =  pruntime * NICE_0_LOAD/ weight

其中NICE_0_LOAD是1024,也就是NICE是0的进程的weight。vruntime是进程的虚拟运行时间,pruntime是物理运行时间,weight是权重,权重彻底由nice决定,以下表:

 
在RT进程都睡过去以后(有一个特例就是RT没睡也会跑普通进程,那就是RT加起来跑地实在过久过久,普通进程必须喝点汤了),Linux开始跑NORMAL的,它倾向于调度vruntime(虚拟运行时间)最小的普通进程,根据咱们小学数学知识,vruntime要小,要么分子小(喜欢睡,I/O型进程,pruntime不容易长大),要么分母大(nice值低,优先级高,权重大)。这样一个简单的公式,就同时照顾了普通进程的优先级和CPU/IO消耗状况。
好比有4个普通进程,以下表,目前显然T1的vruntime最小(这是它喜欢睡的结果),而后T1被调度到。
 

pruntime

Weight

vruntime

T1

8

1024(nice=0)

8*1024/1024=8

T2

10

526(nice=3)

10*1024/526 =19

T3

20

1024(nice=0)

20*1024/1024=20

T4

20

820(nice=1)

20*1024/820=24

而后,咱们假设T1被调度再执行12个pruntime,它的vruntime将增大delta*1024/weight(这里delta是12,weight是1024),因而T1的vruntime成为20,那么这个时候vruntime最小的反而是T2(为19),此后,Linux将倾向于调度T2(尽管T2的nice值大于T1,优先级低于T1,可是它的vruntime如今只有19)。
因此,普通进程的调度,是一个综合考虑你喜欢干活仍是喜欢睡和你的nice值是多少的结果。鉴于此,咱们去问一个普通进程的调度延迟究竟有多大,这个问题,自己意义就不是特别大,它彻底取决于当前的系统里面还有谁在跑,取决于你唤醒的进程的nice和它前面喜欢不喜欢睡觉。
明白了这一点,你就不会在Linux里面问一些让回答的人吐血的问题。好比,一个普通进程多久被调度到?明确地说,不知道!装逼的说法,就是“depend on …”,依赖的东西太多。再装逼的说法,就是“一言难尽”,但这也是大实话。

分配vs. 占据

Linux做为一个把应用程序员当傻逼的操做系统,它必须容许应用程序犯错。因此这类问题就不要问了:进程malloc()了内存,尚未free()就挂了,那么我前面分配的内存没有释放,是否是就泄漏掉了?明确的说,这是不可能的,Linux内核若是这么傻,它是没法应付乱七八糟的各类开源有漏洞软件的,因此进程死的时候,确定是资源皆被内核释放的,这类傻问题,你明白Linux的出发点,就不会再去问了。

一样的,你在应用程序里面malloc()成功的一刻,也不要觉得真的拿到了内存,这个时候你的vss(虚拟地址空间,Virtual Set Size)会增大,可是你的rss(驻留在内存条上的内存,Resident SetSize)内存会随着写到每一页而缓慢增大。因此,分配成功的一刻,顶多只是被忽悠了,和你实际占有仍是不占有,暂时没有半毛钱关系。

以下图,最初的堆是8KB,这8KB也写过了,因此堆的vss和rss都是8KB。此后咱们调用brk()把堆变大到16KB,可是实际上它占据的内存rss仍是8KB,由于第3页尚未写,根本没有真正从内存条上拿到内存。直到写第3页,堆的rss才变为12KB。这就是Linux针对app的lazy分配机制,它的出发点,固然也是防止应用程序傻逼了。

代码段的内存、堆的内存、栈的内存都是这样懒惰地拿到,demanding page。

咱们有一台1GB内存的32位Linux系统,咱们关闭swap,同时透过修改overcommit_memory为1来容许申请不超过进程虚拟地址空间的内存:

$ sudo swapoff -a

$ sudo sh -c 'echo 1 >/proc/sys/vm/overcommit_memory'

此后,咱们的应用能够申请一个超级大的内存(比实际内存还大):

上述程序在1GB的电脑上面运行,申请2GB内存能够申请成功,可是在写到必定程度后,系统出现out-of-memory,上述程序对应的进程做为oom_score最大(最该死的)的进程被系统杀死。

隔离vs. 共享

Linux进程究竟耗费了多少内存,是一个很是复杂的概念,除了上面的vss, rss外,还有pss和uss,这些都是Linux不一样于RTOS的显著特色之一。Linux各个进程既要作到隔离,可是隔离中又要实现共享,好比1000个进程都用libc,libc的代码段显然在内存只应该有一份。

下面的一幅图上有3个进程,pid为1044的 bash、pid为1045的 bash和pid为1054的 cat。每一个进程透过本身的页表,把虚拟地址空间指向内存条上面的物理地址,每次切换一个进程,即切换一份独特的页表。

 

仅今后图而言,进程1044的vss和rss分别是:

vss= 1+2+3

rss= 4+5+6

可是是否是“4+5+6”就是1044这个进程耗费的内存呢?这显然也是不许确的,由于4明显被3个进程指向,5明显被2个进程指向,坏事是你们一块儿干的,不能1044一我的背黑锅。这个时候,就衍生出了一个pss(按比例计算的驻留内存, Proportional Set Size )的概念,仅从这一幅图而言,进程1044的pss为:

rss= 4/3 +5/2 +6

最后,还有进程1044独占且驻留的内存uss(Unique Set Size ),仅今后图而言,

Uss = 6。

因此,分析Linux,咱们不能模棱两可地停留于表面,或者想固然地说:“Linux的进程耗费了多少内存?”由于这个问题,又是一个要靠装逼来回答的问题,“dependon…”。坦白讲,每次当我问到老外问题,老外第一句话就是“depend on…”的时候,我就想上去抽他了,可是我又抑制了这个冲动,由于,不少问题,不是简单的0和1问题,正反问题,黑白问题,它确实是一个“depend on …”的问题。

有时候,小白问大拿一个问题,大拿实在是没法正面回答,因而就支支吾吾一番。这个时候小白会很生气,以为大拿态度很差,或者在装逼。你实际上,明白不少问题不是简单的0与1问题以后,你就会理解,他真的不是在装逼。这个时候,咱们要反过来反省本身,是否是咱们本身问的问题太LOW逼了?

思考大于接受

咱们前面提出了30个问题,而本文也仅仅只是回答了其中极少的一部分。此文的目的在于创建思惟,导入方向,而不是洋洋洒洒地把全部问题回答掉,由于哥确实没有时间写个几百页的文档来一一回答这些问题。不少事情,用口头描述,比直接写冗长地文档要更加容易也轻松。

最后,我仍然想要强调的一个观点是,咱们在思惟Linux的时候,更多地能够把本身想象成Linus Torvalds,若是你是Linus Torvalds,你要设计Linux,你碰到某个诉求,好比调度器和内存方面的诉求,你应该如何解决。咱们不是被动地接受“是什么”,更多地要思考“为何”,“怎么办”。

若是你是Linus Torvalds,有个傻逼应用程序员要申请1GB内存,你是直接给他,仍是伪装给他,可是实际没有给他,直到它写的时候再给他?

若是你是Linus Torvalds,有个家伙打开了串口,而后进程就作个1/0运算或者访问空指针挂了,你要不要在这个进程挂的时候给它关闭串口?

若是你是Linus Torvalds,你是要让nice值低(优先级高)的普通进程在睡眠前一直堵着nice值高的进程,仍是虽然它优先级高,可是因为跑的时间比较长后,也要让给优先级低(nice值高)的进程?若是你认为nice值低的应该一直跑,那么如何照顾喜欢睡觉的I/O消耗型进程?万一nice值低的进程有bug,进入死循环,那么nice高的进程岂不是丝毫机会都没有?这样的设计,是否是反人类?

当你带着这些思考,武装这些concept,再去看Linux的时候,你就从被动的“接受”,变成了主动地“思考”,这正好是任何一个优秀程序员都具有的品质,也是打通进程调度和内存管理任督二脉的关键。

 

linux进程调度浅析

     操做系统要实现多进程,进程调度必不可少。
      进程调度是对TASK_RUNNING状态的进程进行调度(参见《linux进程状态浅析》)。若是进程不可执行(正在睡眠或其余),那么它跟进程调度没多大关系。
      因此,若是你的系统负载很是低,盼星星盼月亮才出现一个可执行状态的进程。那么进程调度也就不会过重要。哪一个进程可执行,就让它执行去,没有什么须要多考虑的。
      反之,若是系统负载很是高,时时刻刻都有N多个进程处于可执行状态,等待被调度运行。那么进程调度程序为了协调这N个进程的执行,一定得作不少工做。协调得很差,系统的性能就会大打折扣。这个时候,进程调度就是很是重要的。
      尽管咱们日常接触的不少计算机(如桌面系统、网络服务器、等)负载都比较低,可是Linux做为一个通用操做系统,不能假设系统负载低,必须为应付高负载下的进程调度作精心的设计。
      固然,这些设计对于低负载(且没有什么实时性要求)的环境,没多大用。极端状况下,若是CPU的负载始终保持0或1(永远都只有一个进程或没有进程须要在CPU上运行),那么这些设计基本上都是徒劳的。


优先级
      如今的操做系统为了协调多个进程的“同时”运行,最基本的手段就是给进程定义优先级。定义了进程的优先级,若是有多个进程同时处于可执行状态,那么谁优先级高谁就去执行,没有什么好纠结的了。
      那么,进程的优先级该如何肯定呢?有两种方式:由用户程序指定、由内核的调度程序动态调整。(下面会说到)
       linux内核将进程分红两个级别:普通进程和实时进程。实时进程的优先级都高于普通进程,除此以外,它们的调度策略也有所不一样。

实时进程的调度
      实时,本来的涵义是“给定的操做必定要在肯定的时间内完成”。重点并不在于操做必定要处理得多快,而是时间要可控(在最坏状况下也不能突破给定的时间)。
      这样的“实时”称为“硬实时”,多用于很精密的系统之中(好比什么火箭、导弹之类的)。通常来讲,硬实时的系统是相对比较专用的。
      像linux这样的通用操做系统显然无法知足这样的要求,中断处理、虚拟内存、等机制的存在给处理时间带来了很大的不肯定性。硬件的cache、磁盘寻道、总线争用、也会带来不肯定性。
      好比考虑“i++;”这么一句C代码。绝大多数状况下,它执行得很快。可是极端状况下仍是有这样的可能:
  一、i的内存空间未分配,CPU触发缺页异常。而linux在缺页异常的处理代码中试图分配内存时,又可能因为系统内存紧缺而分配失败,致使进程进入睡眠;
  二、代码执行过程当中硬件产生中断,linux进入中断处理程序而搁置当前进程。而中断处理程序的处理过程当中又可能发生新的硬件中断,中断永远嵌套不止……;等等……
      而像linux这样号称实现了“实时”的通用操做系统,其实只是实现了“软实时”,即尽量地知足进程的实时需求。
      若是一个进程有实时需求(它是一个实时进程),则只要它是可执行状态的,内核就一直让它执行,以尽量地知足它对CPU的须要,直到它完成所须要作的事情,而后睡眠或退出(变为非可执行状态)。
      而若是有多个实时进程都处于可执行状态,则内核会先知足优先级最高的实时进程对CPU的须要,直到它变为非可执行状态。因而,只要高优先级的实时进程一直处于可执行状态,低优先级的实时进程就一直不能获得CPU;只要一直有实时进程处于可执行状态,普通进程就一直不能获得CPU。
后来,内核添加了/proc/sys/kernel/sched_rt_runtime_us和/proc/sys/kernel/sched_rt_period_us两个参数,限定了在以sched_rt_period_us为周期的时间内,实时进程最多只能运行sched_rt_runtime_us这么多时间。这样就在一直有实时进程处于可执行状态的状况下,给普通进程留了一点点可以获得执行的机会。参阅《linux组调度浅析》。)

       那么,若是多个相同优先级的实时进程都处于可执行状态呢?这时就有两种调度策略可供选择:
  一、SCHED_FIFO:先进先出。直到先被执行的进程变为非可执行状态,后来的进程才被调度执行。在这种策略下,先来的进程能够行sched_yield系统调用,自愿放弃CPU,以让权给后来的进程;
  二、SCHED_RR:轮转调度。内核为实时进程分配时间片,在时间片用完时,让下一个进程使用CPU;
      强调一下,这两种调度策略仅仅针对于相同优先级的多个实时进程同时处于可执行状态的状况。

      在linux下,用户程序能够经过sched_setscheduler系统调用来设置进程的调度策略以及相关调度参数sched_setparam系统调用则只用于设置调度参数这两个系统调用要求用户进程具备设置进程优先级的能力(CAP_SYS_NICE,通常来讲须要root权限)(参阅capability相关的文章)。
经过将进程的策略设为SCHED_FIFO或SCHED_RR,使得进程变为实时进程。而进程的优先级则是经过以上两个系统调用在设置调度参数时指定的。

      对于实时进程,内核不会试图调整其优先级。由于进程实时与否?有多实时?这些问题都是跟用户程序的应用场景相关,只有用户可以回答,内核不能臆断。

      综上所述,实时进程的调度是很是简单的。进程的优先级和调度策略都由用户定死了,内核只须要老是选择优先级最高的实时进程来调度执行便可。惟一稍微麻烦一点的只是在选择具备相同优先级的实时进程时,要考虑两种调度策略。

普通进程的调度
      实时进程调度的中心思想是,让处于可执行状态的最高优先级的实时进程尽量地占有CPU,由于它有实时需求;而普通进程则被认为是没有实时需求的进程,因而调度程序力图让各个处于可执行状态的普通进程和平共处地分享CPU,从而让用户以为这些进程是同时运行的。
与实时进程相比,普通进程的调度要复杂得多。内核须要考虑两件麻烦事:

1、动态调整进程的优先级
      按进程的行为特征,能够将进程分为“交互式进程”和“批处理进程”:
      交互式进程(如桌面程序、服务器、等)主要的任务是与外界交互。这样的进程应该具备较高的优先级,它们老是睡眠等待外界的输入。而在输入到来,内核将其唤醒时,它们又应该很快被调度执行,以作出响应。好比一个桌面程序,若是鼠标点击后半秒种还没反应,用户就会感受系统“卡”了;
批处理进程(如编译程序)主要的任务是作持续的运算,于是它们会持续处于可执行状态。这样的进程通常不须要高优先级,好比编译程序多运行了几秒种,用户多半不会太在乎;

      若是用户可以明确知道进程应该有怎样的优先级,能够经过nicesetpriority(非实时进程优先级的设置)系统调用来对优先级进行设置。(若是要提升进程的优先级,要求用户进程具备CAP_SYS_NICE能力。)
      然而应用程序未必就像桌面程序、编译程序这样典型。程序的行为可能五花八门,可能一下子像交互式进程,一下子又像批处理进程。以至于用户难以给它设置一个合适的优先级。再者,即便用户明确知道一个进程是交互式仍是批处理,也多半碍于权限或由于偷懒而不去设置进程的优先级。(你又是否为某个程序设置过优先级呢?)
      因而,最终,区分交互式进程和批处理进程的重任就落到了内核的调度程序上。

      调度程序关注进程近一段时间内的表现(主要是检查其睡眠时间和运行时间),根据一些经验性的公式,判断它如今是交互式的仍是批处理的?程度如何?最后决定给它的优先级作必定的调整。
进程的优先级被动态调整后,就出现了两个优先级:
  一、用户程序设置的优先级(若是未设置,则使用默认值),称为静态优先级。这是进程优先级的基准,在进程执行的过程当中每每是不改变的;
  二、优先级动态调整后,实际生效的优先级。这个值是可能时时刻刻都在变化的;

2、调度的公平性
      在支持多进程的系统中,理想状况下,各个进程应该是根据其优先级公平地占有CPU。而不会出现“谁运气好谁占得多”这样的不可控的状况。
      linux实现公平调度基本上是两种思路:
  一、给处于可执行状态的进程分配时间片(按照优先级),用完时间片的进程被放到“过时队列”中。等可执行状态的进程都过时了,再从新分配时间片;
  二、动态调整进程的优先级。随着进程在CPU上运行,其优先级被不断调低,以便其余优先级较低的进程获得运行机会;
      后一种方式有更小的调度粒度,而且将“公平性”与“动态调整优先级”两件事情合而为一,大大简化了内核调度程序的代码。所以,这种方式也成为内核调度程序的新宠。

      强调一下,以上两点都是仅针对普通进程的。而对于实时进程,内核既不能自做多情地去动态调整优先级,也没有什么公平性可言。

      普通进程具体的调度算法很是复杂,而且随linux内核版本的演变也在不断更替(不只仅是简单的调整),因此本文就不继续深刻了。有兴趣的朋友能够参考下面的连接:《Linux 调度器发展简述

调度程序的效率
      “优先级”明确了哪一个进程应该被调度执行,而调度程序还必需要关心效率问题。调度程序跟内核中的不少过程同样会频繁被执行,若是效率不济就会浪费不少CPU时间,致使系统性能降低。
      在linux 2.4时,可执行状态的进程被挂在一个链表中。每次调度,调度程序须要扫描整个链表,以找出最优的那个进程来运行。复杂度为O(n);
      在linux 2.6早期,可执行状态的进程被挂在N(N=140)个链表中,每个链表表明一个优先级,系统中支持多少个优先级就有多少个链表。每次调度,调度程序只须要从第一个不为空的链表中取出位于链表头的进程便可。这样就大大提升了调度程序的效率,复杂度为O(1);
      在linux 2.6近期的版本中,可执行状态的进程按照优先级顺序被挂在一个红黑树(能够想象成平衡二叉树)中。每次调度,调度程序须要从树中找出优先级最高的进程。复杂度为O(logN)。

      那么,为何从linux 2.6早期到近期linux 2.6版本,调度程序选择进程时的复杂度反而增长了呢?
      这是由于,与此同时,调度程序对公平性的实现从上面提到的第一种思路改变为第二种思路(经过动态调整优先级实现)。而O(1)的算法是基于一组数目不大的链表来实现的,按个人理解,这使得优先级的取值范围很小(区分度很低),不能知足公平性的需求。而使用红黑树则对优先级的取值没有限制(能够用32位、64位、或更多位来表示优先级的值),而且O(logN)的复杂度也仍是很高效的。

调度触发的时机
      调度的触发主要有以下几种状况:
  一、当前进程(正在CPU上运行的进程)状态变为非可执行状态。
进程执行系统调用主动变为非可执行状态。好比执行nanosleep进入睡眠、执行exit退出、等等;
进程请求的资源得不到知足而被迫进入睡眠状态。好比执行read系统调用时,磁盘高速缓存里没有所须要的数据,从而睡眠等待磁盘IO;
进程响应信号而变为非可执行状态。好比响应SIGSTOP进入暂停状态、响应SIGKILL退出、等等;

  二、抢占。进程运行时,非预期地被剥夺CPU的使用权。这又分两种状况:进程用完了时间片、或出现了优先级更高的进程。
优先级更高的进程受正在CPU上运行的进程的影响而被唤醒。如发送信号主动唤醒,或由于释放互斥对象(如释放锁)而被唤醒;
内核在响应时钟中断的过程当中,发现当前进程的时间片用完;
内核在响应中断的过程当中,发现优先级更高的进程所等待的外部资源的变为可用,从而将其唤醒。好比CPU收到网卡中断,内核处理该中断,发现某个socket可读,因而唤醒正在等待读这个socket的进程;再好比内核在处理时钟中断的过程当中,触发了定时器,从而唤醒对应的正在nanosleep系统调用中睡眠的进程;

其余问题
一、内核抢占
      理想状况下,只要知足“出现了优先级更高的进程”这个条件,当前进程就应该被马上抢占。可是,就像多线程程序须要用锁来保护临界区资源同样,内核中也存在不少这样的临界区,不大可能随时随地都能接收抢占。
      linux 2.4时的设计就很是简单,内核不支持抢占。进程运行在内核态时(好比正在执行系统调用、正处于异常处理函数中),是不容许抢占的。必须等到返回用户态时才会触发调度(确切的说,是在返回用户态以前,内核会专门检查一下是否须要调度);
      linux 2.6则实现了内核抢占,可是在不少地方仍是为了保护临界区资源而须要临时性的禁用内核抢占。

      也有一些地方是出于效率考虑而禁用抢占,比较典型的是spin_lock。spin_lock是这样一种锁,若是请求加锁得不到知足(锁已被别的进程占有),则当前进程在一个死循环中不断检测锁的状态,直到锁被释放。
      为何要这样忙等待呢?由于临界区很小,好比只保护“i+=j++;”这么一句。若是由于加锁失败而造成“睡眠-唤醒”这么个过程,就有些得不偿失了。那么既然当前进程忙等待(不睡眠),谁又来释放锁呢?其实已获得锁的进程是运行在另外一个CPU上的,而且是禁用了内核抢占的。这个进程不会被其余进程抢占,因此等待锁的进程只有可能运行在别的CPU上。(若是只有一个CPU呢?那么就不可能存在等待锁的进程了。)
而若是不由用内核抢占呢?那么获得锁的进程将可能被抢占,因而可能好久都不会释放锁。因而,等待锁的进程可能就不知何年何月得偿所望了。

      对于一些实时性要求更高的系统,则不能容忍spin_lock这样的东西。宁肯改用更费劲的“睡眠-唤醒”过程,也不能由于禁用抢占而让更高优先级的进程等待。好比,嵌入式实时linux montavista就是这么干的。
      因而可知,实时并不表明高效。不少时候为了实现“实时”,仍是须要对性能作必定让步的。

二、多处理器下的负载均衡
      前面咱们并无专门讨论多处理器对调度程序的影响,其实也没有什么特别的,就是在同一时刻能有多个进程并行地运行而已。那么,为何会有“多处理器负载均衡”这个事情呢?
      若是系统中只有一个可执行队列,哪一个CPU空闲了就去队列中找一个最合适的进程来执行。这样不是很好很均衡吗?
      的确如此,可是多处理器共用一个可执行队列会有一些问题。显然,每一个CPU在执行调度程序时都须要把队列锁起来,这会使得调度程序难以并行,可能致使系统性能降低。而若是每一个CPU对应一个可执行队列则不存在这样的问题。
      另外,多个可执行队列还有一个好处。这使得一个进程在一段时间内老是在同一个CPU上执行,那么极可能这个CPU的各级cache中都缓存着这个进程的数据,颇有利于系统性能的提高。
      因此,在linux下,每一个CPU都有着对应的可执行队列,而一个可执行状态的进程在同一时刻只能处于一个可执行队列中。

      因而,“多处理器负载均衡”这个麻烦事情就来了。内核须要关注各个CPU可执行队列中的进程数目,在数目不均衡时作出适当调整。何时须要调整,以多大力度进程调整,这些都是内核须要关心的。固然,尽可能不要调整最好,毕竟调整起来又要耗CPU、又要锁可执行队列,代价仍是不小的。
另外,内核还得关心各个CPU的关系。两个CPU之间,多是相互独立的、多是共享cache的、甚至多是由同一个物理CPU经过超线程技术虚拟出来的……CPU之间的关系也是实现负载均衡的重要依据。关系越紧密,进程在它们之间迁移的代价就越小。参见《linux内核SMP负载均衡浅析》。

三、优先级继承
      因为互斥,一个进程(设为A)可能由于等待进入临界区而睡眠。直到正在占有相应资源的进程(设为B)退出临界区,进程A才被唤醒。
      可能存在这样的状况:A的优先级很是高,B的优先级很是低。B进入了临界区,可是却被其余优先级较高的进程(设为C)抢占了,而得不到运行,也就没法退出临界区。因而A也就没法被唤醒。
      A有着很高的优先级,可是如今却沦落到跟B一块儿,被优先级并不过高的C抢占,致使执行被推迟。这种现象就叫作优先级反转。

      出现这种现象是很不合理的。较好的应对措施是:当A开始等待B退出临界区时,B临时获得A的优先级(仍是假设A的优先级高于B),以便顺利完成处理过程,退出临界区。以后B的优先级恢复。这就是优先级继承的方法。
      为了实现优先级继承,内核又得作不少事情。更细节的东西能够参考一下关于“优先级反转”或“优先级继承”的文章。

四、中断处理线程化
      在linux下,中断处理程序运行于一个不可调度的上下文中。从CPU响应硬件中断自动跳转到内核设定的中断处理程序去执行,到中断处理程序退出,整个过程是不能被抢占的。
      一个进程若是被抢占了,能够经过保存在它的进程控制块(task_struct)中的信息,在以后的某个时间恢复它的运行。而中断上下文则没有task_struct,被抢占了就无法恢复了。
      中断处理程序不能被抢占,也就意味着中断处理程序的“优先级”比任何进程都高(必须等中断处理程序完成了,进程才能被执行)。可是在实际的应用场景中,可能某些实时进程应该获得比中断处理程序更高的优先级。
      因而,一些实时性要求更高的系统就给中断处理程序赋予了task_struct以及优先级,使得它们在必要的时候可以被高优先级的进程抢占。可是显然,作这些工做是会给系统形成必定开销的,这也是为了实现“实时”而对性能作出的一种让步。

 

 

6. 程序运行时内存布局

   咱们在写程序时,既有程序的逻辑代码,也有在程序中定义的变量等数据,那么当咱们的程序进行时,咱们的代码和数据到底是存放在哪里的呢?下面就来总结一下。

 

1、程序运行时的内存空间状况

 
   其实在程序运行时,因为内存的管理方式是以页为单位的,并且程序使用的地址都是虚拟地址,当程序要使用内存时,操做系统再把虚拟地址映射到真实的物理内存的地址上。因此在程序中,以虚拟地址来看,数据或代码是一块块地存在于内存中的,一般咱们称其为一个段。并且代码和数据是分开存放的,即不储存于同于一个段中,并且各类数据也是分开存放在不一样的段中的。
 
下面以一个简单的程序来看一下在Linux下的程序运行空间状况,代码文件名为space.c
  1. #include <unistd.h>
  2. #include <stdio.h>
  3.  
  4. int main()
  5. {
  6. printf("%d\n", getpid());
  7. while(1);
  8. return 0;
  9. }
这个程序很是简单,输出当前进程的进程号,而后进入一个死循环,这个死循环的目的只是让程序不退出。而在Linux下有一个目录/proc/$(pid),这个目录保存了进程号为pid的进程运行时的全部信息,其中有一个文件maps,它记录了程序执行过程当中的内存空间的状况。编译运行上面的代码,其运行结果如图1所示:

在linux 64位操做系统中

从上面的图中,咱们能够看到这样一个简单的程序,在执行时,须要哪些库和哪些空间。上面的图的各列的意思,不一一详述,只对重要的进行说明。
第一列的是一个段的起始地址和结束地址,第二列这个段的权限,第三列段的段内相对偏移量,第六列是这个段所存放的内容所对应的文件。从上图能够看到咱们的程序进行首先要加载系统的两个共享库,而后再加载咱们写的程序的代码(在linux 64位操做系统中尝试是先加载咱们写的程序代码后加载两个共享库,最后是栈)。
 
对于第二列的权限,r:表示可读,w:表示可写,x:表示可执行,p:表示受保护(即只对本进程有效,不共享),与之相对的是s,意是就是共享。
 

从上图咱们能够很是形象地看到一个程序进行时的内存分布状况。下面咱们将会结合上图,进行更加深刻的对内存中的数据段的解说。


2、程序运行时内存的各类数据段
 
1.bss段

该段用来存放没有被初始化或初始化为0的全局变量,由于是全局变量,因此在程序运行的整个生命周期内都存在于内存中。有趣的是这个段中的变量只占用程序运行时的内存空间,而不占用程序文件的储存空间。能够用如下程序来讲明这点经过符号表能够看到未初始化的全局变量没有被存放在任何段,只是一个未定义的“COMMON符号”,这实际上是跟不一样的语言与不一样的编译器实现有关,有些编译器会将全局未初始化变量存放在.bss段,有些则不放,只是预留一个未定义的全局变量符号,等到最终链接成可执行文件的时候再在.bss段分配空间。)
文件名为bss.c

  1. #include <stdio.h>
  2.  
  3. int bss_data[1024 * 1024];
  4.  
  5. int main()
  6. {
  7. return 0;
  8. }

 

这个程序很是简单,定义一个4M的全局变量,而后返回。编译成可执行文件bss,并查看可执行文件的文件属性如图2所示:

 
从可执行文件的大小4774B能够看出,bss数据段(4M)并不占用程序文件的储存空间,在下面的data段中,咱们能够看到data段的数据是占用可执行文件的储存空间的。
 
在图1中,有文件名且属性为rw-p的内存区间,就是bss段。
 
2.data段
初始化过的全局变量数据段,该段用来保存初始化了的非0的全局变量,若是全局变量初始化为0,则编译有时会出于优化的考虑,将其放在bss段中。由于也是全局变量,因此在程序运行的整个生命周期内都存在于内存中。与bss段不一样的是,data段中的变量既占程序运行时的内存空间,也占程序文件的储存空间。能够用下面的程序来讲明,文件名为data.c:
  1. #include <stdio.h>
  2.  
  3. int data_data[1024 * 1024] = {1};
  4.  
  5. int main()
  6. {
  7. return 0;
  8. }

 

这个程序与上面的bss惟一的不一样就是全局变量int型数组data_data,其中第0个元素的值初始化为1,其余元素的值初始化成默认的0,而由于数组的地址是连续的,因此只要有一个元素在data段中,则其余的元素也必然在data段中。编译链接成可执行文件data,并查看可执行文件的文件属性如图3所示:

 
从可执行文件的大小来看,data段数据(data_data数组的大小,4M)占用程序文件的储存空间。
 
在图1中,有文件名且属性为rw-p的内存区间,就是data段,它与bss段在内存中是共用一段内存的,不一样的是,bss段数据不占用文件,而data段数据占用文件储存空间。
 
3.rodata段
该段是常量数据段,用于存放常量数据,ro就是Read Only之意。可是注意并非全部的常量都是放在常量数据段的,其特殊状况以下:
1)有些当即数与指令编译在一块儿直接放在代码段(text段,下面会讲到)中。
2)对于字符串常量,编译器会去掉重复的常量,让程序的每一个字符串常量只有一份。
3)有些系统中rodata段是多个进程共享的,目的是为了提升空间的利用率。
 
在图1中,有文件名的属性为r--p的内存区间就是rodata段。可见他是受保护的,只能被读取,从而提升程序的稳定性。
 
4.text段
text段就是代码段,用来存放程序的代码(如函数)和部分整数常量。它与rodata段的主要不一样是,text段是能够执行的,并且不被不一样的进程共享。
 
在图1中,有文件名且属性为r-xp的内存区间就是text段。就如咱们所知道的那样,代码段是不能被写的。
 
5.stack段
该段就是栈段,用来保存临时变量和函数参数。程序中的函数调用就是以栈的方式来实现的,一般栈是向下(即向低地址)增加的,当向栈中push一个元素,栈顶指针就会向低地址移动,当从栈中pop一个元素,栈顶指针就会向高地址移动。栈中的数据只在当前函数或下一层函数中有效,当函数返回时,这些数据自动被释放,若是继续对这些数据进行访问,将发生未知的错误。一般咱们在程序中定义的不是用malloc系统函数或new出来的变量,都是存放在栈中的。例如,以下函数:

  1. void func()
  2. {
  3. int a = 0;
  4. int *n_ptr = malloc(sizeof(int));
  5. char *c_ptr = new char;
  6. }
整型变量a,整型指针变量n_ptr和char型指针变量c_ptr,都存放在栈段中,而n_ptr和c_ptr指向的变量,因为是malloc或new出来的,因此存放在堆中。当函数func返回时,a、n_ptr、c_ptr都会被释放,可是n_ptr和c_ptr指向的内存却不会释放。由于它们是存在于堆中的数据。
 
在图1中,文件名为stack的内存区间即为栈段。
 
6.heap段
heap(堆)是最自由的一种内存,它彻底由程序来负责内存的管理,包括何时申请,何时释放,并且对它的使用也没有什么大小的限制。在C/C++中,用alloc系统函数和new申请的内存都存在于heap段中。
 
以上面的程序为例,它向堆申请了一个int和一个char的内存,由于没有调用free或delete,因此当函数返回时,堆中的int和char变量并无释放,形成了内存泄漏。
 
因为在图1所对应的代码中没有使用alloc系统函数或new来申请内存,因此heap段并无在图1中显示出来,因此如下面的程序来讲明heap段的位置,代码文件为heap.c,代码以下:

  1. #include <unistd.h>
  2. #include <stdlib.h>
  3. #include <stdio.h>
  4.  
  5. int main()
  6. {
  7. int *n_ptr = malloc(sizeof(int));
  8. printf("%d\n", getpid());
  9. while(1);
  10. free(n_ptr);
  11. return 0;
  12. }


查看其运行时内存空间分布以下:

 
能够看到文件名为heap的内存区间就是heap段。从上图,也能够看出,虽然咱们只申请4个字节(sizeof(int))的空间,可是在操做系统中,内存是以页的方式进行管理的,因此在分配heap内存时,仍是一次分配就为咱们分配了一个页的内存。注:不管是图1,仍是上图,都有一些没有文件名的内存区间,其实没用文件名的内存区间表示使用mmap映射的匿名空间。

 

 

C语言内存模型及运行时内存布局

咱们知道,C程序开发并编译完成后,要载入内存(主存或内存条)才能运行(请查看:载入内存,让程序运行起来),变量名、函数名都会对应内存中的一块区域。

内存中运行着不少程序,咱们的程序只占用一部分空间,这部分空间又能够细分为如下的区域:

内存分区 说明
程序代码区(code area) 存放函数体的二进制代码
静态数据区(data area) 也称全局数据区,包含的数据类型比较多,如全局变量、静态变量、通常常量、字符串常量。其中:
  • 全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另外一块区域。
  • 常量数据(通常常量、字符串常量)存放在另外一个区域。

注意:静态数据区的内存在程序结束后由操做系统释放。
堆区(heap area) 通常由程序员分配和释放,若程序员不释放,程序运行结束时由操做系统回收。malloc()calloc()free() 等函数操做的就是这块内存,这也是本章要讲解的重点。

注意:这里所说的堆区与数据结构中的堆不是一个概念,堆区的分配方式却是相似于链表。
栈区(stack area) 由系统自动分配释放,存放函数的参数值、局部变量的值等。其操做方式相似于数据结构中的栈。
命令行参数区 存放命令行参数和环境变量的值,如经过main()函数传递的值。

 

C语言内存模型示意图
图1:C语言内存模型示意图


提示:关于局部的字符串常量是存放在全局的常量区仍是栈区,不一样的编译器有不一样的实现,VC 将局部常量像局部变量同样对待,存储于栈(⑥区)中,TC则存储在静态数据区的常量区(②区)。

注意:未初始化的全局变量的默认值是 0,而未初始化的局部变量的值倒是垃圾值(任意值)。请看下面的代码:

  1. #include <stdio.h>
  2. #include <conio.h>
  3. int global;
  4. int main()
  5. {
  6. int local;
  7. printf("global = %d\n", global);
  8. printf("local = %d\n", local);
  9. getch();
  10. return 0;
  11. }


运行结果:
global = 0
local = 1912227604

为了更好的理解内存模型,请你们看下面一段代码:

    1. #include<stdio.h>
    2. #include<stdlib.h>
    3. #include<string.h>
    4. int a = 0; // 全局初始化区(④区)
    5. char *p1; // 全局未初始化区(③区)
    6. int main()
    7. {
    8. int b; // 栈区
    9. char s[] = "abc"; // 栈区
    10. char *p2; // 栈区
    11. char *p3 = "123456"; // 123456\0 在常量区(②),p3在栈上,体会与 char s[]="abc"; 的不一样
    12. static int c = 0; // 全局初始化区
    13. p1 = ( char *)malloc(10), // 堆区
    14. p2 = ( char *)malloc(20); // 堆区
    15. // 123456\0 放在常量区,但编译器可能会将它与p3所指向的"123456"优化成一个地方
    16. strcpy(p1, "123456");
    17. }

 

 

 

1 内存模型

在C语言中,内存可分用五个部分:

1. BSS段(Block Started by Symbol): 用来存放程序中未初始化的全局变量的内存区域。

2. 数据段(data segment): 用来存放程序中已初始化的全局变量的内存区域。

3. 代码段(text segment): 用来存放程序执行代码的内存区域。

4. 堆(heap):用来存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc分配内存时,新分配的内存就被动态添加到堆上,当进程调用free释放内存时,会从堆中剔除。

5. 栈(stack):存放程序中的局部变量(但不包括static声明的变量,static变量放在数据段中)。同时,在函数被调用时,栈用来传递参数和返回值。因为栈先进先出特色。因此栈特别方便用来保存/恢复调用现场。

 

APUE中的一个典型C内存空间分布图

 

以下往上,分别是text段,data段,BSS段,堆,栈

Linux下32位环境的用户空间内存分布状况

 

 

由上图可知:

0x0000 0000:保留区域, 最底层

代码区:用来存放程序代码和常量,只读(运行期会一直存在)

常量区:通常常量,字符常量,只读(运行期会一直存在)

全局数据区:全局变量和静态变量,可读写(运行期会一直存在)

堆段:malloc/free的内存,malloc时分配,free时释放(向上增加)

未分配堆内存

0x4000 0000:动态连接库

未分配栈内存

栈段:局部变量,函数调用参数返回值(向上增加)

0xc000 0000 ~ 0xffff ffff:内核空间(1G)

 

2栈详解

栈(stack): 是由系统自动分配和释放,存放函数的参数值,返回值,局部变量等。其操做方式相似于数据结构中的栈。

 

2.1栈的申请

1. 当在函数或块内部声明一个局部变量时,如:int  nTmp; 系统会判断申请的空间是否足够,足够,在栈中开辟空间,提供内存;不够空间,报异常提示栈溢出。

2. 当调用一个函数时,系统会自动为参数当局部变量,压进栈中,当函数调用结束时,会自动提高堆栈。(可查看汇编中的函数调用机制)

 

2.2栈的大小

栈是有必定大小的,一般状况下,栈只有2M,不一样系统栈的大小可能不一样。

在linux中,查看进程/线程栈大小,命令:  ulimit  -s

$  ulimit  -s

$  8192

个人系统中栈大小为 8192, 有些系统为 10240, 具体查看自已系统栈大小

设置栈大小:

1. 临时改变栈大小:ulimit  -s  10240

2. 开机设置栈大小:在/etc/rc.local中加入 ulimit  -s  10240

3. 改变栈大小: 在/etc/security/limits.conf中加入

* soft stack 10240

 

因此,在声明局部变量时,新手要特别注意栈的大小:

1. 对于局部变量,尽可能不定义大的变量,如大数组(大于2*1024*1024字节)

char  buf[2*1024*1024]; // 可能会致使栈溢出

2. 对于内存较大或不知大小的变量,用堆分配,局部变量用指针,注意要释放

char*  pBuf = (char*)malloc(2*1024*1024); // char* 为局部变量  malloc的内存在堆

free(pBuf);

3. 或定义在全局区中,static变量 或常量区中

static  char  buf[2*1024*1024];

 

2.3栈的生长方向

栈的生长方向和存放数据的方向相反,自顶向下

 

2.4 栈分配例子

int  function( int  var1 ,int  var2)

{

int  var3;

int  var4;

}

 

var1,var2,var3在栈中的图以下:

0xc000 0000

var1

0xc000 0000 - 4

var2

0xc000 0000 - 8

var3

0xc000 0000 - 12

var4

 

3 堆详解

堆(heap:是用来存放动态申请或释放的区域。须要程序员分配和释放,系统不会自动管理,若是用完不释放,将会形成内存泄露,直到进程结速后,系统自动回收。

 

3.1 堆的目的

为何在堆呢?缘由很简单,在栈中,大小是有限制的,能常大小为2M,若是须要更大的空间,那么就要用到堆了,堆的目的就是为了分配使用更大的空间。

3.2申请和释放

int  function()

{

char *pTmp = (char*) malloc(1024);   // malloc在堆中分配1024字节空间

  //pTmp 为局部变量,只占四字节

free(pTmp); // free为手动释放堆中空间

pTmp = NULL; // 防止pTmp变野指针误用

}

 

3.3堆的大小

堆是能够申请大块内存的区域,但堆的大小到底有多大,下面分析下,以32位系统为例。

 

在linux中,堆区的内存申请,在32位系统中,理论上:2^32=4G,但如上面的内存分布图可知:内核占用1G空间。

0xFFFF FFFF

1G内核空间

0xC000 0000

0XBFFF FFF

3G用户空间(text段,data段,BSS段,堆,栈)

0x0000 0000


如上所知,理论上,使用malloc最大可以申请空间大约3G。但这是理论值,由于实际中,还会包含代码区,全局变量区和栈区。

char  *buf = (char*) malloc(3GB);   // 理论上

 

3.4 堆的生长方向

   如上面的图可知,堆是由低地址向高地址生长的

 

3.5 堆的注意事项

堆虽然能够分配较大的空间,但有一些要注意的地方,不然会出现问题。

 

1. 释放问题:分配了堆内存,必定要记得手动释放,不然将会致使内存泄露

void*  alloc(int size)

{

char*  ptr = (char*)malloc(size);

return  ptr;

}

上面函数若是外部调用,没有释放,将内存不会释放形成泄露

2. 碎片问题:若是频繁地调用内存分配和释放,将会使堆内存形成不少内存碎片,从而形成空间浪费和效率低下。

a) 对于比较固定,或可预测大小的,能够程序启动时,即分配好空间,如:某个对象不会超过500个,那个可先生成,object *ptr = (object*)malloc(object_size*500);

b) 结构对齐,尽可能使结构不浪费内存

3. 超堆大小问题:若是申请内存超过堆大小,会出现虚拟内存不足等问题

a) 尽可能不要申请很大的内存,如直须要,可采用内存数据库等

4. 分配是否成功问题:申请内存后,都在判断内存是否分配成功,分配成功后才能使用,不然会出现段错误

char *  pTmp = (char*)malloc(102400);

if(pTmp == 0)   // 必定在记得判断

{

return false;

}

5. 释放后野指针问题:释放指针后,必定要记得把指针的值设置成NULL,防止指针被释放后误用

free(pTmp);

pTmp = NULL; // 防止变野指针

6. 屡次释放问题:若是第5并没置NULL,屡次释放将会出现问题。

 

 

8. Linux的IPC都有哪些

为何要进行进程间的通信(IPC (Inter-process communication))

数据传输:一个进程须要将它的数据发送给另外一个进程,发送的数据量在一个字节到几M字节之间
共享数据:多个进程想要操做共享数据,一个进程对共享数据的修改,别的进程应该马上看到。
通知事件:一个进程须要向另外一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
资源共享:多个进程之间共享一样的资源。为了做到这一点,须要内核提供锁和同步机制。
进程控制:有些进程但愿彻底控制另外一个进程的执行(如Debug进程),此时控制进程但愿可以拦截另外一个进程的全部陷入和异常,并可以及时知道它的状态改变。

linux经常使用的进程间的通信方式

(1)、管道(pipe):管道可用于具备亲缘关系的进程间的通讯,是一种半双工的方式,数据只能单向流动,容许一个进程和另外一个与它有共同祖先的进程之间进行通讯。

(2)、命名管道(named pipe):命名管道克服了管道没有名字的限制,同时除了具备管道的功能外(也是半双工),它还容许无亲缘关系进程间的通讯。命名管道在文件系统中有对应的文件名。命名管道经过命令mkfifo或系统调用mkfifo来建立。

(3)、信号(signal):信号是比较复杂的通讯方式,用于通知接收进程有某种事件发生了,除了进程间通讯外,进程还能够发送信号给进程自己;linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又可以统一对外接口,用sigaction函数从新实现了signal函数)。

(4)、消息队列:消息队列是消息的连接表,包括Posix消息队列system V消息队列。有足够权限的进程能够向队列中添加消息,被赋予读权限的进程则能够读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺

(5)、共享内存:使得多个进程能够访问同一块内存空间,是最快的可用IPC形式。是针对其余通讯机制运行效率较低而设计的。每每与其它通讯机制,如信号量结合使用,来达到进程间的同步及互斥。

(6)、内存映射:内存映射容许任何多个进程间通讯,每个使用该机制的进程经过把一个共享的文件映射到本身的进程地址空间来实现它。

(7)、信号量(semaphore):主要做为进程间以及同一进程不一样线程之间的同步手段。

(8)、套接字(Socket):更为通常的进程间通讯机制,可用于不一样机器之间的进程间通讯。起初是由Unix系统的BSD分支开发出来的,但如今通常能够移植到其它类Unix系统上:Linux和System V的变种都支持套接字。

 

 

linux进程间通讯(IPC)有几种方式

       一。管道(pipe)

  管道是Linux支持的最初IPC方式,管道可分为无名管道,有名管道等。

  (一)无名管道,它具备几个特色:

  1) 管道是半双工的,只能支持数据的单向流动;两进程间须要通讯时须要创建起两个管道;
  2) 无名管道使用pipe()函数建立,只能用于父子进程或者兄弟进程之间;
  3) 管道对于通讯的两端进程而言,实质上是一种独立的文件,只存在于内存中;
  4) 数据的读写操做:一个进程向管道中写数据,所写的数据添加在管道缓冲区的尾部;
                                    另外一个进程在管道中缓冲区的头部读数据。

  (二)有名管道

  有名管道也是半双工的,不过它容许没有亲缘关系的进程间进行通讯。具体点说就是,有名管道提供了一个路径名与之进行关联,以FIFO(先进先出)的形式存在于文件系统中。这样即便是不相干的进程也能够经过FIFO相互通讯,只要他们能访问已经提供的路径。

  值得注意的是,只有在管道有读端时,往管道中写数据才有意义。不然,向管道写数据的进程会接收到内核发出来的SIGPIPE信号;应用程序能够自定义该信号处理函数,或者直接忽略该信号。

       管道是*nix系统进程间通讯的最古老形式,全部*nix都提供这种通讯方式。管道是一种半双工的通讯机制,也就是说,它只能一端用来读,另一端用来写;另外,管道只能用来在具备公共祖先的两个进程之间通讯。管道通讯遵循先进先出的原理,而且数据只能被读取一次,当此段数据被读取后,立刻会从数据中消失,这一点很重要。

        Linux上,建立管道使用pipe函数,当它执行后,会产生两个文件描述符,分别为读端和写端。单个进程中的管道几乎没有任何做用,一般会先调用pipe,而后调用fork,从而建立从父进程到子进程的IPC通道。
------------------------------
waitpid()会暂时中止目前进程的执行,直到有信号来到或子进程结束。
#include<sys/types.h>
#include<sys/wait.h>
定义函数 pid_t waitpid(pid_t pid, int * status, int options);
---------------------------------------------------------------------------------------------------------------------------


  二。信号量(semophore)


  信号量是一种计数器,能够控制进程间多个线程或者多个进程对资源的同步访问,它常实现为一种锁机制。实质上,信号量是一个被保护的变量,而且只能经过初始化和两个标准的原子操做(P/V)来访问。(P,V操做也常称为wait(s),signal(s))

  三。信号(Signal)

  信号是Unix系统中使用的最古老的进程间通讯的方法之一。操做系统经过信号来通知某一进程发生了某一种预约好的事件;接收到信号的进程能够选择不一样的方式处理该信号,一是能够采用默认处理机制-进程中断或退出,一是忽略该信号,还有就是自定义该信号的处理函数,执行相应的动做。

  内核为进程生产信号,来响应不一样的事件,这些事件就是信号源。信号源能够是:异常,其余进程,终端的中断(Ctrl-C,Ctrl+\等),做业的控制(前台,后台进程的管理等),分配额问题(cpu超时或文件过大等),内核通知(例如I/O就绪等),报警(计时器)。

  四。消息队列(Message Queue)

  消息队列就是消息的一个链表,它容许一个或者多个进程向它写消息,一个或多个进程向它读消息。Linux维护了一个消息队列向量表:msgque,来表示系统中全部的消息队列。

  消息队列克服了信号传递信息少,管道只能支持无格式字节流和缓冲区受限的缺点。

  五。共享内存(shared memory)

  共享内存映射为一段能够被其余进程访问的内存。该共享内存由一个进程所建立,而后其余进程能够挂载到该共享内存中。共享内存是最快的IPC机制,但因为linux自己不能实现对其同步控制,须要用户程序进行并发访问控制,所以它通常结合了其余通讯机制实现了进程间的通讯,例如信号量。

  五。套接字(socket)

  socket也是一种进程间的通讯机制,不过它与其余通讯方式主要的区别是:它能够实现不一样主机间的进程通讯。一个套接口能够看作是进程间通讯的端点(endpoint),每一个套接口的名字是惟一的;其余进程能够访问,链接和进行数据通讯

 

 

9.共享内存的本质

进程间通讯---共享内存

   ------->双向通讯

   ------->仅是一块内存,能够随意写入数据

   ------->无同步互斥

   ------->生命周期随内核

   -----共享内存是最快的IPC形式.共享内存的本质是物理内存,一旦这样的内存映射到共享它的进程的地址空间,这些空间不涉及内核.

进程是一个独立的资源管理单元,不一样进程间的资源是独立的,不能在一个进程中访问另外一个进程的用户空间和内存空间。可是,进程不是孤立的,不一样进程之间须要信息的交互和状态的传递,所以须要进程间数据的传递、同步和异步的机制。   

固然,这些机制不能由哪个进程进行直接管理,只能由操做系统来完成其管理和维护,Linux提供了大量的进程间通讯机制,包括同一个主机下的不一样进程和网络主机间的进程通讯,以下图所示:

共享内存是进程间通讯中最简单的方式之中的一个。

共享内存是系统出于多个进程之间通信的考虑,而预留的的一块内存区。

共享内存赞成两个或不少其余进程訪问同一块内存,就如同 malloc() 函数向不一样进程返回了指向同一个物理内存区域的指针。

当一个进程改变了这块地址中的内容的时候,其余进程都会察觉到这个更改

用ftok()函数得到一个ID号


应用说明,在IPC中,咱们经常用用key_t的值来建立或者打开信号量,共享内存和消息队列。

key_t ftok(const char *pathname, int proj_id);
參数 描写叙述
pathname 必定要在系统中存在并且进程可以訪问的
proj_id 一个1-255之间的一个整数值,典型的值是一个ASCII值。

当成功运行的时候,一个key_t值将会被返回。不然-1被返回。咱们可以使用strerror(errno)来肯定详细的错误信息。

考虑到应用系统可能在不一样的主机上应用,可以直接定义一个key,而不用ftok得到:

#define IPCKEY 0x344378

建立共享内存


进程经过调用shmget(Shared Memory GET,获取共享内存)来分配一个共享内存块。

int shmget(key_t key ,int size,int shmflg)
參数 描写叙述
key 一个用来标识共享内存块的键值
size 指定了所申请的内存块的大小
shmflg 操做共享内存的标识

返回值:假设成功,返回共享内存表示符,假设失败,返回-1。

该函数的第一个參数key是一个用来标识共享内存块的键值。

该函数的第二个參数size指定了所申请的内存块的大小

第三个參数shmflg是一组标志。经过特定常量的按位或操做来shmget

映射共享内存


shmat()是用来赞成本进程訪问一块共享内存的函数。将这个内存区映射到本进程的虚拟地址空间。

int shmat(int shmid,char *shmaddr,int flag)
參数 描写叙述
shmid 那块共享内存的ID。是shmget函数返回的共享存储标识符
shmaddr 是共享内存的起始地址,假设shmaddr为0,内核会把共享内存映像到调用进程的地址空间中选定位置。假设shmaddr不为0,内核会把共享内存映像到shmaddr指定的位置。因此通常把shmaddr设为0。
shmflag 是本进程对该内存的操做模式。假设是SHM_RDONLY的话,就是仅仅读模式。

其余的是读写模式

成功时,这个函数返回共享内存的起始地址。失败时返回-1。

共享内存解除映射


当一个进程再也不需要共享内存时,需要把它从进程地址空间中多里。

int shmdt(char *shmaddr)
參数 描写叙述
shmaddr 那块共享内存的起始地址

成功时返回0。失败时返回-1。

应经过调用 shmdt(Shared Memory Detach。脱离共享内存块)函数与该共享内存块脱离。

将由 shmat 函数返回的地址传递给这个函数。假设当释放这个内存块的进程是最后一个使用该内存块的进程,则这个内存块将被删除。

控制释放


shmctl控制对这块共享内存的使用

函数原型

int shmctl( int shmid , int cmd , struct shmid_ds *buf );
參数 描写叙述
shmid 是共享内存的ID。
cmd 控制命令
buf 一个结构体指针。

IPC_STAT的时候,取得的状态放在这个结构体中。假设要改变共享内存的状态,用这个结构体指定。

当中cmd的取值例如如下

cmd 描写叙述
IPC_STAT 获得共享内存的状态
IPC_SET 改变共享内存的状态
IPC_RMID 删除共享内存

返回值: 成功:0 失败:-1

 

能够看到内存映射中须要的一个参数是int fd(文件的标识符),可见函数是经过fd将文件内容映射到一个内存空间,
我须要建立另外一个映射来获得文件内容并统计或修改,这时我建立这另外一个映射用的还是mmap函数,
它仍须要用到fd这个文件标识,那我不等于又从新打开文件读取文件里的数据
1.既然这样那同对文件的直接操做有什么区别呢? 2.映射到内存后经过映射的指针addr来修改内容的话是修改共享内存里的内容仍是文件的内容呢? 3.解决上面2个问题,我仍是想确切知道共享内存有什么用??? 一种回答|一、访问共享内存的执行速度比直接访问文件的快N倍(N》10),这对于要求快速输入输出的场合很是有效。 2、经过addr修改的内容是修改的是共享内容中的内容。至因而否修改了文件中的内容,要看文件的类型。 对于显示设备等文件来讲,修改的也是文件的内容,由于他直接写到了显存中。对于普通文件, 在close文件时,kernel会将数据更新到硬盘等存储设备中。 3、共享内存主要是为了提升程序的执行速度,方便多个进程进行快速的大数据量的交换。 第二种回答: 对因而修改文件内容的内存映射: 1、你的这个说法不确切。举个例子来讲:对显示设备文件(显卡)进行内存的映射,并不会在内存中新分配一块内存, 而是直接将显存地址经过addr参数传给应用程序。这样应用程序经过内存映射修改文件时, 其实就是直接修改显存中的内容(也就是改变显示内容)。 二、感受你把内存映射和共享内存搞混了。内存映射是用来加快对文件/设备的访问。 (若是是大文件,并且还想提升读写速度的话,建议使用内存映射。) 共享内存是用来在多个进程间进行快速的大数据量的交换。 3、fd是文件描述符。它和内存映射没有直接的关系。只有作过内存映射后,它和映射到的内存才存在对应关系。 对于不修改文件内容的内存映射 1、不必定,能够在程序中指定要将文件内容映射到哪块内存。对于多个进程打开同一个文件, 不一样的内存映射能够开辟多块内存区域。更新文件内容的顺序依照关闭文件的进程的顺序执行,所以,存在脏读的问题。 二、:-),必定要记住,内存映射是为了加快对文件/设备的访问速度,不是用来进行数据通讯的。 转载自:http://bbs.csdn.net/topics/340203684 我对内存映射的理解就是经过操做内存来实现对文件的操做,这样能够加快执行速度,由于操做内存比操做文件的速度快多了! 共享内存,顾名思义,就是预留出的内存区域,它容许一组进程对其访问。 共享内存是system vIPC中三种通讯机制最快的一种,也是最简单的一种。对于进程来讲, 得到共享内存后,他对内存的使用和其余的内存是同样的。由一个进程对共享内存所进行的 操做对其余进程来讲都是当即可见的,由于每一个进程只须要经过一个指向共享内存空间的指针就能够来读取 共享内存中的内容(说白了就比如申请了一块内存,每一个须要的进程都有一个指针指向这个内存) 就能够轻松得到结果。使用共享内存要注意的问题:共享内存不能确保对内存操做的互斥性。 一个进程能够向共享内存中的给定地址写入,而同时另外一个进程从相同的地址读出,这将会致使不一致的数据。 所以使用共享内存的进程必须本身保证读操做和写操做的的严格互斥。 可以使用锁和原子操做解决这一问题。也可以使用信号量保证互斥访问共享内存区域。 共享内存在一些状况下能够代替消息队列,并且共享内存的读/写比使用消息队列要快!




共享内存能够说是最有用的进程间通讯方式,也是最快的IPC形式。两个不一样进程A、B共享内存的意思是,同一块物理内存被映射到进程A、B各自的进程地址空间。进程A能够即时看到进程B对共享内存中数据的更新,反之亦然。因为多个进程共享同一块内存区域,必然须要某种同步机制,互斥锁和信号量均可以。

采用共享内存通讯的一个显而易见的好处是效率高,由于进程能够直接读写内存,而不须要任何数据的拷贝。对于像管道和消息队列等通讯方式,则须要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次数据[1]:一次从输入文件到共享内存区,另外一次从共享内存区到输出文件。实际上,进程之间在共享内存时,并不老是读写少许数据后就解除映射,有新的通讯时,再从新创建共享内存区域。而是保持共享区域,直到通讯完毕为止,这样,数据内容一直保存在共享内存中,并无写回文件。共享内存中的内容每每是在解除映射时才写回文件的。所以,采用共享内存的通讯方式效率是很是高的。

Linux的2.2.x内核支持多种共享内存方式,如mmap()系统调用,Posix共享内存,以及系统V共享内存。linux发行版本如Redhat 8.0支持mmap()系统调用及系统V共享内存,但还没实现Posix共享内存,本文将主要介绍mmap()系统调用及系统V共享内存API的原理及应用。

1、内核怎样保证各个进程寻址到同一个共享内存区域的内存页面

一、page cache及swap cache中页面的区分:一个被访问文件的物理页面都驻留在page cache或swap cache中,一个页面的全部信息由struct page来描述。struct page中有一个域为指针mapping ,它指向一个struct address_space类型结构。page cache或swap cache中的全部页面就是根据address_space结构以及一个偏移量来区分的。

二、文件与address_space结构的对应:一个具体的文件在打开后,内核会在内存中为之创建一个struct inode结构,其中的i_mapping域指向一个address_space结构。这样,一个文件就对应一个address_space结构,一个address_space与一个偏移量可以肯定一个page cache 或swap cache中的一个页面。所以,当要寻址某个数据时,很容易根据给定的文件及数据在文件内的偏移量而找到相应的页面。

三、进程调用mmap()时,只是在进程空间内新增了一块相应大小的缓冲区,并设置了相应的访问标识,但并无创建进程空间到物理页面的映射。所以,第一次访问该空间时,会引起一个缺页异常。

四、对于共享内存映射状况,缺页异常处理程序首先在swap cache中寻找目标页(符合address_space以及偏移量的物理页),若是找到,则直接返回地址;若是没有找到,则判断该页是否在交换区(swap area),若是在,则执行一个换入操做;若是上述两种状况都不知足,处理程序将分配新的物理页面,并把它插入到page cache中。进程最终将更新进程页表。
注:对于映射普通文件状况(非共享映射),缺页异常处理程序首先会在page cache中根据address_space以及数据偏移量寻找相应的页面。若是没有找到,则说明文件数据尚未读入内存,处理程序会从磁盘读入相应的页面,并返回相应地址,同时,进程页表也会更新。

五、全部进程在映射同一个共享内存区域时,状况都同样,在创建线性地址与物理地址之间的映射以后,不论进程各自的返回地址如何,实际访问的必然是同一个共享内存区域对应的物理页面。
注:一个共享内存区域能够看做是特殊文件系统shm中的一个文件,shm的安装点在交换区上。

上面涉及到了一些数据结构,围绕数据结构理解问题会容易一些。

2、mmap()及其相关系统调用

mmap()系统调用使得进程之间经过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程能够向访问普通内存同样对文件进行访问,没必要再调用read(),write()等操做。

注:实际上,mmap()系统调用并非彻底为了用于共享内存而设计的。它自己提供了不一样于通常对普通文件的访问方式,进程能够像读写内存同样对普通文件的操做。而Posix或系统V的共享内存IPC则纯粹用于共享目的,固然mmap()实现共享内存也是其主要应用之一。

一、mmap()系统调用形式以下:

void* mmap ( void * addr , size_t len , int prot , int flags , int fd , off_t offset )
参数fd为即将映射到进程空间的文件描述字,通常由open()返回,同时,fd能够指定为-1,此时须指定flags参数中的MAP_ANON,代表进行的是匿名映射(不涉及具体的文件名,避免了文件的建立及打开,很显然只能用于具备亲缘关系的进程间通讯)。len是映射到调用进程地址空间的字节数,它从被映射文件开头offset个字节开始算起。prot 参数指定共享内存的访问权限。可取以下几个值的或:PROT_READ(可读) , PROT_WRITE (可写), PROT_EXEC (可执行), PROT_NONE(不可访问)。flags由如下几个常值指定:MAP_SHARED , MAP_PRIVATE , MAP_FIXED,其中,MAP_SHARED , MAP_PRIVATE必选其一,而MAP_FIXED则不推荐使用。offset参数通常设为0,表示从文件头开始映射。参数addr指定文件应被映射到进程空间的起始地址,通常被指定一个空指针,此时选择起始地址的任务留给内核来完成。函数的返回值为最后文件映射到进程空间的地址,进程可直接操做起始地址为该值的有效地址。这里再也不详细介绍mmap()的参数,读者可参考mmap()手册页得到进一步的信息。

二、系统调用mmap()用于共享内存的两种方式:

(1)使用普通文件提供的内存映射:适用于任何进程之间;此时,须要打开或建立一个文件,而后再调用mmap();典型调用代码以下:

 fd=open(name, flag, mode);
if(fd<0)
 ...
 

ptr=mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED , fd , 0); 经过mmap()实现共享内存的通讯方式有许多特色和要注意的地方,咱们将在范例中进行具体说明。

 

(2)使用特殊文件提供匿名内存映射:适用于具备亲缘关系的进程之间;因为父子进程特殊的亲缘关系,在父进程中先调用mmap(),而后调用fork()。那么在调用fork()以后,子进程继承父进程匿名映射后的地址空间,一样也继承mmap()返回的地址,这样,父子进程就能够经过映射区域进行通讯了。注意,这里不是通常的继承关系。通常来讲,子进程单独维护从父进程继承下来的一些变量。而mmap()返回的地址,却由父子进程共同维护。
对于具备亲缘关系的进程实现共享内存最好的方式应该是采用匿名内存映射的方式。此时,没必要指定具体的文件,只要设置相应的标志便可,参见范例2。

三、系统调用munmap()

int munmap( void * addr, size_t len )
该调用在进程地址空间中解除一个映射关系,addr是调用mmap()时返回的地址,len是映射区的大小。当映射关系解除后,对原来映射地址的访问将致使段错误发生。

四、系统调用msync()

int msync ( void * addr , size_t len, int flags)
通常说来,进程在映射空间的对共享内容的改变并不直接写回到磁盘文件中,每每在调用munmap()后才执行该操做。能够经过调用msync()实现磁盘上文件内容与共享内存区的内容一致

 

3、mmap()范例

下面将给出使用mmap()的两个范例:范例1给出两个进程经过映射普通文件实现共享内存通讯;范例2给出父子进程经过匿名映射实现共享内存。系统调用mmap()有许多有趣的地方,下面是经过mmap()映射普通文件实现进程间的通讯的范例,咱们经过该范例来讲明mmap()实现共享内存的特色及注意事项。

范例1:两个进程经过映射普通文件实现共享内存通讯

范例1包含两个子程序:map_normalfile1.c及map_normalfile2.c。编译两个程序,可执行文件分别为map_normalfile1及map_normalfile2。两个程序经过命令行参数指定同一个文件来实现共享内存方式的进程间通讯。map_normalfile2试图打开命令行参数指定的一个普通文件,把该文件映射到进程的地址空间,并对映射后的地址空间进行写操做。map_normalfile1把命令行参数指定的文件映射到进程地址空间,而后对映射后的地址空间执行读操做。这样,两个进程经过命令行参数指定同一个文件来实现共享内存方式的进程间通讯。

 

从程序的运行结果中能够得出的结论

一、 最终被映射文件的内容的长度不会超过文件自己的初始大小,即映射不能改变文件的大小;

二、 能够用于进程通讯的有效地址空间大小大致上受限于被映射文件的大小,但不彻底受限于文件大小。打开文件被截短为5个people结构大小,而在map_normalfile1中初始化了10个people数据结构,在恰当时候(map_normalfile1输出initialize over 以后,输出umap ok以前)调用map_normalfile2会发现map_normalfile2将输出所有10个people结构的值,后面将给出详细讨论。
注:在linux中,内存的保护是以页为基本单位的,即便被映射文件只有一个字节大小,内核也会为映射分配一个页面大小的内存。当被映射文件小于一个页面大小时,进程能够对从mmap()返回地址开始的一个页面大小进行访问,而不会出错;可是,若是对一个页面之外的地址空间进行访问,则致使错误发生,后面将进一步描述。所以,可用于进程间通讯的有效地址空间大小不会超过文件大小及一个页面大小的和。

三、 文件一旦被映射后,调用mmap()的进程对返回地址的访问是对某一内存区域的访问,暂时脱离了磁盘上文件的影响。全部对mmap()返回地址空间的操做只在内存中有意义,只有在调用了munmap()后或者msync()时,才把内存中的相应内容写回磁盘文件,所写内容仍然不能超过文件的大小。

 

4、对mmap()返回地址的访问

前面对范例运行结构的讨论中已经提到,linux采用的是页式管理机制。对于用mmap()映射普通文件来讲,进程会在本身的地址空间新增一块空间,空间大小由mmap()的len参数指定,注意,进程并不必定可以对所有新增空间都能进行有效访问。进程可以访问的有效地址大小取决于文件被映射部分的大小。简单的说,可以容纳文件被映射部分大小的最少页面个数决定了进程从mmap()返回的地址开始,可以有效访问的地址空间大小。超过这个空间大小,内核会根据超过的严重程度返回发送不一样的信号给进程。可用以下图示说明:

 

注意:文件被映射部分而不是整个文件决定了进程可以访问的空间大小,另外,若是指定文件的偏移部分,必定要注意为页面大小的整数倍。

 

 

 

9.  socket是怎么回事,演绎从应用层到最底层的通讯

在说socket以前。咱们先了解下相关的网络知识;

端口

 在Internet上有不少这样的主机,这些主机通常运行了多个服务软件,同时提供几种服务。每种服务都打开一个Socket,并绑定到一个端口上,不一样的端口对应于不一样的服务(应用程序)。

例如:http 使用80端口 ftp使用21端口 smtp使用 25端口

端口用来标识计算机里的某个程序   1)公认端口:从0到1023   2)注册端口:从1024到49151   3)动态或私有端口:从49152到65535

 

Socket相关概念

socket的英文原义是“孔”或“插座”。做为进程通讯机制,取后一种意思。一般也称做“套接字”,用于描述IP地址和端口,是一个通讯链的句柄。(其实就是两个程序通讯用的。)

socket很是相似于电话插座。以一个电话网为例。电话的通话双方至关于相互通讯的2个程序,电话号码就是IP地址。任何用户在通话以前,

首先要占有一部电话机,至关于申请一个socket;同时要知道对方的号码,至关于对方有一个固定的socket。而后向对方拨号呼叫,

至关于发出链接请求。对方假如在场并空闲,拿起电话话筒,双方就能够正式通话,至关于链接成功。双方通话的过程,

是一方向电话机发出信号和对方从电话机接收信号的过程,至关于向socket发送数据和从socket接收数据。通话结束后,一方挂起电话机至关于关闭socket,撤消链接。

 

Socket有两种类型

流式Socket(STREAM): 是一种面向链接的Socket,针对于面向链接的TCP服务应用,安全,可是效率低;

数据报式Socket(DATAGRAM): 是一种无链接的Socket,对应于无链接的UDP服务应用.不安全(丢失,顺序混乱,在接收端要分析重排及要求重发),但效率高.

 

TCP/IP协议

TCP/IP(Transmission Control Protocol/Internet Protocol)即传输控制协议/网间协议,是一个工业标准的协议集,它是为广域网(WANs)设计的。

UDP协议

UDP(User Data Protocol,用户数据报协议)是与TCP相对应的协议。它是属于TCP/IP协议族中的一种。

应用层 (Application):应用层是个很普遍的概念,有一些基本相同的系统级 TCP/IP 应用以及应用协议,也有许多的企业商业应用和互联网应用。 解释:咱们的应用程序

传输层 (Transport):传输层包括 UDP 和 TCP,UDP 几乎不对报文进行检查,而 TCP 提供传输保证。 解释;保证传输数据的正确性

网络层 (Network):网络层协议由一系列协议组成,包括 ICMP、IGMP、RIP、OSPF、IP(v4,v6) 等。 解释:保证找到目标对象,由于里面用的IP协议,ip包含一个ip地址

链路层 (Link):又称为物理数据网络接口层,负责报文传输。 解释:在物理层面上怎么去传递数据

 

你能够cmd打开命令窗口。输入

netstat -a

查看当前电脑监听的端口,和协议。有TCP和UDP

 

 

TCP/IP与UDP有什么区别呢?该怎么选择?

  UDP能够用广播的方式。发送给每一个链接的用户   而TCP是作不到的

  TCP须要3次握手,每次都会发送数据包(但不是咱们想要发送的数据),因此效率低   但数据是安全的。由于TCP会有一个校验和。就是在发送的时候。会把数据包和校验和一块儿   发送过去。当校验和和数据包不匹配则说明不安全(这个安全不是指数据会不会   别窃听,而是指数据的完整性)

  UDP不须要3次握手。能够不发送校验和

  web服务器用的是TCP协议

那何时用UDP协议。何时用TCP协议呢?   视频聊天用UDP。由于要保证速度?反之相反

   

下图显示了数据报文的格式

 

 

Socket通常应用模式(服务器端和客户端)

 

 

服务端跟客户端发送信息的时候,是经过一个应用程序 应用层发送给传输层,传输层加头部 在发送给网络层。在加头 在发送给链路层。在加帧

 

而后在链路层转为信号,经过ip找到电脑 链路层接收。去掉头(由于发送的时候加头了。去头是为了找到里面的数据) 网络层接收,去头 传输层接收。去头 在到应用程序,解析协议。把数据显示出来

 

TCP3次握手

在TCP/IP协议中,TCP协议提供可靠的链接服务,采用三次握手创建一个链接。   第一次握手:创建链接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;SYN:同步序列编号(Synchronize SequenceNumbers)。   第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时本身也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;   第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。

 

 

看一个Socket简单的通讯图解

相关文章
相关标签/搜索