├── README.md ├── image_res ├── image-20220427150023003.png ├── 企业微信截图_20220429102740.png └── 企业微信截图_20220429103101.png ├── 写代码的规范 └── C++编码规范推荐.md ├── 分布式系统 ├── 一致性相关资料.md └── 数据一致性总结.md └── 计算机基础 ├── 协程和libco.assets ├── Screen Shot 2021-11-08 at 2.25.18 AM.png ├── Screen Shot 2021-11-08 at 2.26.13 AM.png ├── Screen Shot 2021-11-08 at 2.27.19 AM.png ├── Screen Shot 2022-06-23 at 21.49.01.png ├── image-20211108114011003.png ├── image-20211108143152821.png ├── image-20211108145036017.png ├── image-20211108145506743.png ├── image-20211108151511806.png ├── image-20211108152241026.png ├── image-20211108152437853.png ├── image-20211108152728942.png ├── image-20211108152802093.png ├── image-20211108153849692.png ├── image-20211108154822613.png ├── image-20211114211112749.png ├── image-20211114213728751.png ├── image-20211114232738889.png ├── image-20211114232746933.png ├── image-20211115180330921.png ├── image-20211115180343208.png ├── image-20211115180354606.png ├── image-20220623214812934.png └── 进程地址空间.png └── 协程和libco.md /README.md: -------------------------------------------------------------------------------- 1 | # cs-notes 2 | ⭐一个计算机系的笔记仓库,所有人都可往这个仓库里面增加你的笔记,一定要保证你的笔记内容是你自己总结的,并且按照规定提交PR,让我们共同维护这样一个仓库! 3 | -------------------------------------------------------------------------------- /image_res/image-20220427150023003.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomstillcoding/cs-notes/b1b35995c3124bfe46fee1c38350be6782329d1a/image_res/image-20220427150023003.png -------------------------------------------------------------------------------- /image_res/企业微信截图_20220429102740.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomstillcoding/cs-notes/b1b35995c3124bfe46fee1c38350be6782329d1a/image_res/企业微信截图_20220429102740.png -------------------------------------------------------------------------------- /image_res/企业微信截图_20220429103101.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomstillcoding/cs-notes/b1b35995c3124bfe46fee1c38350be6782329d1a/image_res/企业微信截图_20220429103101.png -------------------------------------------------------------------------------- /写代码的规范/C++编码规范推荐.md: -------------------------------------------------------------------------------- 1 | # C++编码规范的推荐实践方式 2 | 3 | 按照这里的方法进行自我检查 4 | 5 | ## 头文件 6 | 7 | 1、自给自足 8 | 9 | 比如你的头文件中用到了 `std::string`,那么就需要自己包含 ``,而不应当依赖使用者先包含了 `` 才能包含你的头文件。 10 | 11 | 12 | 13 | ## 作用域 14 | 15 | ## 类 16 | 17 | ## 函数 18 | 19 | ## 命名约定 20 | 21 | ## 注释 22 | 23 | ## 格式 24 | 25 | ## 其他C++特性 26 | 27 | -------------------------------------------------------------------------------- /分布式系统/一致性相关资料.md: -------------------------------------------------------------------------------- 1 | 线性一致性:什么是线性一致性? - tom-sun的文章 - 知乎 https://zhuanlan.zhihu.com/p/42239873 2 | 3 | 分布式系统的一致性与共识性 - 苏小乐的文章 - 知乎 https://zhuanlan.zhihu.com/p/35596768 -------------------------------------------------------------------------------- /分布式系统/数据一致性总结.md: -------------------------------------------------------------------------------- 1 | > 来源:每日一博 | 你知道数据一致性有几种吗?谈谈数据一致性《https://www.jianshu.com/p/f64d56d46911》 2 | 3 | # 理论知识 4 | 5 | ## CAP 6 | 7 | 在[理论计算机科学](https://zh.wikipedia.org/wiki/理論計算機科學)中,**CAP定理**(CAP theorem),又被称作**布鲁尔定理**(Brewer's theorem),它指出对于一个[分布式计算系统](https://zh.wikipedia.org/wiki/分布式计算)来说,[不可能同时满足以下三点](https://zh.wikipedia.org/wiki/三难困境):[[1\]](https://zh.wikipedia.org/wiki/CAP定理#cite_note-Lynch-1)[[2\]](https://zh.wikipedia.org/wiki/CAP定理#cite_note-2) 8 | 9 | - 一致性(**C**onsistency) (等同于所有节点访问同一份最新的数据副本) 10 | - [可用性](https://zh.wikipedia.org/wiki/可用性)(**A**vailability)(每次请求都能获取到非错的响应——但是不保证获取的数据为最新数据) 11 | - [分区容错性](https://zh.wikipedia.org/w/index.php?title=网络分区&action=edit&redlink=1)(**P**artition tolerance)(以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择[[3\]](https://zh.wikipedia.org/wiki/CAP定理#cite_note-3)。) 12 | 13 | ## ACID 14 | 15 | **ACID**,是指[数据库管理系统](https://zh.wikipedia.org/wiki/数据库管理系统)([DBMS](https://zh.wikipedia.org/wiki/DBMS))在写入或更新资料的过程中,为保证[事务](https://zh.wikipedia.org/wiki/数据库事务)(transaction)是正确可靠的,所必须具备的四个特性:[原子性](https://zh.wikipedia.org/w/index.php?title=原子性&action=edit&redlink=1)(atomicity,或称不可分割性)、[一致性](https://zh.wikipedia.org/wiki/一致性_(数据库))(consistency)、[隔离性](https://zh.wikipedia.org/wiki/隔離性)(isolation,又称独立性)、[持久性](https://zh.wikipedia.org/wiki/持久性)(durability)。 16 | 17 | # 数据一致性的分类 18 | 19 | 一般来说数据一致性我们可以分成三类,时间点一致性,事务一致性,应用一致性。 20 | 21 | ## 时间点一致性 22 | 23 | 如果所有相关的数据组件在任意时刻都是一致的,那么可以称作为时间点一致性。 24 | 25 | 简单来说,就是在任意时刻,所有分布式组件的数据,都是一模一样的,“本是分布式,结果完全一模一样” 26 | 27 | 当然CAP和时间点一致性并不是完全的一致:时间点一致性的定义中要求所有数据组件的数据在任意时刻都是完全一致的,但是一般来说信息传播的速度最大是光速,其实并不能达到任意时刻一致,总有一定的时间不一致,对于我们CAP中的一致性来说只要达到读取到最新数据即可,达到这种情况并不需要严格的任意时间一致。 28 | 29 | ## 事务一致性 30 | 31 | ACID中的C,定义如下: 32 | 33 | > 事务的一致性指的是在一个事务执行之前和执行之后数据库都必须处于一致性状态。如果事务成功地完成,那么系统中所有变化将正确地应用,系统处于有效状态。如果在事务中出现错误,那么系统中的所有变化将自动地回滚,系统返回到原始状态。 34 | 35 | 这里一般的初学者都会把CAP和ACID中的C都会误解成一样的含义,其实他们其中一个表示的数据的相同,而另一个是用来表示某种约束。 36 | 37 | ## 应用一致性 38 | 39 | 应用一致性可以看做是约束一致性中的一种。上面的事务一致性代表的是单一数据源,如果数据源是多个,比如数据源有多个数据库,文件系统,缓存等。那么就需要我们应用一致性,这里也看做是分布式事务一致性。 40 | 41 | 在应用程序中涉及多个不同的单机事务,只有在所有的单机事务完成之前和完成之后,数据是完全一致的。比如给用户发送券和积分,券服务和积分服务是两个服务,他们各自有自己单机事务,这两个单机单机事务开始前和完成后都能保证用户的帐是对应上的。但是在这两个单机事务执行过程有可能会出现只送了券,没有送积分的情况,有可能状态不正确。 42 | 43 | # 一致性的模型 44 | 45 | 如果有人问你你知道哪些一致性模型呢?很多人马上答出,强一致,最终一致。其实一致性的模型远远不止这么点,在《[Operational Characterization of Weak Memory Consistency Models](https://es.cs.uni-kl.de/publications/datarsg/Senf13.pdf)》这篇论文当中描述了15种弱内存一致模型,而在维基百科对内存模型的描述还有更多。 46 | 47 | 很多一致性的模型最开始是用来描述内存是否一致的,也就是最开始并不是运用于分布式系统当中的。如果我们的机器是单核的话,那么他的内存一定是强一致的。如果我们的机器是多核的话,那么由于处理器并不是直接访问的内存而是访问的处理器独享的缓存,那么就有可能会出现不一致。再分布式中我们的每个节点其实就可以看成一个独立的处理器,而我们最初运用于内存一致性模型,也可以运用于我们分布式系统当中。下面我会从强到弱讲讲一些常见的一致性模型。 48 | 49 | ## 线性一致性 50 | 51 | 线性一致性又叫做原子一致性,强一致性。线性一致性可以看做只有一个单核处理器,或者可以看做只有一个数据副本,并且所有操作都是原子的。 52 | 53 | 在可线性化的分布式系统中,如果某个节点更新了数据,那么在其他节点都能读取到这个最新的数据。可以看见线性一致性和我们的CAP中的C是一致的。 54 | 55 | 举个非线性一致性的例子,比如有个秒杀活动,你和你的朋友同时去抢购一样东西,有可能他那里的库存已经没了,但是在你手机上显示还有几件,这个就违反了线性一致性,哪怕过了一会你的手机也显示库存没有,也依然是违反了。 56 | 57 | 线性一致性有什么作用呢?在《DDIA》这本书中描述了下面3个作用: 58 | 59 | - 加锁与主节点选举:主从复制系统需要确保只有一个主节点,否则会产生脑裂。选举新的主节点一般是使用锁:每个启动的节点都需要获得锁。而这个锁就需要满足可线性化,让所有的节点都同时同意哪个节点有锁。我们的ZooKeeper就可以用来提供分布式锁功能,那么我们就可以说ZooKeeper是满足线性一致性的吗?这个只能说说对了一部分,后面再顺序一致性的时候会对ZK是什么一致性再次说明。 60 | - 约束与唯一性保证:比如同一个文件目录下不允许有两个相同的文件名,数据库主键不能重复,这些都需要线性化。其实这些本质和加锁类似,比如相同的文件名,那其实就是对这个文件名去做一个加锁操作,然后去保存,后保存的自然会出错。 61 | - 跨通道的时间依赖:之前的那个抢购的那个例子为什么会被违反呢?原因是因为我们通过朋友告知这个通道,让我们提前知道了这个货物已经卖完。同样的如果我们计算机中出现了多个通道。举个例子,在用户交易的场景下,用户使用了50元,那么会在其余额中扣减50元,这个时候把这个事件作为一个消息队列给发送出去,然后短信服务会查询用户的余额然后进行发送短信,如果余额数据库的从库这个时候还没有更新数据,那么这个短信就有可能会取到用户旧的余额。这里出现不一致的原因就是因为多了一个通道,就和我们上面朋友告知我们卖完的通道一样。解决这个办法可以控制某一个通道,比如说将这个用户的余额作为参数给传进去,或者只读主库。秒杀的那个例子中,你可以不要自己的手机,去用朋友的手机。 62 | 63 | ![image-20220427150023003](../image_res/image-20220427150023003.png) 64 | 65 | ## 顺序一致性 66 | 67 | **所谓的顺序一致性,**其实就是规定了一下两个条件: 68 | **(1)每个线程内部的指令都是按照程序规定的顺序(program order)执行的(单个线程的视角)** 69 | **(2)线程执行的交错顺序可以是任意的,但是所有线程所看见的整个程序的总体执行顺序都是一样的(整个程序的视角)** 70 | 71 | 顺序一致性弱于严格一致性。对变量的写操作不一定要在瞬间看到,但是,不同处理器对变量的写操作必须在所有处理器上以相同的顺序看到,这里处理器再分布式系统中可以换成不同的节点。 72 | 73 | 这里我们又再回到Zookeeper到底是什么一致性?有很多面试题都会问到Zookeeper是CP还是AP呢?很多人都会回答到Zookeeper是CP,其实这个回答并不是很严谨的,我们从线性一致性中知道CAP中的一致性指的是线性一致性,那我们就可以说Zookeeper是线性一致性的吗?答案是否定的。当我们写入一个值的时候,会交由Leader去处理,Zab协议只需要保证半数从节点成功即可,那么就会有节点的数据是老的数据,这样客户端就有可能读出的数据并非是最新的从而破坏了线性一致性。 74 | 75 | Zookeeper其实实现的是顺序一致性,在ZK中利用zxid(ZooKeeper Transaction Id),实现了整体顺序一致性,当然也可以认为Zookeeper的的写是线性一致性,读是顺序一致性。从节点通过zxid顺序的接收leader的广播,所以ZK不能保证所有的信息马上看到,但是最终都会看到。当然Zookeeper其实可以实现线性化,在ZK中有一个sync()命令,只要我们每次读的时候都去调用sync()强制同步数据,那么我们都能保证其是最新的。 76 | 77 | 顺序一致性是由Lamport(Paxos算法的作者)提出的,最开始只用来定义多处理内存的一致性,在Lamport的《How to Make a Multiprocessor Computer That Correctly Executes Multiprocess Programs》中其定义了什么是顺序一致性: 78 | 79 | > the result of any execution is the same as if the operations of all the processors were executed in some sequential order, and the operations of each individual processor appear in this sequence in the order specified by its program. 80 | 81 | 这句话的大致意思是多处理器的执行效果和单个处理器的执行效果是一样的,每个独立的处理器的操作都会按照指定的顺序出现在操作队列。这个最开始是用于并发编程的,但是让多处理器的执行变得和单处理器的确是没啥作用,后来就用于分布式系统当中。在ZK中所有的写操作都会交给Leader节点去做,并且所有操作的更新都会根据zxid的顺序进行更新,这里就是上面所说的指定的顺序,这个队列就是按照zxid的顺序。 82 | 83 | ## 因果一致性 84 | 85 | 因果一致性指的是:如果节点A在更新完某个数据后通知了节点B,那么节点B之后对该数据的访问和修改都是基于A更新后的值。于此同时,和节点A无因果关系的节点C的数据访问则没有这样的限制。 86 | 87 | 怎么理解因果关系呢?简单来说如果有人问你一个问题,那么你给出答案,这两个就是因果关系,但如果你给出答案再问题之前,那么这个就违反了因果关系。 举个简单的例子如果节点1更新了数据A,节点2读取数据A,并更新数据B,这里的数据B有可能是根据数据A计算出来的,所有具备因果关系,但是如果节点3看到的是先更新的B,再更新的A那么就破坏了因果一致性。 88 | 89 | ![企业微信截图_20220429102740](../image_res/%E4%BC%81%E4%B8%9A%E5%BE%AE%E4%BF%A1%E6%88%AA%E5%9B%BE_20220429102740.png) 90 | 91 | ## 处理器一致性 92 | 93 | 处理器一致性是更加弱的一致性模型,他只需要保证处理器看到某个处理器或者多个不同处理对相同位置的写入都是一致的。不需要考虑因果关系,而是对同一个内存或者同一个数据更新需要看到一致的顺序。 94 | 95 | ## FIFO一致性 96 | 97 | FIFO一致性是比处理器一致性还更加弱的一种,它不需要保证对相同位置的写入是一致的。 是指在一个处理器上完成的所有写操作,将会被以它实际发生的顺序通知给所有其它的处理器;但是在不同处理器上完成的写操作也许会被其它处理器以不同于实际执行的顺序所看到。这个在分布式系统中反映了网络中不同节点的延迟可能是不相同的。为了说明其和处理器一致性不同有如下例子: 98 | 99 | ![企业微信截图_20220429103101](../image_res/%E4%BC%81%E4%B8%9A%E5%BE%AE%E4%BF%A1%E6%88%AA%E5%9B%BE_20220429103101.png) 100 | 101 | 上面这个图中,可以发现是违反了处理器一致性的,为什么呢因为写入顺序是w(x)1,w(x)2而,p4应该是先R(x)1再R(x)2。但是这个符合FIFO一致性,FIFO只需要把自己的发生顺序通知给其他的处理器或者节点,不需要保证同一个值写入顺序是一致的。 102 | 103 | ## 最终一致性 104 | 105 | 其实除了强一致以外,其他的一致性都可以看作为最终一致性,只是根据一致性不同模型的不同要求又衍生出了很多具体一致性模型。当然最简单的最终一致性,是不需要关注中间变化的顺序,只需要保证在某个时间点一致即可。只是这个某个时间点需要根据不同的系统,不同业务再去衡量。再最终一致性完成之前,有可能返回任何的值,不会对这些值做任何顺序保证。 106 | 107 | > BASE理论中的E就是最终一致。 108 | 109 | -------------------------------------------------------------------------------- /计算机基础/协程和libco.assets/Screen Shot 2021-11-08 at 2.25.18 AM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomstillcoding/cs-notes/b1b35995c3124bfe46fee1c38350be6782329d1a/计算机基础/协程和libco.assets/Screen Shot 2021-11-08 at 2.25.18 AM.png -------------------------------------------------------------------------------- /计算机基础/协程和libco.assets/Screen Shot 2021-11-08 at 2.26.13 AM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomstillcoding/cs-notes/b1b35995c3124bfe46fee1c38350be6782329d1a/计算机基础/协程和libco.assets/Screen Shot 2021-11-08 at 2.26.13 AM.png -------------------------------------------------------------------------------- /计算机基础/协程和libco.assets/Screen Shot 2021-11-08 at 2.27.19 AM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomstillcoding/cs-notes/b1b35995c3124bfe46fee1c38350be6782329d1a/计算机基础/协程和libco.assets/Screen Shot 2021-11-08 at 2.27.19 AM.png -------------------------------------------------------------------------------- /计算机基础/协程和libco.assets/Screen Shot 2022-06-23 at 21.49.01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomstillcoding/cs-notes/b1b35995c3124bfe46fee1c38350be6782329d1a/计算机基础/协程和libco.assets/Screen Shot 2022-06-23 at 21.49.01.png -------------------------------------------------------------------------------- /计算机基础/协程和libco.assets/image-20211108114011003.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomstillcoding/cs-notes/b1b35995c3124bfe46fee1c38350be6782329d1a/计算机基础/协程和libco.assets/image-20211108114011003.png -------------------------------------------------------------------------------- /计算机基础/协程和libco.assets/image-20211108143152821.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomstillcoding/cs-notes/b1b35995c3124bfe46fee1c38350be6782329d1a/计算机基础/协程和libco.assets/image-20211108143152821.png -------------------------------------------------------------------------------- /计算机基础/协程和libco.assets/image-20211108145036017.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomstillcoding/cs-notes/b1b35995c3124bfe46fee1c38350be6782329d1a/计算机基础/协程和libco.assets/image-20211108145036017.png -------------------------------------------------------------------------------- /计算机基础/协程和libco.assets/image-20211108145506743.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomstillcoding/cs-notes/b1b35995c3124bfe46fee1c38350be6782329d1a/计算机基础/协程和libco.assets/image-20211108145506743.png -------------------------------------------------------------------------------- /计算机基础/协程和libco.assets/image-20211108151511806.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomstillcoding/cs-notes/b1b35995c3124bfe46fee1c38350be6782329d1a/计算机基础/协程和libco.assets/image-20211108151511806.png -------------------------------------------------------------------------------- /计算机基础/协程和libco.assets/image-20211108152241026.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomstillcoding/cs-notes/b1b35995c3124bfe46fee1c38350be6782329d1a/计算机基础/协程和libco.assets/image-20211108152241026.png -------------------------------------------------------------------------------- /计算机基础/协程和libco.assets/image-20211108152437853.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomstillcoding/cs-notes/b1b35995c3124bfe46fee1c38350be6782329d1a/计算机基础/协程和libco.assets/image-20211108152437853.png -------------------------------------------------------------------------------- /计算机基础/协程和libco.assets/image-20211108152728942.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomstillcoding/cs-notes/b1b35995c3124bfe46fee1c38350be6782329d1a/计算机基础/协程和libco.assets/image-20211108152728942.png -------------------------------------------------------------------------------- /计算机基础/协程和libco.assets/image-20211108152802093.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomstillcoding/cs-notes/b1b35995c3124bfe46fee1c38350be6782329d1a/计算机基础/协程和libco.assets/image-20211108152802093.png -------------------------------------------------------------------------------- /计算机基础/协程和libco.assets/image-20211108153849692.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomstillcoding/cs-notes/b1b35995c3124bfe46fee1c38350be6782329d1a/计算机基础/协程和libco.assets/image-20211108153849692.png -------------------------------------------------------------------------------- /计算机基础/协程和libco.assets/image-20211108154822613.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomstillcoding/cs-notes/b1b35995c3124bfe46fee1c38350be6782329d1a/计算机基础/协程和libco.assets/image-20211108154822613.png -------------------------------------------------------------------------------- /计算机基础/协程和libco.assets/image-20211114211112749.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomstillcoding/cs-notes/b1b35995c3124bfe46fee1c38350be6782329d1a/计算机基础/协程和libco.assets/image-20211114211112749.png -------------------------------------------------------------------------------- /计算机基础/协程和libco.assets/image-20211114213728751.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomstillcoding/cs-notes/b1b35995c3124bfe46fee1c38350be6782329d1a/计算机基础/协程和libco.assets/image-20211114213728751.png -------------------------------------------------------------------------------- /计算机基础/协程和libco.assets/image-20211114232738889.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomstillcoding/cs-notes/b1b35995c3124bfe46fee1c38350be6782329d1a/计算机基础/协程和libco.assets/image-20211114232738889.png -------------------------------------------------------------------------------- /计算机基础/协程和libco.assets/image-20211114232746933.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomstillcoding/cs-notes/b1b35995c3124bfe46fee1c38350be6782329d1a/计算机基础/协程和libco.assets/image-20211114232746933.png -------------------------------------------------------------------------------- /计算机基础/协程和libco.assets/image-20211115180330921.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomstillcoding/cs-notes/b1b35995c3124bfe46fee1c38350be6782329d1a/计算机基础/协程和libco.assets/image-20211115180330921.png -------------------------------------------------------------------------------- /计算机基础/协程和libco.assets/image-20211115180343208.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomstillcoding/cs-notes/b1b35995c3124bfe46fee1c38350be6782329d1a/计算机基础/协程和libco.assets/image-20211115180343208.png -------------------------------------------------------------------------------- /计算机基础/协程和libco.assets/image-20211115180354606.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomstillcoding/cs-notes/b1b35995c3124bfe46fee1c38350be6782329d1a/计算机基础/协程和libco.assets/image-20211115180354606.png -------------------------------------------------------------------------------- /计算机基础/协程和libco.assets/image-20220623214812934.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomstillcoding/cs-notes/b1b35995c3124bfe46fee1c38350be6782329d1a/计算机基础/协程和libco.assets/image-20220623214812934.png -------------------------------------------------------------------------------- /计算机基础/协程和libco.assets/进程地址空间.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomstillcoding/cs-notes/b1b35995c3124bfe46fee1c38350be6782329d1a/计算机基础/协程和libco.assets/进程地址空间.png -------------------------------------------------------------------------------- /计算机基础/协程和libco.md: -------------------------------------------------------------------------------- 1 | ## 协程简介 2 | 3 | ### 基本概念 4 | 5 | #### 协程 6 | 7 | > 在这里略过进程和线程的基本概念,默认读者了解 8 | 9 | 协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程那样需要上下文切换来消耗资源(用户态和内核态的切换),因此**协程的开销远远小于线程的开销**。 10 | 11 | - 协程本质上就是用户态线程,将调度的代码在用户态重新实现。因为子程序切换不是线程切换而是由程序自身控制,没有线程切换的开销,所以协程有极高的执行效率。协程通常是纯软件实现的多任务,与CPU和操作系统通常没有关系,跨平台,跨体系架构。 12 | - 协程在执行过程中,可以调用别的协程自己则中途退出执行,之后又从调用别的协程的地方恢复执行。这有点像操作系统的线程,执行过程中可能被挂起,让位于别的线程执行,稍后又从挂起的地方恢复执行。 13 | - 对于线程而言,其上下文存储在内核栈上。线程的上下文切换必须先进入内核态并切换上下文, 这就造成了调度开销。线程的结构体存在于内核中,在pthread_create时需要进入内核态,频繁创建开销大。 14 | 15 | #### 协程的优点与缺点 16 | 17 | 优点: 18 | 19 | - 跨平台体系结构 20 | - 无需线程上下文切换的开销(相比线程切换) 21 | - 无需原子操作锁定及同步的开销(相比多线程程序) 22 | - 方便切换控制流,简化编程模型(调用与回调可以在同一个地方写完) 23 | - 高并发+高扩展性+低成本:高性能CPU可以启用非常多的协程,很适合用于高并发处理。 24 | 25 | 缺点: 26 | 27 | - ###### 无法利用多核资源:协程的本质是个单线程,它不能将一个**多核处理器**的的多个核同时用上,协程需要和进程配合才能运行在多CPU上。(线程、多核、超线程参见CSAPP第三版1.9.2并发和并行P17)当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。 28 | 29 | - 进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序。(https://cloud.tencent.com/developer/article/1684951) 30 | 31 | | | 进程 | 线程 | 协程 | 32 | | ------------------ | -------------------------------------------------- | ------------------------------ | ---------------------------------- | 33 | | 切换者 | OS | OS | 用户 | 34 | | 切换时机 | 根据操作系统自己定义的切换策略 | 根据操作系统自己定义的切换策略 | 用户自己的程序决定 | 35 | | 切换内容 | 页全局目录、内核栈、硬件上下文(进程空间相关知识) | 内核栈、硬件上下文 | 硬件上下文 | 36 | | 切换内容的保存位置 | 保存在内核中 | 保存在内核中 | 保存于用户自己定义的用户栈或者堆中 | 37 | | 切换过程 | 用户态-内核态-用户态 | 用户态-内核态-用户态 | 用户态(无陷入内核态) | 38 | | 切换效率 | 低 | 中 | 高 | 39 | 40 | ### 协程实现相关概念 41 | 42 | #### 函数栈切换 43 | 44 | Linux 使用虚拟地址空间,大大增加了进程的寻址空间,由低地址到高地址分别为: 45 | 46 | - 只读段/代码段:只能读,不可写;可执行代码、字符串字面值、只读变量 47 | - 数据段:已初始化且初值非0全局变量、静态变量的空间 48 | - BSS(Block Started Symbol)段:未初始化或初值为0的全局变量和静态局部变量 49 | - 堆 :就是平时所说的动态内存, malloc/new 大部分都来源于此 50 | - 文件映射区域 :如动态库、共享内存等映射物理空间的内存,一般是 mmap 函数所分配的虚拟地址空间 51 | - 栈:用于维护函数调用的上下文空间;局部变量、函数参数、返回地址等 52 | - 内核虚拟空间:用户代码不可见的内存区域,由内核管理(页表就存放在内核虚拟空间) 53 | 54 | 进程地址空间 55 | 56 | ##### 栈帧 57 | 58 | 栈帧是指为一个函数调用单独分配的那部分栈空间。比如,当运行中的程序调用另一个函数时,就要进入一个新的栈帧,原来函数的栈帧称为调用者函数的帧,新的栈帧称为被调用函数的帧(当前帧)。被调用的函数运行结束后当前帧全部回收,回到调用者的帧。 59 | 60 | 栈帧的详细结构如下图所示: 61 | 62 | 函数A调用函数B,A就是调用者函数,B就是被调用函数 63 | 64 | (参考CSAPP 3.7 过程【3.7.1 运行时栈】P164) 65 | 66 | image-20211108143152821 67 | 68 | ##### 函数调用时的esp/ebp 69 | 70 | 当进行函数调用的时候,除了将参数挨个入栈、将**返回地址(指明当B返回的时候,要从A程序的哪个位置(程序指令在内存中的地址)继续执行,参见程序计数器相关:https://www.zhihu.com/question/22609253)**入栈以外,接下来就是移动esp和ebp指针 71 | 72 | ```c++ 73 | void func A() 74 | { 75 | int x = 1; 76 | B(); 77 | int y = 2; // 调用完函数,需要执行的下一条语句。该语句在程序运行时,最终变成机器可以执行的指令,该指令存储在内存中的位置就是返回地址。那么到底所谓的该指令长什么样子呢?那就是回到前面说的代码段(Code Segment)的地方,PC程序计数器中的内容就是一个地址,当前执行指令的地址(样子就是CS:IP,Code Segment:Instruction Pointer),代码段(CS)里面的内容是机器(指代处理器)都能通过使用指令集来看懂的指令,是二进制内容! 78 | return; 79 | } 80 | 81 | void func B() 82 | { 83 | int z = 3; 84 | return; 85 | } 86 | ``` 87 | 88 | 参考资料: 89 | 90 | **1、CS:IP是什么?** 91 | 92 | image-20220623214812934 93 | 94 | **2、程序计数器** 95 | 96 | https://www.zhihu.com/question/22609253 97 | 98 | **3、[What does the text segment in a program's memory refer to? ](https://stackoverflow.com/questions/27452946/what-does-the-text-segment-in-a-programs-memory-refer-to)** 99 | 100 | Screen Shot 2022-06-23 at 21.49.01 101 | 102 | 103 | 104 | 将调用者(A)函数的ebp入栈(push ebp),然后将调用者函数的栈顶指针ESP赋值给被调函数的EBP(作为被调函数的栈底,move ebp,esp),之后便可以将局部变量push的方式入栈了,结果如下图所示: 105 | 106 | image-20211108145036017 107 | 108 | 此时,EBP寄存器处于一个非常重要的位置,该寄存器中存放着一个地址(原EBP入栈后的栈顶),以该地址为基准,向上(栈底方向)能获取返回地址、参数值,向下(栈顶方向)能获取函数的局部变量值,而该地址处又存放着上一层函数调用时的EBP值; 109 | 110 | 一般规律,SS:[ebp+4]处为被调函数的返回地址,SS:[EBP+8]处为传递给被调函数的第一个参数(最后一个入栈的参数,此处假设其占用4字节内存)的值,SS:[EBP-4]处为被调函数中的第一个局部变量,SS:[EBP]处为上一层EBP值;由于EBP中的地址处总是"上一层函数调用时的EBP值",而在每一层函数调用中,都能通过当时的EBP值"向上(栈底方向)能获取返回地址、参数值,向下(栈顶方向)能获取被调函数的局部变量值"; 111 | 112 | 如此递归,就形成了函数调用栈; 113 | 114 | 115 | 116 | ##### 函数调用栈 117 | 118 | 不管是较早的帧,还是调用者的帧,还是当前帧,它们的结构是完全一样的,因为每个帧都是基于一个函数,帧随着函数的生命周期产生、发展和消亡。这里用到了两个寄存器,`%ebp`是帧指针(帧寄存器),它总是指向当前帧的底部;`%esp`是栈指针(栈寄存器),它总是指向当前帧的顶部。这两个寄存器用来定位当前帧中的所有空间,在后面的代码中将会经常出现。**编译器需要根据IA32指令集的规则小心翼翼地调整这两个寄存器的值,一旦出错,参数传递、函数返回都可能出现问题。** 119 | 120 | ```c++ 121 | int caller() 122 | { 123 | int arg1 = 534; 124 | int arg2 = 1057; 125 | int sum = swap_add(&arg1, &arg2); 126 | int diff = arg1 - arg2; 127 | return sum * diff; 128 | } 129 | 130 | int swap_add(int *xp, int *yp) 131 | { 132 | int x = *xp; 133 | int y = *yp; 134 | *xp = y; 135 | *yp = x; 136 | return x + y; 137 | } 138 | ``` 139 | 140 | 首先,程序从`caller`开始运行,为了详细说明每一行程序都做了什么操作,我们将`caller`函数的C代码编译成汇编码,并给每一句附上注释: 141 | 142 | ```assembly 143 | 1 caller: 144 | 2 pushl %ebp # 将caller函数的上一层函数的ebp位置进行保存 145 | 3 movl %esp, %ebp # 将这时候的esp位置设置为ebp位置(栈帧底部) 146 | 4 subl $24, %esp # 分配 24 bytes 空间(后续介绍) 147 | 5 movl $534, -4(%ebp) # 分配局部变量1为534 148 | 6 movl $1057, -8(%ebp) # 分配局部变量2为1057 149 | 7 leal -8(%ebp), %eax # 计算&arg2并放入%eax 150 | 8 movl %eax, 4(%esp) # 将&arg2放入参数2位置(从右往左放入) 151 | 9 leal -4(%ebp), %eax # 计算&arg1并放入%eax 152 | 10 movl %eax, (%esp) # 将&arg1放入参数1位置(从右往左放入) 153 | 11 call swap_add # 调用swap_add函数 154 | 12 ... 155 | ``` 156 | 157 | 第3行执行完如下: 158 | 159 | image-20211108151511806 160 | 161 | 第10行执行完如下: 162 | 163 | image-20211108152241026 164 | 165 | 来解释栈帧为什么申请了24字节的空间。在现代处理器中,栈帧必须16字节对齐,就是说栈底和栈顶的地址必须是16的整数倍。至于为什么会有这样的要求,请查看文章[《联合、数据对齐和缓冲区溢出攻击》](https://www.jianshu.com/p/b20c8838b929)。现在,既然要求是16的整数倍,24字节肯定是不够的,仔细观察栈帧除了这额外申请的24字节空间外,还有最初压栈的`%ebp`寄存器占用4字节,以及调用子函数前保存的返回地址占用4字节,加起来正好32字节,实现了16字节对齐。如下图所示。 166 | 167 | image-20211108152437853 168 | 169 | ```assembly 170 | 1 caller: 171 | 2 pushl %ebp # 将caller函数的上一层函数的ebp位置进行保存 172 | 3 movl %esp, %ebp # 将这时候的esp位置设置为ebp位置(栈帧底部) 173 | 4 subl $24, %esp # 分配 24 bytes 空间(后续介绍) 174 | 5 movl $534, -4(%ebp) # 分配局部变量1为534 175 | 6 movl $1057, -8(%ebp) # 分配局部变量2为1057 176 | 7 leal -8(%ebp), %eax # 计算&arg2并放入%eax 177 | 8 movl %eax, 4(%esp) # 将&arg2放入参数2位置(从右往左放入) 178 | 9 leal -4(%ebp), %eax # 计算&arg1并放入%eax 179 | 10 movl %eax, (%esp) # 将&arg1放入参数1位置(从右往左放入) 180 | 11 call swap_add # 调用swap_add函数 181 | 12 ... 182 | ``` 183 | 184 | 接下来执行第11行: call swap_add 185 | 186 | `call`指令不仅仅是跳转到子函数的位置,而且还要为子函数的正确返回做准备。事实上,`call`指令可以分为两步,第一步将当前程序段的下一行代码的地址入栈,第二步才是跳转到子函数的代码段,相当于如下两行指令 187 | 188 | ```assembly 189 | pushl [当执行结束返回到caller时,接下来需要执行的代码的地址] 190 | jmp swap_add 191 | ``` 192 | 193 | 在上面的代码中就是 int diff = arg1 - arg2; 这段代码的地址。 194 | 195 | ```c++ 196 | int swap_add(int *xp, int *yp) 197 | { 198 | int x = *xp; 199 | int y = *yp; 200 | *xp = y; 201 | *yp = x; 202 | return x + y; 203 | } 204 | 205 | int caller() 206 | { 207 | int arg1 = 534; 208 | int arg2 = 1057; 209 | int sum = swap_add(&arg1, &arg2); 210 | int diff = arg1 - arg2; 211 | return sum * diff; 212 | } 213 | ``` 214 | 215 | 栈帧如下: 216 | 217 | image-20211108152802093 218 | 219 | 接下来看`swap_add`函数的汇编代码:也就是被调函数 220 | 221 | ```assembly 222 | 1 swap_add: 223 | 2 pushl %ebp # 保存旧的%ebp,即caller的%ebp 224 | 3 movl %esp, %ebp # 将这时的%esp设置为%ebp,也叫设置为swap_add函数的ebp 225 | 4 pushl %ebx # 将%ebx入栈保存 226 | 227 | 5 movl 8(%ebp), %edx #Get xp 228 | 6 movl 12(%ebp), %ecx #Get yp 229 | 7 movl (%edx), %ebx #Get x 230 | 8 movl (%ecx), %eax #Get y 231 | 9 movl %eax, (%edx) #Store y at xp 232 | 10 movl %ebx, (%ecx) #Store x at yp 233 | 11 addl %ebx, %eax #Return value = x+y 234 | 235 | 12 popl %ebx #Restore %ebx 236 | 13 popl %ebp #Restore %ebp 237 | 14 ret #Return 238 | ``` 239 | 240 | 2-4行为预处理部分,和前面分析过的预处理相似,保存旧的帧指针,设置新的帧指针,但多了一步:将第4行的%ebx寄存器入栈。该操作是为了保存`%ebx`寄存器的值,以便在函数结束时恢复原值,即第12行的`popl %ebx`。 241 | 242 | image-20211108153849692 243 | 244 | 知识点:寄存器的使用惯例 245 | 246 | 为什么`caller`中没有保存`%ebx`而`swap_add`中却保存了呢?这涉及到IA32指令集的寄存器使用惯例,这个惯例保证了函数调用时寄存器的值不会丢失或紊乱。 247 | 248 | > `%eax`、`%edx`和`%ecx`称为**调用者保存**寄存器,被调用者使用这三个寄存器时不必担心它们原来的值有没有保存下来,这是调用者自己应该负责的事情。 249 | > 250 | > `%ebx`、`%esi`和`%edi`称为**被调用者保存**寄存器,被调用者如果想要使用它们,必须在开始时保存它们的值并在结束时恢复它们的值,一般通过压栈和出栈来实现。 251 | 252 | 这就可以解释我们的疑问了。由于`%ebx`是被调用者保存寄存器,因此在`swap_add`中我们通过`pushl %ebx`和`popl %ebx`来保存该寄存器的值在函数执行前后不变。 253 | 254 | ```c++ 255 | int swap_add(int *xp, int *yp) 256 | { 257 | int x = *xp; 258 | int y = *yp; 259 | *xp = y; 260 | *yp = x; 261 | return x + y; 262 | } 263 | ``` 264 | 265 | ```assembly 266 | 1 swap_add: 267 | 2 pushl %ebp # 保存旧的%ebp,即caller的%ebp 268 | 3 movl %esp, %ebp # 将这时的%esp设置为%ebp,也叫设置为swap_add函数的ebp 269 | 4 pushl %ebx # 将%ebx入栈保存 270 | 271 | 5 movl 8(%ebp), %edx #Get xp 272 | 6 movl 12(%ebp), %ecx #Get yp 273 | 7 movl (%edx), %ebx #Get x 274 | 8 movl (%ecx), %eax #Get y 275 | 9 movl %eax, (%edx) #Store y at xp 276 | 10 movl %ebx, (%ecx) #Store x at yp 277 | 11 addl %ebx, %eax #Return value = x+y 278 | 279 | 12 popl %ebx #Restore %ebx 280 | 13 popl %ebp #Restore %ebp 281 | 14 ret #Return 282 | ``` 283 | 284 | 5~11行为`swap_add`函数的功能实现代码。略过不看,这里没有进行栈的push操作。 285 | 286 | 12~14行为结束代码,做一些函数的收尾工作。11行执行结束的函数栈帧如下: 287 | 288 | image-20211108154822613 289 | 290 | 首先第12行恢复`%ebx`寄存器的值,接着第13行恢复`%ebp`寄存器的值,最后`ret`返回。而`ret`指令也分为两步,第一步取出当前栈顶的值(即int diff = arg1 - arg2;这段代码的地址),第二步将这个值作为跳转指令的地址跳转,相当于下面两行代码: 291 | 292 | ```assembly 293 | popl %edx 294 | jmp %edx 295 | ``` 296 | 297 | `ret`之后将会执行`call swap_add`指令紧跟着的下一行代码。 298 | 299 | 接下来给出`caller`函数剩下的汇编代码:(即call swap_add后的代码) 300 | 301 | ```assembly 302 | 11 call swap_add 303 | 12 movl -4(%ebp), %edx 304 | 13 subl -8(%ebp), %edx 305 | 14 imull %edx, %eax 306 | 15 leave 307 | 16 ret 308 | ``` 309 | 310 | 12~14行都是在完成之后的一些运算而已,略过。但是15行用了一个没见过的指令`leave`,这又是什么意思呢? 311 | 312 | 我们来分析一下,这段代码和`swap_add`最后三行代码相比,少了两句`popl %ebx`和`popl %ebp`,多了一句`leave`。首先,`popl %ebx`不用考虑了,因为在`caller`的开头并没有`pushl %ebx`,因此也就没必要`popl %ebx`。那么我猜测`leave`是否替代了`popl %ebp`的功能呢?之所以这样猜测,首先我们得弄懂`popl %ebp`到底是什么功能。 313 | 314 | 很简单,**每个函数结束前需要将栈恢复到函数调用前的样子,其实就是恢复两个指针——帧指针和栈指针的位置**。`popl %ebp`的作用就是恢复帧指针的位置。而栈指针`%esp`呢?似乎没有看到哪条指令把它恢复。让我们再仔细捋一遍。先看子函数`swap_add`运行过程中的栈指针。使栈指针变化的只有四条语句,2、4行的`pushl`指令和12、13行的`popl`指令,而且两对指令对栈指针的影响正好对消,于是栈指针在函数结束时已经回到了最初的位置,因此根本不需要额外的调整。 315 | 316 | 再考虑`caller`函数,**与`swap_add`不同的地方在于第4行申请了24字节的栈空间**,即手动将`%esp`寄存器的值减去了24。这就导致函数结束时栈指针无法回到最初的位置,需要我们手动将它恢复,`leave`指令就是这个作用。该指令相当于下面两条指令的合成: 317 | 318 | ```assembly 319 | movl %ebp, %esp # 手动恢复栈顶指针位置 320 | popl %ebp # 恢复上一个函数的ebp的位置 321 | ``` 322 | 323 | #### 有栈协程 324 | 325 | > 协程切换时主要保存的上下文环境就是指寄存器的内容、栈帧的内容。 326 | 327 | ##### 独立栈 328 | 329 | 独立栈指的是在所有协程运行的过程中,它们用的栈帧是自己的栈,这块栈的地址的内容不会让其他协程进行读写。 330 | 331 | 缺点:独立栈往往会更加的**浪费**内存。因为,我们需要为每一个协程预先分配一个栈空间,但是问题是协程不一定会用完这个栈空间,而那些多出来的栈空间就是被浪费掉了的。而且空间太小也会有爆栈的隐患。 332 | 333 | 优点:每次切换协程的时候,不需要对栈进行拷贝。(相比于共享栈) 334 | 335 | ![image-20211114211112749](协程和libco.assets/image-20211114211112749.png) 336 | 337 | ##### 共享栈 338 | 339 | 共享栈指的是在所有协程运行的过程中,它们用的任务栈是同一个栈。 340 | 341 | 优点:可以更加的节省内存。因为,我们只需要让协程使用这个共享的栈即可,然后,当协程挂起的时候,依据当前协程使用的栈空间大小来分配内存备份协程的栈内容。 342 | 343 | 缺点:就会使得每次换入和换出协程的时候,都要进行协程的栈数据的拷贝。 344 | 345 | ![image-20211114213728751](协程和libco.assets/image-20211114213728751.png) 346 | 347 | ## 协程切换 348 | 349 | libco使用:https://blog.csdn.net/arbboter/article/details/101375476 350 | 351 | ### 协程环境 352 | 353 | 数量对应关系来说,协程之于线程,相当于线程之于进程,一个进程可以包含多个线程,而一个线程中可以包含多个协程。以libco为例,线程中用于管理协程的结构体为`stCoRoutineEnv_t`(环境),它在该线程中第一个协程创建的时候进行初始化。 354 | 每个线程中都只有一个`stCoRoutineEnv_t`实例,线程可以通过该`stCoRoutineEnv_t`实例了解现在有哪些协程,哪个协程正在运行,以及下一个运行的协程是哪个。 355 | 356 | 简单来说,一个线程对应一个stCoRoutineEnv_t结构,一个stCoRoutineEnv_t结构对应多个协程。在第一次创建协程的时候会对stCoRoutineEnv_t进行初始化,并自动将当前线程执行的上下文空间为主协程,然后再创建真正的用户自己定义的协程。 357 | 358 | ```c++ 359 | struct stCoRoutineEnv_t 360 | { 361 | stCoRoutine_t *pCallStack[ 128 ]; // 保存当前栈中的协程,上限128个 362 | int iCallStackSize; // 表示当前在运行的协程的下一个位置,即cur_co_runtine_index + 1 363 | stCoEpoll_t *pEpoll; //用于协程时间片切换 364 | 365 | //for copy stack log lastco and nextco 366 | stCoRoutine_t* pending_co; 367 | stCoRoutine_t* occupy_co; 368 | }; 369 | ``` 370 | 371 | ### 协程核心切换实现 372 | 373 | 首先是一个协程对应的它的上下文coctx_t的初始化如下: 374 | 375 | ```c++ 376 | int coctx_make( coctx_t *ctx,coctx_pfn_t pfn,const void *s,const void *s1 ) 377 | { 378 | //make room for coctx_param 379 | // 获取(栈顶 - param size)的指针,栈顶和sp指针之间用于保存函数参数 380 | char *sp = ctx->ss_sp + ctx->ss_size - sizeof(coctx_param_t); 381 | sp = (char*)((unsigned long)sp & -16L); // 用于16位对齐 382 | 383 | // 将参数填入到param中 384 | coctx_param_t* param = (coctx_param_t*)sp ; 385 | param->s1 = s; 386 | param->s2 = s1; 387 | 388 | memset(ctx->regs, 0, sizeof(ctx->regs)); 389 | // 为什么要 - sizeof(void*)呢? 用于保存返回地址 390 | ctx->regs[ kESP ] = (char*)(sp) - sizeof(void*); 391 | ctx->regs[ kEIP ] = (char*)pfn; 392 | 393 | return 0; 394 | } 395 | ``` 396 | 397 | 这段代码主要是做了什么呢? 398 | 399 | 1. 先给`coctx_pfn_t`函数预留2个参数的大小,并4位地址对齐 400 | 2. 将参数填入到预存的参数中 401 | 3. `regs[kEIP]`中保存了`pfn`的地址,`regs[kESP]`中则保存了栈顶指针 - 4个字节的大小的地址。这预留的4个字节用于保存`return address`。 402 | 403 | 现在我们来看下协程切换的核心 **coctx_swap**,这个函数是使用汇编实现的。主要分为保存当前栈空间上下文的寄存器,并写入即将到来的栈空间上下文的寄存器两个步骤。 404 | 405 | 先看一下执行汇编程序前的栈帧情况。`esp`寄存器指向`return address`。 406 | 407 | image-20211114232746933 408 | 409 | ```assembly 410 | //----- -------- 411 | // 32 bit 412 | // | regs[0]: ret | 413 | // | regs[1]: ebx | 414 | // | regs[2]: ecx | 415 | // | regs[3]: edx | 416 | // | regs[4]: edi | 417 | // | regs[5]: esi | 418 | // | regs[6]: ebp | 419 | // | regs[7]: eax | = esp 420 | 421 | coctx_swap: 422 | 1 leal 4(%esp), %eax // eax = esp + 4 保存co_swap栈空间的cur_ctx的地址到eax 423 | 2 movl 4(%esp), %esp // esp = *(esp+4) = &cur_ctx 将cur_ctx这个地址赋值给esp。 424 | 3 leal 32(%esp), %esp // parm a : ®s[7] + sizeof(void*) 425 | // esp=®[7]+sizeof(void*) 移动esp的位置,为增加 426 | // 为后续pushl的时候esp从高地址到低地址移动准备 427 | 4 pushl %eax // cur_ctx->regs[ESP] = %eax = returnAddress + 4 428 | 5 pushl %ebp // cur_ctx->regs[EBX] = %ebp 429 | 6 pushl %esi // cur_ctx->regs[ESI] = %esi 430 | 7 pushl %edi // cur_ctx->regs[EDI] = %edi 431 | 8 pushl %edx // cur_ctx->regs[EDX] = %edx 432 | 9 pushl %ecx // cur_ctx->regs[ECX] = %ecx 433 | 10 pushl %ebx // cur_ctx->regs[EBX] = %ebx 434 | 11 pushl -4(%eax) // cur_ctx->regs[EIP] = return address 保存return address 435 | ``` 436 | 437 | 1、其中的cur_ctx是一个堆的地址,里面的内容如下: 438 | 439 | ```c++ 440 | // 栈切换的时候需要保存的寄存器空间 441 | struct coctx_t 442 | { 443 | #if defined(__i386__) 444 | void *regs[ 8 ]; 445 | #else 446 | void *regs[ 14 ]; 447 | #endif 448 | size_t ss_size; 449 | char *ss_sp; 450 | }; 451 | ``` 452 | 453 | 2、执行完第2句后,如下: 454 | 455 | ![image-20211115180330921](协程和libco.assets/image-20211115180330921.png) 456 | 457 | 3、执行完第3句后,如下: 458 | 459 | ![image-20211115180343208](协程和libco.assets/image-20211115180343208.png) 460 | 461 | 4、然后就是pushl的过程,通过该指令,esp向上移动,挨个保存寄存器内容。 462 | 463 | 464 | 465 | 下面是恢复`pend_ctx`中的寄存器信息到`cpu`寄存器中 466 | 467 | ```assembly 468 | movl 4(%eax), %esp //parm b -> ®s[0] 469 | // esp=&pend_ctx 同样的,将pending_ctx的esp弄过去 470 | popl %eax //%eax= pend_ctx->regs[EIP] = pfunc_t地址,通过popl就是反向的操作 471 | // popl本身就是低地址到高地址,所以不需要提前移动esp的位置 472 | popl %ebx //%ebx = pend_ctx->regs[EBX] 473 | popl %ecx //%ecx = pend_ctx->regs[ECX] 474 | popl %edx //%edx = pend_ctx->regs[EDX] 475 | popl %edi //%edi = pend_ctx->regs[EDI] 476 | popl %esi //%esi = pend_ctx->regs[ESI] 477 | popl %ebp //%ebp = pend_ctx->regs[EBP] 478 | popl %esp //%ebp = pend_ctx->regs[ESP] 即 (char*) sp - sizeof(void*) 479 | pushl %eax //set ret func addr 480 | // return address = %eax = pfunc_t地址 481 | xorl %eax, %eax 482 | ret // popl %eip 即跳转到pfunc_t地址执行 483 | ``` 484 | 485 | ### 汇编栈帧切换时的注意事项 486 | 487 | libco里面的swap函数里面大量用到push(pushl)和pop指令(如下所示),然而在一些其他的协程库里面的swap函数却使用大量的mov指令而非push/pop指令,为什么? 488 | 489 | ```assembly 490 | coctx_swap: 491 | 1 leal 4(%esp), %eax // eax = esp + 4 保存co_swap栈空间的cur_ctx的地址到eax 492 | 2 movl 4(%esp), %esp // esp = *(esp+4) = &cur_ctx 将cur_ctx这个地址赋值给esp。 493 | 3 leal 32(%esp), %esp // parm a : ®s[7] + sizeof(void*) 494 | // esp=®[7]+sizeof(void*) 移动esp的位置,为增加 495 | // 为后续pushl的时候esp从高地址到低地址移动准备 496 | 4 pushl %eax // cur_ctx->regs[ESP] = %eax = returnAddress + 4 497 | 5 pushl %ebp // cur_ctx->regs[EBX] = %ebp 498 | 6 pushl %esi // cur_ctx->regs[ESI] = %esi 499 | 7 pushl %edi // cur_ctx->regs[EDI] = %edi 500 | 8 pushl %edx // cur_ctx->regs[EDX] = %edx 501 | 9 pushl %ecx // cur_ctx->regs[ECX] = %ecx 502 | 10 pushl %ebx // cur_ctx->regs[EBX] = %ebx 503 | 11 pushl -4(%eax) // cur_ctx->regs[EIP] = return address 保存return address 504 | ``` 505 | 506 | 原因:栈指针的使用方式违反Sys V ABI约定 507 | 508 | 注:应用程序二进制接口(Application Binary Interface,ABI) 509 | 510 | > The end of the input argument area shall be aligned on a 16 (32 or 64, if __m256 or __m512 is passed on stack) byte boundary. In other words, the value (%esp + 4) is always a multiple of 16 (32 or 64) when control is transferred to the function entry point. **The stack pointer, %esp, always points to the end of the latest allocated stack frame.** 511 | > 512 | > — Intel386-psABI-1.1:2.2.2 The Stack Frame 513 | 514 | > **The stack pointer, %rsp, always points to the end of the latest allocated stack frame.** 515 | > 516 | > — Sys V ABI AMD64 Version 1.0:3.2.2 The Stack Frame 517 | 518 | 不管是i386还是sys V的ABI都提到了The stack pointer, %rsp, always points to the end of the latest allocated stack frame. 519 | 520 | 即用户空间程序的栈指针必须时刻指到运行栈的[栈顶](https://zh.wikipedia.org/wiki/堆栈#操作),而[coctx_swap.S](https://github.com/Tencent/libco/blob/v1.0/coctx_swap.S#L27)中却使用栈指针直接对位于堆中的数据结构进行寻址内存操作,这违反了ABI约定。 521 | 522 | 而在Linux信号处理手册中可以看到: 523 | 524 | > **By default, the signal handler is invoked on the normal process stack.** It is possible to arrange that the signal handler uses an alternate stack; see sigalstack(2) for a discussion of how to do this and when it might be useful. 525 | > 526 | > — man 7 signal : Signal dispositions 527 | 528 | 也就是说,用pop方式实现的汇编代码,在有一个时刻出现了esp并不在栈顶的情况,要理解这一点,必须把ebp和esp绑定起来看,即esp在下图所示的时刻和ebp变得“毫无对应关系”,这会导致严重的后果。比如说此时用户态收到信号进入内核态,如果信号处理的时候使用了该esp指针,然而该esp指针的push操作仅仅只能维持sizeof(struct coctx_t)大小,显然很可能会出现问题,因为在正常情况下,esp的push时所在的栈空间都是远大于sizeof(struct coctx_t)的(即使使用共享栈的情况下)。 529 | 530 | ![image-20211115180354606](协程和libco.assets/image-20211115180354606.png) 531 | 532 | 当然查了相关资料后了解到libco内部版本早已解决了这个问题,只是在开源版本里面仍然保留了这个bug。 533 | 534 | ## 协程上层管理 535 | 536 | **了解了协程的实现原理之后,上层管理可以自己选择自己喜欢的方式,除了直接选择使用共享栈和独立栈的方式以外,甚至可以通过判断栈空间的大小来比较灵活地自动选择模式,以及进行一些具体业务强相关的协程库优化,从而得以让性能提高。** 537 | 538 | ### 协程模块数据结构 539 | 540 | 协程控制块stCoRoutine_t 541 | 542 | ```c++ 543 | struct stCoRoutine_t 544 | { 545 | stCoRoutineEnv_t *env; // 即协程执行的环境,libco协程一旦创建便跟对应线程绑定了,不支持在不同线程间迁移,这里env即同属于一个线程所有协程的执行环境,包括了当前运行协程、嵌套调用的协程栈,和一个epoll的封装结构。这个结构是跟运行的线程绑定了的,运行在同一个线程上的各协程是共享该结构的,是个全局性的资源。 546 | pfn_co_routine_t pfn; // 实际等待执行的协程函数 547 | void *arg; // 上面协程函数的参数 548 | coctx_t ctx; // 上下文,即ESP、EBP、EIP和其他通用寄存器的值 549 | 550 | // 一些状态和标志变量 551 | char cStart; 552 | char cEnd; 553 | char cIsMain; 554 | char cEnableSysHook; 555 | char cIsShareStack; 556 | 557 | void *pvEnv; // 保存程序系统环境变量的指针 558 | 559 | //char sRunStack[ 1024 * 128 ]; 560 | stStackMem_t* stack_mem; // 协程运行时的栈内存,这个栈内存是固定的 128KB 的大小。 561 | 562 | 563 | //save stack buffer while confilct on same stack_buffer; 564 | // 共享栈模式中使用 565 | char* stack_sp; 566 | unsigned int save_size; 567 | char* save_buffer; 568 | 569 | stCoSpec_t aSpec[1024]; 570 | }; 571 | ``` 572 | 573 | ```c++ 574 | // 已经介绍过,略 575 | struct stCoRoutineEnv_t 576 | { 577 | stCoRoutine_t *pCallStack[ 128 ]; 578 | int iCallStackSize; 579 | stCoEpoll_t *pEpoll; 580 | 581 | //for copy stack log lastco and nextco 582 | stCoRoutine_t* pending_co; 583 | stCoRoutine_t* occupy_co; 584 | }; 585 | ``` 586 | 587 | ```c++ 588 | // 栈切换的时候需要保存的寄存器空间 589 | struct coctx_t 590 | { 591 | #if defined(__i386__) 592 | void *regs[ 8 ]; 593 | #else 594 | void *regs[ 14 ]; 595 | #endif 596 | size_t ss_size; 597 | char *ss_sp; 598 | }; 599 | ``` 600 | 601 | - stack_sp、save_size、save_buffer:这里要提到实现 stackful 协程(与之相对的还有一种stackless协程)的两种技术:Separate coroutine stacks 和 Copying the stack(又叫共享栈)。这三个变量就是用来实现这两种技术的。 602 | 603 | - 实现细节上,前者为每一个协程分配一个单独的、固定大小的栈;而后者则仅为正在运行的协程分配栈内存,当协程被调度切换出去时,就把它实际占用的栈内存 copy 保存到一个单独分配的缓冲区;当被切出去的协程再次调度执行时,再一次 copy 将原来保存的栈内存恢复到那个共享的、固定大小的栈内存空间。 604 | - 如果是独享栈模式,分配在堆中的一块作为当前协程栈帧的内存 stack_mem,这块内存的默认大小为 128K。 605 | - 如果是共享栈模式,协程切换的时候,用来拷贝存储当前共享栈内容的 save_buffer,长度为实际的共享栈使用长度。 606 | - 通常情况下,一个协程实际占用的(从 esp 到栈底)栈空间,相比预分配的这个栈大小(比如 libco 的 128KB)会小得多;这样一来, copying stack 的实现方案所占用的内存便会少很多。当然,协程切换时拷贝内存的开销有些场景下也是很大的。因此两种方案各有利弊,而 libco 则同时实现了两种方案,默认使用前者,也允许用户在创建协程时指定使用共享栈。 607 | 608 | 609 | ### 创建协程(create) 610 | 611 | ```c++ 612 | int co_create( stCoRoutine_t **ppco,const stCoRoutineAttr_t *attr,pfn_co_routine_t pfn,void *arg ) 613 | { 614 | if( !co_get_curr_thread_env() ) 615 | { 616 | co_init_curr_thread_env(); 617 | } 618 | stCoRoutine_t *co = co_create_env( co_get_curr_thread_env(), attr, pfn,arg ); 619 | *ppco = co; 620 | return 0; 621 | } 622 | ``` 623 | 624 | 调用 co_create 将协程创建出来后,这时候它还没有启动,也即是说我们传递的 routine 函数还没有被调用。实质上,这个函数内部仅仅是分配并初始化 stCoRoutine_t 结构体、设置任务函数指针、分配一段“栈”内存,以及分配和初始化 coctx_t。 625 | 626 | - ppco:输出参数,co_create内部为新协程分配一个协程控制块,ppco将指向这个分配的协程控制块。 627 | - attr:指定要创建协程的属性(栈大小、指向共享栈的指针(使用共享栈模式)) 628 | - pfn:协程的任务(业务逻辑)函数 629 | - arg:传递给任务函数的参数 630 | 631 | ### 启动协程(resume) 632 | 633 | ```c++ 634 | void co_resume( stCoRoutine_t *co ) 635 | { 636 | stCoRoutineEnv_t *env = co->env; 637 | stCoRoutine_t *lpCurrRoutine = env->pCallStack[ env->iCallStackSize - 1 ]; 638 | if( !co->cStart ) 639 | { 640 | coctx_make( &co->ctx,(coctx_pfn_t)CoRoutineFunc,co,0 ); 641 | co->cStart = 1; 642 | } 643 | env->pCallStack[ env->iCallStackSize++ ] = co; 644 | co_swap( lpCurrRoutine, co ); 645 | } 646 | ``` 647 | 648 | 在调用 co_create 创建协程返回成功后,便可以调用 co_resume 函数将它启动了。 649 | 650 | - 取当前协程控制块指针,将待启动的协程压入pCallStack栈,然后co_swap切换到指向的新协程上取执行,co_swap不会就此返回,而是要等当前执行的协程主动让出cpu时才会让新的协程切换上下文来执行自己的内容。 651 | 652 | ### 挂起协程(yield) 653 | 654 | ```c++ 655 | void co_yield_env( stCoRoutineEnv_t *env ) 656 | { 657 | stCoRoutine_t *last = env->pCallStack[ env->iCallStackSize - 2 ]; 658 | stCoRoutine_t *curr = env->pCallStack[ env->iCallStackSize - 1 ]; 659 | env->iCallStackSize--; 660 | co_swap( curr, last); 661 | } 662 | ``` 663 | 664 | - 在非对称协程理论,yield 与 resume 是个相对的操作。A 协程 resume 启动了 B 协程,那么只有当 B 协程执行 yield 操作时才会返回到 A 协程。在上一节剖析协程启动函数 co_resume() 时,也提到了该函数内部 co_swap() 会执行被调协程的代码。只有被调协程 yield 让出 CPU,调用者协程的 co_swap() 函数才能返回到原点,即返回到原来 co_resume() 内的位置。 665 | - 在被调协程要让出 CPU 时,会将它的 stCoRoutine_t 从 pCallStack 弹出,“栈指针” iCallStackSize 减 1,然后 co_swap() 切换 CPU 上下文到原来被挂起的调用者协程恢复执行。这里“被挂起的调用者协程”,即是调用者 co_resume() 中切换 CPU 上下文被挂起的那个协程。 666 | - 同一个线程上所有协程是共享一个 stCoRoutineEnv_t 结构的,因此任意协程的 co->env 指向的结构都相同。 667 | 668 | ### 切换协程(switch) 669 | 670 | - 上面的启动协程和挂起协程都设计协程的切换,本质是上下文的切换,发生在co_swap()中。 671 | - 如果是独享栈模式:将当前协程的上下文存好,读取下一协程的上下文。 672 | - 如果是共享栈模式:libco对共享栈做了个优化,可以申请多个共享栈循环使用,当目标协程所记录的共享栈没有被其它协程占用的时候,整个切换过程和独享栈模式一致。否则就是:将协程的栈空间内容从共享栈拷贝到自己的save_buffer中,将下一协程的save_buffer中的栈内容拷贝到共享栈中,将当前协程的上下文存好,读取下一协程上下文。 673 | - 协程的本质是,使用ContextSwap,来代替汇编中函数call调用,在保存寄存器上下文后,把需要执行的协程入口push到栈上。 674 | 675 | ```c++ 676 | void co_swap(stCoRoutine_t* curr, stCoRoutine_t* pending_co) 677 | { 678 | stCoRoutineEnv_t* env = co_get_curr_thread_env(); 679 | 680 | //get curr stack sp 681 | // 略 682 | } 683 | ``` 684 | 685 | 这里起寄存器拷贝切换作用的coctx_swap函数,是用汇编来实现的。 686 | 687 | - coctx_swap接受两个参数,第一个是当前协程的coctx_t指针,第二个参数是待切入的协程的coctx_t指针。该函数调用前还处于第一个协程的环境,调用之后就变成另一个协程的环境了。 688 | 689 | ```c++ 690 | extern "C" 691 | { 692 | extern void coctx_swap( coctx_t *,coctx_t* ) asm("coctx_swap"); 693 | }; 694 | ``` 695 | 696 | coctx_swap不再介绍 697 | 698 | ### 协程的事件管理 699 | 700 | https://segmentfault.com/a/1190000012834756 701 | 702 | https://segmentfault.com/a/1190000012656741 703 | 704 | ### hook系统 705 | 706 | **libco库通过仅有的几个函数接口 co_create/co_resume/co_yield 再配合 co_poll,可以支持同步或者异步的写法,如线程库一样轻松。同时库里面提供了socket族函数的hook,使得后台逻辑服务几乎不用修改逻辑代码就可以完成异步化改造。** 707 | 708 | #### 静态链接 709 | 710 | 在linux系统中,使用以下命令将源代码编译成可执行文件,源代码经过 预处理,编译,汇编,链接的过程最终生成可执行文件。一个简单的编译命令如下: 711 | 712 | ```shell 713 | gcc -o hello hello.c main.c -lcolib 714 | ``` 715 | 716 | ![Screen Shot 2021-11-08 at 2.26.13 AM](./协程和libco.assets/Screen Shot 2021-11-08 at 2.26.13 AM.png) 717 | 718 | 使用静态库有许多的缺点: 719 | 720 | 1. 可执行文件大小过大,造成硬盘的浪费 721 | 2. 如果库文件有更新,则依赖该库文件的可执行文件必须重新编译后,才能应用该更新 722 | 3. 假设有多个可执行文件都依赖于该库文件,那么每个可执行文件的`.code`段都会包含相同的机器码,造成内存的浪费 723 | 724 | #### 动态链接 725 | 726 | 为了解决静态链接的缺点,就出现了动态链接的概念。动态库这个大家都不会陌生,比如`Windows`的`dll`文件,`Linux`的`so`文件。动态库加载后在系统中只会存有一份,所有依赖它的可执行文件都会共享动态库的`code`段,`data`段私有。 727 | 动态链接的命令如下: 728 | 729 | ```SHELL 730 | gcc -o main main.o -L${libcolib.so path} -lcolib 731 | ``` 732 | 733 | ![Screen Shot 2021-11-08 at 2.27.19 AM](./协程和libco.assets/Screen Shot 2021-11-08 at 2.27.19 AM.png) 734 | 735 | #### 运行时的动态链接 736 | 737 | 系统为我们提供了dlsym、dlopen等函数,用于运行时加载动态库。可执行文件在运行时可以加载不同的动态库,这就为hook系统函数提供了基础。 738 | 739 | https://man7.org/linux/man-pages/man3/dlsym.3.html 740 | 741 | dlopen以指定模式打开指定的动态连接库文件,并返回一个句柄给调用进程,dlsym通过句柄和连接符名称获取函数名或者变量名。具体做法在这里就不介绍了。 742 | 743 | ## 参考资料 744 | 745 | https://juejin.cn/post/6961414532715511839 746 | 747 | https://github.com/chenyahui/AnnotatedCode/blob/master/coroutine/coroutine.c 748 | 749 | https://www.changliu.me/post/libco-coroutine/ 750 | 751 | https://zhuanlan.zhihu.com/p/94018082 752 | 753 | https://www.cyhone.com/articles/analysis-of-libco/ 754 | 755 | https://segmentfault.com/a/1190000012834756 756 | 757 | https://runzhiwang.github.io/2019/06/21/libco/ 758 | 759 | https://www.zhihu.com/question/52193579 760 | 761 | https://github.com/Tencent/libco/issues/90 --------------------------------------------------------------------------------