浅析overlayfs

overlayfs
overlayfs试图在其它文件系统之上提供一个联合的文件系统视图

Upper and Lower
overlayfs组合了2个文件系统—Upper文件系统和Lower文件系统。

当同名文件在Upper和Lower中都存在时,Lower中的文件会隐藏,如果是目录,则会合并显示.

Lower文件系统更准确的说法应该是目录树,因为这些目录树可以属于不同的文件系统。

Lower文件系统可以是linux支持的任何文件系统类型,其甚至可以是overlayfs.

目录合并
对于非目录文件,如果upper和lower中都存在,那么lower中的对象会被隐藏.

如果是目录,upper和lower中的目录会合并.

我们通过下面的命令来指定upperdir, lowerdir,它们将会合并成merged目录.

mount -t overlay overlay -o lowerdir=/root/lowerdir/,upperdir=/root/overlayt/upper,workdir=/root/overlayt/work /root/overlayt/merged

其中lowerdir目录中的内容如下:

[root@localhost overlayt]

ls -l /root/lowerdir/

总用量 4

-rw-r–r– 1 root root 4 3月  15 18:35 1

-rw-r–r– 1 root root 0 3月  15 18:35 2

-rw-r–r– 1 root root 0 3月  15 18:35 3

drwxr-xr-x 2 root root 6 3月  15 18:35 4

[root@localhost overlayt]

upper:我们的可写层

lower:底层目录树

merged:联合视图

workdir:需要是一个空目录,且和upper在同一个文件系统中

当在merged目录进行lookup操作时,lookup会在各个目录中搜寻并将结果联合起来缓存到overlayfs文件系统的dentry中。

如果upper,lower中存在相同的目录,则会合并在merged中显示

我们在lower中的dir1目录下创建low1文件

在upper的dir1目录下创建up1文件

在merged中的dir1目录下会同时出现up1,low1.

再来看看删除文件的处理
whiteouts和opaque目录
为了支持rm,rmdir操作,且不影响lowdir的内容,overlayfs需要在upper中记录被删除文件的信息。

whiteout是一个主从设备号为0/0的字符设备文件,当在merged目录下有whiteout文件时,lower目录中的同名文件会被忽略.whiteout文件本身也会隐藏,我们在upper目录中能够找到它.

readdir
当在merged目录调用readdir时,upper和lower目录的文件都会被读取(先读取upper目录,再读取lower目录).这个合并列表会被缓存在file结构中并保持到file关闭。如果目录被两个不同的进程打开并读取,那么它们有不同的缓存。seekdir设置偏移为0后再调用readdir会使缓存无效并进行重建。

这意味着,merged的变化在dir打开期间不会体现,对于很多程序容易忽略这一点。

非目录
对于非目录对象(文件,符号链接,特殊设备文件)。当对lowdir的对象进行写操作时,会先进行copy_up操作。创建硬链接也需要copy_up,软链接则不需要。

copy_up有时可能不需要,比如以read-write方式打开而实际上并没有进行修改.

copy_up处理的过程大致如下:

按需创建目录结构
用相同的元数据创建文件对象
拷贝文件数据
拷贝扩展属性  

copy_up完成之后,overlayfs就可以简单的在upper文件系统上提供对对象的访问。

多个lower layers
我们可以通过”:”来指定多个lower layers.

 mount -t overlay overlay -olowerdir=/lower1:/lower2:/lower3 /merged

在上面的例子中,我们没有指定upper,workdir.这意味着overlayfs是只读的。

多个lower layers从右边依次入栈,按照在栈中的顺序在merge中体现。lower3在最底层,lower1在是顶层.

非标准行为
copy_up操作创建了一个新的文件。新文件可能处于和老文件不同的文件系统中,因此st_dev,st_ino都是新的。

在copy_up之前老文件上的文件锁不会copy_up

如果一个有多个硬链接的文件被copy_up,那么会打断这种链接。改变不会传播给相同硬链接文件。

修改底层underlay文件系统
线下修改,当overlay没有mount时,允许修改upper,lower目录

不允许在mount overlay时对underlay进行修改。如果对underlay进行了修改,那么overlay的行为是不确定的。尽管不会crash或者deadlock.
————————————————
版权声明:本文为CSDN博主「self-motivation」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/happyAnger6/article/details/104885037

剑指offer p8:找出二叉树中序遍历的下一个节点

使用两种方法实现。

方法一:中序遍历二叉树,记录指定节点,当遍历到下一个节点时返回

时间复杂度o(n)

方法二:

1)当指定节点有右子树时,返回其右子树的第一个中序遍历节点

2)当指定节点无右子树时,如果其是父节点的左节点,则返回其父节点

3)当指定节点无右子树,且是其父节点的右节点,则一直向上找父节点,当找到某个父节点是其父节点的左节点时返回

时间复杂度o(logn)

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
class BinNode:
def __init__(self, val, left=None, right=None, father=None):
self.val = val
self.left = left
self.right = right

if self.left:
self.left.father = self
if self.right:
self.right.father = self

self.father = father

def __str__(self):
return self.val

def no_traverse_next(target):
if target.right:
r = target.right
while r.left:
r = r.left
return r

f = target
ff = f.father
while f and ff:
if ff.left == f:
return ff
else:
f = ff
ff = ff.father
return None


def inorder_traverse_next(root, target):
prev, node, stack = None, root, []
while node or stack:
if node:
stack.append(node)
node = node.left
else:
n = stack.pop()
if n == target:
prev = n
if prev and n != target:
return n
node = n.right
return None

if __name__ == "__main__":
i = BinNode('i')
h = BinNode('h')
g = BinNode('g')
f = BinNode('f')
e = BinNode('e', h, i)
d = BinNode('d')
c = BinNode('c', f, g)
b = BinNode('b', d, e)
a = BinNode('a', b, c)

print(inorder_traverse_next(a, i))
print(inorder_traverse_next(a, b))
print(inorder_traverse_next(a, g))

print(no_traverse_next(i))
print(no_traverse_next(b))
print(no_traverse_next(g)

leetcode竞赛题(一)----生成每种字符都是奇数个的字符串

有志同道合的朋友,可以大家一起交流监督学习。哈哈哈 !!!

5352. 生成每种字符都是奇数个的字符串
给你一个整数 n,请你返回一个含 n 个字符的字符串,其中每种字符在该字符串中都恰好出现 奇数次 。

返回的字符串必须只含小写英文字母。如果存在多个满足题目要求的字符串,则返回其中任意一个即可。

示例 1:

输入:n = 4
输出:”pppz”
解释:”pppz” 是一个满足题目要求的字符串,因为 ‘p’ 出现 3 次,且 ‘z’ 出现 1 次。当然,还有很多其他字符串也满足题目要求,比如:”ohhh” 和 “love”。
示例 2:

输入:n = 2
输出:”xy”
解释:”xy” 是一个满足题目要求的字符串,因为 ‘x’ 和 ‘y’ 各出现 1 次。当然,还有很多其他字符串也满足题目要求,比如:”ag” 和 “ur”。
示例 3:

输入:n = 7
输出:”holasss”  

提示:

1 <= n <= 500
我的解答:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Solution:
def generateTheString(self, n: int) -> str:
cur_len = 0
states = [0 for _ in range(26)]
i, bFind = 0, False
while i <= 26 and i >= 0:
if bFind:
break

if i == 26:
i-= 1
continue

while True:
if states[i] == 0:
j = 1
else:
j = states[i] + 2

cur_len-=states[i]
if cur_len + j == n:
states[i] = j
bFind = True
break
elif cur_len + j > n:
states[i] = 0
i-=1
break
else:
states[i] = j
cur_len += j
i+=1
break

s = ""
cnt = 0
for i, j in enumerate(states):
if j > 0:
c = chr(ord('a')+i)*j
s += c
cnt+=j

return s

python中的并发线程

python也提供了线程相关的并发原语,如锁threading.Lock,事件threading.Event,条件变量threading.Condition。

本质上都是对pthread_mutex_t, pthread_condition_t的封装。

本篇文章通过2个例子来分析理解python中如何控制并发。

1.实现2个线程交替打印

2.实现一个支持并发的环形队列

代码1:2个线程交替打印:


import threading
import time

c1 = threading.Condition() #用2个条件变量控制交替执行
c2 = threading.Condition()

def prt(i, wait, notify, name):
while True:
with wait:
wait.wait()
print(i, name)
i += 2
time.sleep(1)
with notify:
notify.notify_all()

t1 = threading.Thread(target=prt, args=(0, c1, c2, “thread1”, )) #等待通知交替传递
t2 = threading.Thread(target=prt, args=(1, c2, c1, “thread2”, ))

t1.start()
t2.start()

with c1: #选择一个线程先运行
c1.notify_all()

t1.join()
t2.join()


代码2:一个支持并发的环形队列实现

import threading

class RingQueue:
def init(self, maxsize):
self._maxsize = maxsize self._tail = 0 self._head = 0 self._len = 0 self.queue = [None for in range(maxsize)]
self._mutex = threading.Lock() #控制并发访问的线程锁
self.not_full = threading.Condition(self._mutex) #等待队列有空闲位置
self.not_empty = threading.Condition(self._mutex) #等待队列有数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def put(self, item):
with self.not_full:
while self._len == self._maxsize:
self.not_full.wait()

i = self._tail
self._queue[i] = item
self._tail = (self._tail + 1 ) % self._maxsize
if self._len == 0: #当前队列为空,则尝试唤醒可能的消费者
self.not_empty.notify()
self._len += 1
return i

def get(self):
with self.not_empty:
while self._len == 0:
self.not_empty.wait()
i = self._head
data = self._queue[self._head]
self._head = (self._head + 1) % self._maxsize
if self._len == self._maxsize: #如果队列满,则唤醒可能的生产者
self.not_full.notify()
self._len -= 1
return i

def producer(q):
while True:
for i in range(10000):
print(‘put’, q.put(i))

def consumer(q):
while True:
print(‘get’, q.get())

q = RingQueue(10)
t1 = threading.Thread(target=producer, args=(q,))
t2 = threading.Thread(target=consumer, args=(q,))

t1.start()
t2.start()

t1.join()
t2.join()  

我们再考虑为上面的队列加入以下需求:

1.我们想知道队列中的所有任务都被消费了,通常在关闭清除队列时需要知道。

我们可以通过在队列中加入另一个条件变量来实现

self.all_tasks_done = threading.Condition(self.mutex)
self.unfinished_tasks = 0
注意,这个新的条件变量和之前用于协调队列长度的锁是同一把锁。

然后增加下面2个方法:

def task_done(self):
‘’’
  当我们从队列中取出一个任务,并处理完成后调用这个方法.
通常消费者在调用get()并完成任务后调用,用于通知正在处理的任务完成.
如果当前有一个阻塞的join调用,那么当所有任务处理完成后,会解除阻塞.
在调用次数超过队列条目数量时抛出异常.
‘’’
with self.all_tasks_done:
unfinished = self.unfinished_tasks - 1
if unfinished <= 0:
if unfinished < 0:
raise ValueError(‘task_done() called too many times’)
self.all_tasks_done.notify_all()
self.unfinished_tasks = unfinished

def join(self):
‘’’阻塞到队列中的所有条目都被处理完成.
‘’’
with self.all_tasks_done:
while self.unfinished_tasks:
self.all_tasks_done.wait()
然后我们再修改put方法,每加一个任务都对unfinished_tasks进行加1.

def put(self, item):
with self.not_full:
while self._len == self._maxsize:
self.not_full.wait()

1
2
3
4
5
6
7
8
    i = self._tail
self._queue[i] = item
self._tail = (self._tail + 1 ) % self._maxsize
self.unfinished_tasks += 1 #有任务加入
if self._len == 0: #当前队列为空,则尝试唤醒可能的消费者
self.not_empty.notify()
self._len += 1
return i

 
————————————————
版权声明:本文为CSDN博主「self-motivation」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/happyAnger6/article/details/104452063

一行shell实现统计单词词频

写一个 bash 脚本以统计一个文本文件 words.txt 中每个单词出现的频率。

为了简单起见,你可以假设:

words.txt只包括小写字母和 ‘ ‘ 。
每个单词只由小写字母组成。
单词间由一个或多个空格字符分隔。
示例:

假设 words.txt 内容如下:

the day is sunny the the
the sunny is is
你的脚本应当输出(以词频降序排列):

the 4
is 3
sunny 2
day 1

代码:
cat words.txt tr -s ‘ ‘ ‘n’ sort uniq -c sort -rnk1 awk ‘{print $2,$1}’

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/word-frequency
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
————————————————

二十三式武功招式--总纲

扯淡

写程序犹如练武,一样需要内外兼修。

数据结构算法,操作系统原理,编译原理这些知识就犹如武侠中的内功心法,需要日夜旦夕苦练,经年累月方能有所小成。

而内功一旦有所成就,其它任何武功学起来就会轻松加愉快。

比如射雕中的郭靖,起初江南七怪教时,进展缓慢。自从全真教掌教马钰传授了一些呼吸吐纳的内功修练方法后,则进步神速。再如黑风双煞虽然有九阴真经下册,但也只学会了”九阴白骨掌”,始终无法达到五绝的水平,就是因为下册只有招式,没有上册的心法加持,难以更进一步。

说完了内功,再谈招式。招式是一些有着固定套路的功夫。招式可分为防守型和进攻型。针对不同的敌人,不同的兵器,不同的场景,可以采用一些固定的招工。你来一招”长虹贯日”,我还一招”有凤来仪”。如果不学扫式,对战起来就只能像一些市井之徒一样,乱打一通。而在程序里面,设计模式就如同招工一样,这23招都是无数高手在实战中总结出来的晶华,能够应对大多数的对战场景。但是招是死的,人是活的,好的招式最终还是要靠人去发挥其威力。

最后是兵器,编程语言就是兵器,我们有了内功和招式之后,还需要使用兵器。而兵器之中也有倚天剑,屠龙刀这种绝世神兵,即使是个小菜鸟,拿着这些兵器也能上来就发挥出唬人的威力。

习武的层次和写程序的层次也是一样的。起初我们可能沉迷于选择一些厉害的兵器,将一种兵器使用的得心应手之后。可能就会去练其它的兵器,并乐此不疲。练了几年之后,发现练来练去也无法提高了。这时候就需要去学习一些招式,所有的套路都练熟了之后。能否成为顶尖高手,比拼的就是内功修维了。这时候,我们最终想要达到的就是独孤求败这种不滞于物,草木皆可为剑的境界;以及独孤九剑这种随心而发,剑之所至,无招胜有招的境界。

这篇武功秘笈讲的是第2层修维,招式。即23种设计模式。

在架构设计,代码框架设计的过程中,我们始终逃不开的都是以下3类问题:对象创建,模块结构,组件行为交互。因此23种设计模式也是针对解决这3类问题的。因此这篇总纲就是先总体将23种招式化入这3种场景之中,供学习者抓住其要旨。

总纲

使用这23招的根本目的都是代码复用,以及方便扩展功能。而实现这一目标的关键就在于分离关注点,使得各组件能够独立地演化。

目的

创建型

结构型

行为型

范围

工厂方法(Factory Method)

适配器(Adapter使用继承)

解释器(Interpreter)

模板方法(Template Method)

对象

抽象工厂(Abstract Factory)

适配器(Adapter使用组合)

责任链(Chain of Responsibility)

构造者(Builder)

桥接模式(Bridge)

命令模式(Command)

原型模式(Prototype)

组合模式(Composite)

迭代器(Iterator)

单例(Singleton)

装饰器(Decorator)

仲裁者模式(Mediator)

门面模式(Façade)

备忘录模式(Memento)

享元模式(Flyweight)

观者者模式(Observer)

代理模式(Proxy)

状态模式(State)

策略模式(Strategy)

访问者模式(Visitor)

下面是本篇秘笈23式的招式及其要旨所在

  • 抽象工厂Abstract Factory:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类

  • 适配器Adapter:将一个类的接口转换为客户希望的另外一个接口。使得原本由于接口不兼容而不能一起工作的类可以一起工作

  • 桥接模式Bridge:将抽象部分与它的实现部分分离,使它们都可以独立地变化

  • 建造者模式Builder:将一个复杂对象的构建与表示分离,使得同样的构建过程可以创建不同的表示

  • 责任链Chain of responsibility:为解除请求的发送者和接收者之间的耦合,而使多个对象都有机会处理这个请求。这些对象形成一条链,并沿着此链传递请求,直到有一个对象处理它

  • 命令模式Command:将一个请求封装成一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录日志,以及支持可取消的操作

  • 组合模式Composite:将对象组合成树形结构以表示“整体-部分”的层次结构。组合模式使得单个对象和复合对象的使用具有一致性

  • 装饰器模式Decorator:动态地给一个对象添加一些额外的职责。就扩展功能而言,装饰器比生成子类更灵活

  • 门面模式Facade:为子系统中的一组接口提供一个一致的界面,其定义了一个高层次的接口,这个接口使得这一子系统更加容易使用

  • 工厂方法Factory Method:定义一个用于创建对象的接口,让子类决定将哪一个类实例化,其使一个类的实例化延迟到子类

  • 享元模式Flyweight:运用共享技术有效地支持大量细粒度的对象

  • 解释器Interpreter:给定一个语言,定义它的文法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中的句子

  • 迭代器Iterator:提供一种方法顺序访问一个聚合对象中各个元素,而又不暴露该对象的内部表示

  • 仲裁者Mediator:用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从而使其松耦合,而且可以独立地改变它们之间的交互

  • 备忘录Memnto:在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可以将对象恢复到保存的状态

  • 观者者Observer:定义对象间的一种一对多的依赖关系,以便当一个对象的状态发生变化时,所有依赖于它的对象都得到通知并自动刷新

  • 原型Prototype:用原型实例指定创建对象的种类,并且通过拷贝这个原型来创建新的对象

  • 代理Proxy:为其他对象提供一个代理以控制对这个对象的访问

  • 单例Singleton:保证一个类仅有一个实例,并提供一个访问它的全局访问点

  • 状态State:允许一个对象在内部状态改变时改变它的行为。对象看起来似乎修改了它所属的类

  • 策略Strategy:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。本模式使得算法的变化可独立于使用它的客户

  • 模板方法Template Method:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤

  • 访问者Visitor:表示一个作用于某种对象结构中的各元素的操作。它可以使你在不改变各元素类的前提下定义作用于这些元素的新操作

既然是招式,必然要熟悉其应用的场景,而且要经过大量的实战练习。因此后面的秘笈将以实际的代码为基础进行讲解。

除了一些便于记忆的实际代码外还会加入优秀开源框架代码中的例子,以便能够参考高手是如何使用这些招式的。

gRPC C++源码剖析(二) ---------数据结构篇之闭包调度器

grpc_closure_scheduler
顾名思义,闭包调度器的作用就是对闭包进行调度。

下面是它的定义:

struct grpc_closure_scheduler {
  const grpc_closure_scheduler_vtable* vtable;
};

typedef struct grpc_closure_scheduler_vtable {
  void (run)(grpc_closure closure, grpc_error* error);
  void (sched)(grpc_closure closure, grpc_error* error);
  const char* name;
} grpc_closure_scheduler_vtable;

通过上一节的介绍,我们知道在创建闭包是会为其指定调度器,然后可以调用GRPC_CLOSURE_RUN(closure, error)在调度器上运行闭包或者调用GRPC_CLOSURE_SCHED(closure, error)在调度器上调度闭包。

其实这2个方法就是对调度器run和sched方法的调用。

调度的作用就是为闭包提供运行环境,那么gRPC提供了哪些调度器,这些调度器的作用和应用场景有是哪些呢?

 
grpc_schedule_on_exec_ctx
这个调度器可能是用的最多的了,它的作用是在当前线程的ExecCtx上运行闭包。

有必要先了解一下ExecCtx。代码中对这个结构的作用说明是在调用栈上收集数据信息,是不是太抽象了。

考虑下面的场景:


A,B,C,D为4个依次调用的函数,B执行过程中调度了一个闭包1,并希望其在返回到A时执行。D同样调度了闭包2,也希望返回到A时再执行。这个时候就要使用这个调度器了grpc_schedule_on_exec_ctx.

要达到这种目的,我们要做的就是在A中声明ExecCtx,然后在B,D中调度绑定给grpc_schedule_on_exec_ctx调度器的闭包,然后在返回A时调用ExecCtx的Flush方法,这样就能达到目的了.ExecCtx内部维护了一个队列,用于存放调度给它的闭包。调用Flush方法时再依次从队列中取出闭包并运行。

在最新的gRPC版本中,简化了这种调度器的使用方法。gRPC框架为每个线程创建了这个ExecCtx,并存放在线程私有变量中,当需要时,只需要调用grpc_core::ExecCtx::Get()即可获取并使用。

可能你会问,搞这么复杂有什么意义呢?

我认为可能有以下2个好处:

1.可以使代码更加清晰

2.可以避免和缓解调用栈层次过深

你还有什么见解,欢迎来交流。

global_executor
全局调度器,这个调度器是在gRPC启动时创建的。它内部使用一个线程池,最多会创建CPU核心数相同的线程。

在这个调度器上调度的闭包会在这些全局线程中运行。

gRPC主要使用它来运行一些阻塞的任务,如dns解析。

要使用这个调度器,使用grpc_executor_scheduler这个接口

好了调度器的相关知识就介绍完了,理解调度器对我们理解gRPC的代码执行流有很大的帮助。
————————————————
版权声明:本文为CSDN博主「self-motivation」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/happyAnger6/article/details/102753742

gRPC C++源码剖析(二)---- 数据结构篇之闭包

上篇文章中提到了阅读gRPC源码的几大困难,其中数据结构是基础中的基础。

如果连这些数据结构的原理和作用都不了解的话,阅读起代码来肯定事倍功半。因此这篇文章对gRPC提供的数据结构进行讲解。

grpc_closure闭包
闭包是一些编程语言中提供的功能,如python. 

closure就是闭包的英文名称.

简单的理解,闭包函数将创建闭包时的上下文中的变量与自己绑定在一起,将变量的生存期和作用域延长到闭包函数结束。

概念有点儿抽象,下面是python中一个闭包的例子:

def add_n(n):
def real_add(m):
nonlocal n
n+=1
return n + m
return real_add

f = add_n(10)
print(f(5)) //输出16
print(f(5)) //输出17
注意real_add函数,它将函数体外的变量n与自己绑定,能够访问和修改它.

那么闭包有什么好处呢?

通过上面的例子,我们可以看到创建闭包时将变量与其绑定,在闭包实际运行时再使用它。

这样能够方便地在创建闭包时即将当前上下文中的变量传递给它,因为在运行闭包时并不容易再得到这个变量。

这个特性十分有利于在gRPC中方便的编写异步代码。

为了方便地使用闭包,gRPC提供了下面4个宏:

GRPC_CLOSURE_CREATE(cb, cb_arg, scheduler):创建一个闭包,返回创建后的闭包。参数分别为:回调函数,参数,调度器

GRPC_CLOSURE_INIT(closure, cb, cb_arg, scheduler):初始化一个闭包,第一个参数为要初始化的闭包。后面3个参数同上

GRPC_CLOSURE_RUN(closure, error):立即运行一个闭包,并传递错误状态.

GRPC_CLOSURE_SCHED(closure, error):调度一个闭包,并传递错误状态。

可以看出,创建闭包时将当前上下文的参数cb_arg传递给闭包对象保存,实际运行闭包时闭包就会使用这个创建时的参数。

上面提到的调度器在下文会详细介绍。

运行(GRPC_CLOSURE_RUN)和调度闭包(GRPC_CLOSURE_SCHED)的区别是:运行闭包立即运行。调度闭包是在指定的调度器上运行闭包,运行上下文可能是当前线程,也可能是另外的线程。

最后看gRPC源码中一个实际使用闭包的例子:

创建闭包
tcp_server_start()

{
    …
    grpc_tcp_listener* sp;
    …
            GRPC_CLOSURE_INIT(&sp->read_closure, on_read, sp,
                              grpc_schedule_on_exec_ctx);
}

在启动tcp监听时,创建一个read_closure闭包,并将当前的监听者信息绑定到闭包上。

运行闭包
当epoll循环监听到有连接接入时,会实际运行闭包.

        fd_become_readable(fd, pollset)—-> fd->read_closure->SetReady()—->GRPC_CLOSURE_SCHED((grpc_closure*)curr, GRPC_ERROR_NONE);;

这时候就会实际调用on_read函数

static void on_read(void* arg, grpc_error* err) {
  grpc_tcp_listener* sp = static_cast(arg);

}

on_read函数这时就能够使用创建时绑定的sp变量了.

怎么样,通过上面的讲解,应该知道了闭包的原理和作用了。再在源码中看到闭包相关代码,应该能够理解了吧!?

下一篇将介绍调度器
 
————————————————
版权声明:本文为CSDN博主「self-motivation」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/happyAnger6/article/details/102750612

gRPC C++ 源码剖析(一)----------入门

通过一段时间阅读gRPC c++的源码,对其实现原理算是初窥门境了。

在这里通过一系列循序渐进的文章把其中的经验和学习到东西分享出来,希望志同道合之人能够共同交流进步。

gRPC c++源码难吗?

个人认为gRPC c++源码算是质量比较高的源码了,google工程师们的抽象和设计能力都能够在其中有所体现。

可是阅读其源码还是有不少困难的,个人认为造成源码阅读困难的原因有以下几个:

  1. 是用C++写的

出于性能和灵活性的考虑,gRPC的核心代码是用C++写的,其中有部分代码还是C的。最新的gRPC代码已经将大部分C代码用C++重写了。

如果对C++语言不熟悉的话,就很容易被gRPC所使用的很多C++语言特性所迷惑,而不得其要旨。比如模板,智能指针,继承多态等等。

2.很多高级的抽象

gRPC的源码为了简化异步代码的编写,同时为了更好的代码复用。设计了许多高级的数据结构。如grpc_closure,grpc_closure_scheduler,ExecCtx,grpc_combiner,grpc_completion_queue等。如果不能很好地理解这些高级数据结构的作用和原理,阅读起代码来也会事倍功半。

3.大量的设计模式

为了提供跨平台能力,grpc核心代码采用了bridge设计模式,因此可以看到各种vtable,这也为阅读源代码增加了困难。

4.异步编程

grpc核心库采用reactor设计模式,如grpc_closure就是为了方便异步编程而设计的数据结构。

5.高性能

出于性能考虑,gRPC还使用了无锁队列这种高级数据结构,不熟悉其原理的读者可能会陷入各种原子操作的细节中。

阅读gRPC源码有哪些收获?

其实上面阅读源码的困难也就是收获,如何高效地在C中进行异步编程,如何抽象数据结构和使用设计模式,如何编写高性能代码,如何使用C++的高级特性等等。

说了这么多,是不是已经跃跃欲试了呢?让我们从下节开始吧!