除夕夜在看《开端》,看了前几集,就似乎了有一种熟悉的感觉。
从程序员的视角来看,《开端》就是发现了一个bug后定位根因并解决的过程。期间不断通过重现bug排除错误答案。
重现第六次时,试图对bug视而不见,选择逃避。后因bug重现概率过高被boss强行拉回继续定位。
重现第十次时,确认了bug根因来源于程序内部,并非由外部API或环境因素触发。
又经过六次重现,排除了两个嫌疑重大的逻辑。
重现到第十七次,算是定位到了直接原因。
又花了七次重现,才最终定位到根因并解决bug。
除夕夜在看《开端》,看了前几集,就似乎了有一种熟悉的感觉。
从程序员的视角来看,《开端》就是发现了一个bug后定位根因并解决的过程。期间不断通过重现bug排除错误答案。
重现第六次时,试图对bug视而不见,选择逃避。后因bug重现概率过高被boss强行拉回继续定位。
重现第十次时,确认了bug根因来源于程序内部,并非由外部API或环境因素触发。
又经过六次重现,排除了两个嫌疑重大的逻辑。
重现到第十七次,算是定位到了直接原因。
又花了七次重现,才最终定位到根因并解决bug。
无锁编程,即访问多线程共享数据时,不加/解锁。
这里的“锁”并不特指mutex,还包括使用semaphore、条件变量、信号等构造出的线程挂起等待机制。甚至我们不使用这些操作系统提供的支撑,也可以写出一个“有锁”的接口(在接口中死等某个变量,类似spinlock)。
无锁操作,通常被抽象成方法、接口。比如说针对一个无锁的队列,pop、push就是它的无锁操作。Herlihy & Shavit 给无锁操作给出一个简洁的定义:调用无锁操作时,无论如何都不应该产生任何阻塞。
无锁编程有如下几点优势:
这里先介绍RMW原子操作,因为这是支撑各类无锁编程算法的基础。
原子操作的想必大家都熟知,它是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何上下文切换。
RMW(read-modify-write)原子操作,是指把“读-改-写”三步指令合并到一个原子操作里。例如以下两例,实现数的原子性增减
_InterlockedIncrement
OSAtomicAdd32
RMW原子操作需要CPU的支撑,当前各类主流的CPU都提供了类似的功能。
CAS(compare-and-swap)是一种RMW原子操作,它将以下操作封装在一个原子操作里:
伪代码如下:
1 | function cas(p: pointer to int, old: int, new: int) is |
在实际应用中,CAS函数常常返回*p的当前值。例如,想用CAS构造一个栈的push和pop,伪代码如下:
1 | push(node): |
无线网络建设一直是运营商网络综合成本(TCO)的最主要部分,大致占比在60%~70%。大部分运营商刚经历4G网络的巨大投资,就将面对5G网络的投资建设压力。
5G网络不同于4G网络,5G网的速度快,带宽大,频段高。也就是说,5G网络的穿透性会远差于4G网络。因此,未来中国就需要成百万甚至上千万的5G小基站。同时,5G网络又是运营商必须投资的网络,大规模建网势必带来极大的耗资。在“无线互联网”流量收入增长放缓、语音收入下降的背景下,垂直行业是运营商必须进入的“蓝海市场”,拓展运营商的盈利能力将是5G网络的首要任务。垂直行业的新业务意味着更多样的业务类型、更复杂的网络管理,需要更高效的资源管理方案以及更灵活的网络架构以便于开展业务创新。
在这一大背景下,2018年,在西班牙巴塞罗那一年一度的世界移动大会(MWC)期间,中国移动,美国AT&T,德国电信,日本NTT DOCOMO,法国的O-RANge宣布了O-RAN联盟的诞生。由运营商主导的O-RAN产业联盟应运而生,提出了“开放”和“智能”两大核心愿景。
O-RAN推动以下四个方向的发展:
O-RAN制定的标准,是3GPP标准的补充和增强。如图一所示:在3GPP定义的E1,F1,NG,Xn,X2的基础上,O-RAN进一步开放,定义了O1,O2,E2,A1,Open-FH等接口
为制定各类规范,O-RAN下设10个工作组(WG):
2016年,Facebook发起了一个叫做TIP(Telecom Infra Project,电信基础设施)的项目,下面包含了很多子项目,其中就有一个OpenRAN的项目计划。
2017年,全球运营商巨头沃达丰把自己研究的SDR RAN的成果奉献给了TIP,并创建OpenRAN工作组,旨在建立一个基于通用服务器,可软件定义技术的白盒化RAN解决方案。参与OpenRAN的运营商成员以欧美地区为主,中国的三大运营商都没有参与。项目由沃达丰和西班牙电信牵头,沃达丰负责全力推进。传统设备商中,除诺基亚积极参与之外,爱立信,华为和中兴都没有参与。此外新晋设备商三星对此也非常激进。此外,希望夹缝生存,在通信市场分得一杯羹的大量欧美新兴的中小设备商的参与非常积极,他们已经在全球开始部署OpenRAN商用网络,并开始组建自己的生态系统。
跟O-RAN联盟不同,OpenRAN工作组并没有对开放网络的内外部接口进行严格规范的定义,他们积极鼓励各运营商和设备商进行Open RAN网络的实际部署,并在外场进行互操作测试。
总体而言,O-RAN和OpenRAN这两个组织的参与成员虽然不尽相同,推进策略也各有侧重,但其目标和产品方案却大体一致,拥有非常广泛的共同语言。在2020年2月份,两者携起手来,共同在欧洲成立了开放测试和集成中心(OTIC),共享资源来进行Open RAN的研究和推进。OpenRAN组织与会使用和参考O-RAN制定的标准来推动开放式基站的部署。
O-RAN是侧重制定标准,而OpenRAN则侧重部署验证与产业推广。
上节中提到,Rust中,无Copy Trait的数据类型的函数传参,等效于所有权的转移。因此当函数退出时,接收所有权的变量作用域结束,数据就被销毁。那么,是否有办法让函数传参不发生所有权的转移?
有的!
Rust提供了被称为“引用”的机制。如下例:
1 | fn main() { |
以上程序可以被正常运行,变量s
不会在离开自己的作用域时销毁其指向的数据,因为它并不拥有数据的所有权。
1 | fn main() { |
编译结果:
1 | cargo build |
解析:引用是默认是不可变的,修改引用变量指向的数据是被禁止的。
Rust允许我们使用mut
将某个引用声明为“可变引用”
1 | fn main() { |
以上程序可以被正常执行,append_string
函数的入参类型被指定为&mut String
,即可变引用的String类型。append_string
中完成了对入参的修改。
Rust以数据访问安全著称。那么它是如何在支持可变引用的情况下,仍然可以避免数据的竞争的?大家可以通过以下编译错误的例子体会一下。
1 | fn main() { |
编译结果 :
1 | Compiling hello v0.1.0 (/home/spencer/share/my_code/rust_prj/hello) |
解析:以上例子中,r1和r2都为s的可变引用,且在同一个作用域中。Rust编译不不允许这样的代码存在。因为r1
和r2
可以在同一个作用域操作同一份数据,会产生数据竞争。
当我们用大括号将r1
的作用域加以限制之后,r1
和r2
的作用域不再重叠,就可以编译通过了。如下:
1 | fn main() { |
1 | fn main() { |
编译结果 :
1 | error[E0499]: cannot borrow `s` as mutable more than once at a time |
解析:与上一个错误类似,同样也出现了“second mutable borrow occurs here”的提示。而发生此错误的地方,是第4行使用原始变量s
做了数据修改的操作。编译器将使用原始可变变量做的数据修改,视为另一种可变引用。于是,s
和r1
产生了数据竞争,编译失败。
更确切地说,在以下三个条件同时满足时,会产生数据竞争,发出编译错误:
引用的原则:
代码段1:
1 | let s1 = String::from("hello"); |
编译结果:
1 | Compiling ownership v0.1.0 (file:///projects/ownership) |
解析:String的所有权从s1转移到s2后,不能再使用s1访问数据。否则违反原则2。
然而,对于下面这段代码,似乎产生了与代码段1矛盾的编译结果。
代码段2:
1 | fn main() { |
如果按代码段1的逻辑,u2=u1时,数字20的所有权传移给u2,u1应该不能再被访问。然而这段代码可以被正确地编译,这是为什么呢?
原因是:针对u32类型的数据,rust赋予其Copy特性(trait)。凡是拥有Copy trait的数据类型,“=”都表示数据的复制而非传有权的转移。因此在u2=u1之后,u2和u1里,都保存有20这一整型数据。
以下数据类型有Copy trait:
代码段3
1 | fn main() { |
编译结果:
1 | cargo build |
解析:函数的传参赋值,视作与“=”有相同的作用。即:变量s
被传入print_string
函数后,它的所有仅传移给some_string
,又因为原则三:“当所有者离开其作用域后,它所拥有的数据会被释放。”some_string
所拥有的字符串数据,在离开print_string
之后,即被释放。因此第9行的println!("{}", s);
对s
的访问编译失败。
而对整型数据的传参,则不存在类似的问题。因为整型数据有Copy trait,传参意味着数据的复制,而不是所有权的转移。因此第4行println!("{}", x);
并没有编译失败。
我们这里从TCP连接建立的前提入手。TCP连接建立的前提,是通信的双方都要知道本方和对方的发送和接收功能,都是正常的。也就是在TCP连接建立之前,有以下八个待确认项。
服务端发送 | 服务端接收 | 客户端发送 | 客户端接收 | |
---|---|---|---|---|
服务端 | ○ | ○ | ○ | ○ |
客户端 | ○ | ○ | ○ | ○ |
图例:○:待确认,√:已确认 |
客户端向服务端发送连接请求报文段。该报文段的头部中SYN=1,ACK=0,seq=x。
服务端接收到报文后,可以确认客户端的发送功能,服务端的接收功能,都是正常的。
服务端发送 | 服务端接收 | 客户端发送 | 客户端接收 | |
---|---|---|---|---|
服务端 | ○ | √ | √ | ○ |
客户端 | ○ | ○ | ○ | ○ |
服务端收到连接请求报文段后,如果同意连接,则会发送一个应答:SYN=1,ACK=1,seq=y,ack=x+1。
客户端接收到第二次握手报文后,可以确认服务端的发送功能,客户端的接收功能是正常的。不过,此时服务端并不知道自己的发送功能,客户端的接收功能是否正常。
服务端发送 | 服务端接收 | 客户端发送 | 客户端接收 | |
---|---|---|---|---|
服务端 | ○ | √ | √ | ○ |
客户端 | √ | √ | √ | √ |
当客户端收到连接同意的应答后,还要向服务端发送一个确认报文段,表示:服务端发来的连接同意应答已经成功收到。该报文段的头部为:ACK=1,seq=x+1,ack=y+1
此时,服务端就可以确认自己的发送功能,客户端的接收功能是正常的。
服务端发送 | 服务端接收 | 客户端发送 | 客户端接收 | |
---|---|---|---|---|
服务端 | √ | √ | √ | √ |
客户端 | √ | √ | √ | √ |
至此,八个确认项都确认完成。服务端和客户端就可以开始愉快地通信了。
家里迎来了新成员。真是很可爱。但免不了的手忙脚乱,新手爸爸还不太会抱娃。我最近的睡觉时间提前到了21:30,进入养生模式。就是为了每晚给娃留出2小时哭闹哄睡的时间。
这个博客更新得少了,最近个人项目的重心转到另一个项目中。我估且把它称为OneCmd。工作上本来就忙,现在又多了个娃,我估了一下进度(每周投入2~6小时)。OneCmd估计一年后才能上线。随性吧,反正也是玩票。
工作上,其实挺不如意的。项目进度紧,其实这倒不是最难的,难的是向兄弟部门和领导解释我们的工作量。为啥别人眼里的任务的工作量,和我们认为的任务的工作量,总是差距这么大。
最后再回到孩子身上,当抱抱起柔软的它时,之前对它的种种抱负都消失不见了。唯独希望,你平安喜乐。
Linux提供了非常丰富的手段,供我们来评估一个进程的内存占用。top,/proc/[pid]/status,/proc/[pid]/statm等等。什么RSS,RES,VIRT,到底哪个才是真正的进程使用内存量?
有没有简单的手段直接就能知识一个进程的内存占用?很遗憾地说,没有。因为内存的使用,本来就不简单。
但是我们可以找到相对简单的方式。
我们要先从进程的内存分布说起。Linux下,一个进程的内存分布如下:
从低到高,分别包括:
要评估一个进程的内存占用,就是要把以上几个段的内存占用一一加起来。
此文件包含了有关内存使用情况的重要信息,以Vm为前缀。
VmSize = VmRss + 申请但未使用的内存块
这个文件反应了运行时的进程的在内存中的完整分布。这是一张完整的清单。通过它可以看到对应进程所关联的所有的内存信息(包含共享的,和私有的)
smap示例:
1 | 7fc4d49df000-7fc4d49e1000 rw-p 001eb000 08:01 2102913 /lib/x86_64-linux-gnu/libc-2.27.so |
几个关键字段:
其中:
private = private_clean + private_dirty: 这个数据一般能够比较准确反映一个进程内部占用的内存,在内存优化的时候使用这个作为参考值比较合理,进程的物理内存占用就是smaps中所有的private的相加(链接的动态库的也要统计进去)。
昨晚和蚊子斗争了一晚上,斗争到睡不着。得出感悟,以下:
打蚊子的一晚上 | 找bug的一晚上 |
---|---|
一关灯蚊子就出来嗡嗡嗡,但是开灯手握电蚊拍,它就不见踪影。 | 一上生产环境bug就出现,但是打开调试开关,它就不复现。 |
好不容易打死一只,另一只又出来嗡嗡嗡,永远不知道房子里一共有多少只蚊子。 | 好不容易修复一个bug,又有别的bug出现,永远不知道程序里到底有多少bug。 |
不打死蚊子,撑起蚊帐,让蚊子不影响睡眠。 | 搞不定bug,引入规避手段,让bug不影响程序的核心功能。继续运行 |
结论:打蚊子与找bug真是一模一样。
用docker打包一个image,这个image的容器实现打包输出”hello docker”
我们计划使用echo命令打印,echo命令基于bash。因此,我们先搜索看看是否有bash相关的image。
1 | ~$ sudo docker search bash |
可以看到官方的bash image。因此我们可以基于此image来打包生成我们自己的image。
创建一个目录hello_docker,在下面创建子目录app,Dockerfile,和脚本hello.sh
1 | spencer@ubuntu:~/my_docker/hello_docker$ tree . |
hello.sh中,就是我们的容器需要执行的指令。
1 | echo "hello docker" |
Dockerfile是此步的关键,其内容是:
1 | FROM bash |
解释:
可以看出,Dockerfile中,除了CMD指令外,其它指令即是在告诉docker框架,如何一步一步地生成image。
执行以下命令打包:
1 | docker image build -t hello_docker . |
打包完成后,验证一下image已生成
1 | spencer@ubuntu:~$ docker image ls |
1 | spencer@ubuntu:~$ docker run hello_docker |
搞定!
Dockerfile是打包image的关键,此例中只用到了简单的几个。如果想进一步了解Dockerfile,请参考这里。