引子 2022年,我们很可能会看到Linux内核中的实验性Rust编程语言支持成为主流。2021。12。6早上发出了更新的补丁,介绍了在内核中处理Rust的初始支持和基础设施。 这次更新的内容包括:升级到了最新Stable编译器和Rust2021edition。因此可以摆脱了constfntransmute,constpanic、constunreachableunchecked、corepanic和tryreserve这几个之前未稳定的特性。自定义core和alloc。为alloc添加了更加模块化的选项,以便禁用一些他们不需要的功能:norc和nosync,主要是为上游Rust项目添加。更严格的代码、文档和新的lint。抽象和驱动程序更新。添加了序列锁、电源管理回调的抽象,io内存(readXwriteX)、irq芯片和高级流处理程序,gpio芯片(包括irq芯片)、设备、amba设备和驱动程序以及证书。此外,也改进并简化了Ref(refcountt支持)对象并用它替换了Rust的Arc的所有实例。完全地从alloccrate中删除了Arc和Rc。 从现在开始,Rustforlinux团队将开始定期提交补丁,每两周左右。 除了来自Arm、Google和Microsoft的支持外,这次该团队又收到一封来自红帽的信:红帽对Rust用于内核的工作也非常感兴趣(ThereisinterestinusingRustforkernelworkthatRedHatisconsidering)。v2补丁:https:lore。kernel。orglkml20211206140313。56531ojedakernel。orgwww。phoronix。comscan。php?pakernelcrate文档为什么需要引入Ref来代替Arc RustforLinux中这个kernelcrate中之前使用的是Arc,但是现在换成了Ref。通过查看相关PRrust:updateReftousethekernelsrefcountt,可以了解其中主要有两点原因:最大化利用现有的C代码和消除恐慌(Panic)。内核中已经有了引用计数的实现refcountt,而且它超过引用计数的阈值时,不是Panic(abort)而是返回最大值(饱和加法)。因为这个原因,也使用RBTree(红黑树)替代了BTreeMap。不需要弱引用。 Arc有一个MAXREFCOUNT的限制,是isize::MAXasusize大小,引用计数加法超过该大小就会溢出然后发生Panic(abort)。 所以最终实现的Ref与Arc的区别在于:Ref是基于内核的refcountt来支持的它不支持弱引用,所以大小减少了一半当它超过阈值时,它使得引用计数饱和(saturating)而非中止(abort)它不提供getmut方法,所以引用计数对象是Pin的。Ref源码分析 接下来分析一下Ref的实现。Ref结构体 Ref结构体定义如下:AreferencecountedpointertoaninstanceofT。Thereferencecountisincrementedwhennewinstancesof〔Ref〕arecreated,anddecrementedwhentheyaredropped。Whenthecountreacheszero,theunderlyingTisalsodropped。InvariantsThereferencecountonaninstanceof〔Ref〕isalwaysnonzero。Theobjectpointedtoby〔Ref〕isalwayspinned。pubstructRefT:?Sized{ptr:NonNullRefInnerT,p:PhantomDataRefInnerT,}复制代码 它维护一个不变量(Invariants):引用计数Ref总是一个非零的一个实例,并且被Ref引用的对象总是Pin的(不可移动)。 该结构体中使用NonNull,而非mutT,这里需要协变(covariant),而非不变(invariant)。可以参考下面示例:usestd::ptr::NonNull;structRefT:?Sized{x:NonNullT,x:mutT,如果换成mutT,编译将不会通过}fntakea(r:Refau32,y:au32){}fngive()Refstaticu32{todo!()}fntest(){lety1;协变,能传入Refau32的函数take,也能接收Refstaticu32类型的参数,因为static:a,能接受子类型,也能接受父类型take(give(),y);}复制代码 NonNull是mutT的协变版本,并且也代表了非空指针,代表了引用计数对象总是非空的,因为当计数为零就会释放。 而这里使用PhatomData则是为了Drop检查,此处表示Ref类型拥有RefInner,当Ref被Drop的时候,RefInner也能跟着被Drop。RefInner结构体 再来看RefInner结构体:〔repr(C)〕structRefInnerT:?Sized{refcount:Opaquebindings::refcountt,data:T,}复制代码 RefInner内部包含了内核中C语言实现的引用计数结构体refcountt,这里就是为了复用C代码。 其中Opaque类型是kernelcrate内置的专门为了和C打交道提供的一个包装类型,定义如下:pubstructOpaqueT(MaybeUninitUnsafeCellT);implTOpaqueT{Createsanewopaquevalue。pubfnnew(value:T)Self{Self(MaybeUninit::new(UnsafeCell::new(value)))}Createsanuninitialisedvalue。pubfnuninit()Self{Self(MaybeUninit::uninit())}Returnsarawpointertotheopaquedata。pubfnget(self)mutT{UnsafeCell::rawget(self。0。asptr())}}复制代码 Opaque类型意味着永远都不需要Rust代码来解释的FFi对象。所以,为了使用内核中已经存在的引用计数结构体,这里用Opaque类型。关于refcountt Linux内核中定义的refcountt结构体定义如下:from:https:github。comtorvaldslinuxblobmastertoolsincludelinuxrefcount。htypedefstructrefcountstruct{atomictrefs;}refcountt;复制代码 refcounttAPI的目标是为实现对象的引用计数器提供一个最小的API。虽然内部使用了原子操作,但一些refcount()和atomic()函数在内存顺序保证方面有很多不同。 refcountt在2018年曾经发生过引用计数溢出的安全漏洞,即,当引用计数达到最大值时,如果再加一,则引用计数就会归零。所以,此时引用的对象就会被错误释放。这样就变成了一个UAF(useafterfree)漏洞,容易被人利用。 所以现在refcountt被增加了引用计数检测:from:https:github。comtorvaldslinuxblobmastertoolsincludelinuxrefcount。hL69staticinlinerefcountcheckboolrefcountincnotzero(refcounttr){unsignedintold,new,valatomicread(rrefs);for(;;){newval1;if(!val)returnfalse;if(unlikely(!new))returntrue;oldatomiccmpxchgrelaxed(rrefs,val,new);if(oldval)break;valold;}REFCOUNTWARN(newUINTMAX,refcountt:saturated;leakingmemory。);returntrue;}复制代码 在达到引用计数最大时采用饱和(saturated)加法,即,返回最大的值,而非零。注意,这里使用了compareandswap原子操作且并未提供内存顺序(使用relaxed)。为Ref实现的一些trait 为了让Ref拥有一些类似于Arc的行为,所以为其实现一些内置trait。Thisistoallow〔Ref〕(andvariants)tobeusedasthetypeofself。implT:?Sizedcore::ops::ReceiverforRefT{}Thisistoallow〔RefBorrow〕(andvariants)tobeusedasthetypeofself。implT:?Sizedcore::ops::ReceiverforRefBorrow,T{}ThisistoallowcoercionfromRefTtoRefUifTcanbeconvertedtothedynamicallysizedtype(DST)U。implT:?SizedUnsizeU,U:?Sizedcore::ops::CoerceUnsizedRefUforRefT{}ThisistoallowRefUtobedispatchedonwhenRefTcanbecoercedintoRefU。implT:?SizedUnsizeU,U:?Sizedcore::ops::DispatchFromDynRefUforRefT{}SAFETY:ItissafetosendRefTtoanotherthreadwhentheunderlyingTisSyncbecauseiteffectivelymeanssharingT(whichissafebecauseTisSync);additionally,itneedsTtobeSendbecauseanythreadthathasaRefTmayultimatelyaccessTdirectly,forexample,whenthereferencecountreacheszeroandTisdropped。unsafeimplT:?SizedSyncSendSendforRefT{}SAFETY:ItissafetosendRefTtoanotherthreadwhentheunderlyingTisSyncforthesamereasonasabove。TneedstobeSendaswellbecauseathreadcancloneaRefTintoaRefT,whichmayleadtoTbeingaccessedbythesamereasoningasabove。unsafeimplT:?SizedSyncSendSyncforRefT{}复制代码 从上面代码里看得出来,用到的trait有:core::ops::Receiver:是一个未稳定特性(receivertraitfeatures),它表示一个结构体可以作为方法接收者,不需要arbitraryselftypes特性。标准库中一些智能指针实现了该trait,比如BoxArcRcTPin 等。core::ops::CoerceUnsized:也是一个未稳定特性(coerceunsizedfeatures),它表示将Size类型转换为DST类型。core::ops::DispatchFromDyn:同样是一个未稳定的特性(dispatchfromdynfeatures),它用于对象安全(动态安全dynsafe)的检查。实现DispatchFromDyn的类型可以安全地用作对象安全方法中的self类型。SendSync,是Rust中稳定的特性,用于标记线程间可安全传递和共享的类型。 现在为Ref实现了这些trait,那么Ref也就拥有了相应的行为。基本上Ref的行为和Arc类似了,除了上面所说的那些区别。引用计数管理 因为Ref是复用内核C代码,所以对于引用计数的管理,只需要实现相应的trait即可。 比如,Clone时应该自增引用计数,而Drop时应该自减引用计数。所以,分别来看一下这两个实现。实现ClonetraitimplT:?SizedCloneforRefT{fnclone(self)Self{INVARIANT:Crefcountincsaturatestherefcount,soitcannotoverflowtozero。SAFETY:Bythetypeinvariant,thereisnecessarilyareferencetotheobject,soitissafetoincrementtherefcount。unsafe{bindings::refcountinc(self。ptr。asref()。refcount。get())};SAFETY:Wejustincrementedtherefcount。ThisincrementisnowownedbythenewRef。unsafe{Self::frominner(self。ptr)}}}复制代码 实现Clonetrait很简单,直接通过bindings::refcountinc来调用内核中refcountt的自增方法refcountinc即可。 因为refcountinc已经是有了引用计数溢出检测,使用饱和加法,所以不用担心归零。实现DroptraitimplT:?SizedDropforRefT{fndrop(mutself){SAFETY:Bythetypeinvariant,thereisnecessarilyareferencetotheobject。WecannottouchrefcountafteritsdecrementedtoanonzerovaluebecauseanotherthreadCPUmayconcurrentlydecrementittozeroandfreeit。Itisoktohavearawpointertofreedinvalidmemoryaslongasitisneverdereferenced。letrefcountunsafe{self。ptr。asref()}。refcount。get();INVARIANT:Iftherefcountreacheszero,therearenootherinstancesofRef,andthisinstanceisbeingdropped,sothebrokeninvariantisnotobservable。SAFETY:Alsobythetypeinvariant,weareallowedtodecrementtherefcount。letiszerounsafe{bindings::refcountdecandtest(refcount)};ifiszero{Thecountreachedzero,wemustfreethememory。SAFETY:Thisthreadholdstheonlyremainingreferencetoself,soitissafetogetamutablereferencetoit。letinnerunsafe{self。ptr。asmut()};letlayoutLayout::forvalue(inner);SAFETY:Thevaluestoredininnerisvalid。unsafe{core::ptr::dropinplace(inner)};SAFETY:Thepointerwasinitialisedfromtheresultofacalltoalloc。unsafe{dealloc(self。ptr。cast()。asptr(),layout)};}}}复制代码 实现Droptrait,同样直接通过bindings::refcountdecandtest调用内核refcountdecandtest函数即可,该函数也包含了引用计数溢出检查。但是在引用计数归零的时候,需要释放内存。 注意上面Clone和Drop这两个trait的实现,是UnsafeRust抽象为SafeRust的一个经典范例,主要是其中的Safety注释,考虑了安全边界,并且加以说明。创建新的引用计数对象 接下来需要关注Ref如何创建新的引用计数对象。implTRefT{ConstructsanewreferencecountedinstanceofT。pubfntrynew(contents:T)ResultSelf{letlayoutLayout::new::RefInnerT();SAFETY:ThelayoutsizeisguaranteedtobenonzerobecauseRefInnercontainsthereferencecount。letinnerNonNull::new(unsafe{alloc(layout)})。okor(Error::ENOMEM)?。cast::RefInnerT();INVARIANT:Therefcountisinitialisedtoanonzerovalue。letvalueRefInner{SAFETY:JustanFFIcallthatreturnsarefcounttinitialisedto1。refcount:Opaque::new(unsafe{bindings::REFCOUNTINIT(1)}),data:contents,};SAFETY:inneriswritableandproperlyaligned。unsafe{inner。asptr()。write(value)};SAFETY:Wejustcreatedinnerwithareferencecountof1,whichisownedbythenewRefobject。Ok(unsafe{Self::frominner(inner)})}}复制代码 该trynew方法中使用core::alloc::Layout结构体来定义内存布局。 通过NonNull::new和自定义的core::alloc::alloc函数来分配新的内存,并转换为RefInner类型,并通过bindings::REFCOUNTINIT调用内核C函数对其初始化为1。其中自定义的core::alloc模块将来都会同步到rustcore中。 其中Error::ENOMEM代表OOM错误。在kernelerror。rs中定义了很多内核错误码对应的错误。 Linux内核中使用整数定义了很多错误码,在kernelcrate中,使用了NewType模式对其进行封装,而非直接使用整数错误码:macrorules!declareerr{(err:tt){pubconsterr:SelfError((bindings::errasi32));};(err:tt,(doc:expr),){(〔docdoc〕)pubconsterr:SelfError((bindings::errasi32));};}〔derive(Clone,Copy,PartialEq,Eq)〕pubstructError(ctypes::cint);implError{declareerr!(EPERM,Operationnotpermitted。);declareerr!(ENOENT,Nosuchfileordirectory。);declareerr!(ESRCH,Nosuchprocess。);declareerr!(ENOMEM,Outofmemory。);。。。}复制代码从已经存在的RefInner构造Refh1 在上面的trynew方法中看到,最后一步使用frominner方法将一个裸指针构造为最终的Ref。并且它是一个内部方法,不是公开的API。 注意,它是一个unsafe的方法,因为需要调用者来确保inner的指针是有效且非空的,对于这一点其文档注释也写的比较清楚。implT:?SizedRefT{Constructsanew〔Ref〕fromanexisting〔RefInner〕。SafetyThecallermustensurethatinnerpointstoavalidlocationandhasanonzeroreferencecount,oneofwhichwillbeownedbythenew〔Ref〕instance。unsafefnfrominner(inner:NonNullRefInnerT)Self{INVARIANT:Bythesafetyrequirements,theinvariantshold。Ref{ptr:inner,p:PhantomData,}}}复制代码RefBorrow 不存在对底层引用计数结构体的可变借用,但是存在一个不可变的借用,并且需要手动维护生命周期。Aborrowed〔Ref〕withmanuallymanagedlifetime。InvariantsTherearenomutablereferencestotheunderlying〔Ref〕,anditremainsvalidforthelifetimeofthe〔RefBorrow〕instance。pubstructRefBorrowa,T:?Sizeda{inner:NonNullRefInnerT,p:PhantomDataa(),}implT:?SizedCloneforRefBorrow,T{fnclone(self)Self{self}}implT:?SizedCopyforRefBorrow,T{}复制代码 RefBorrow结构体使用PhantomDataa()来持有生命周期参数,并为其实现Copytrait,其行为和普通的不可变引用类似。 然后为Ref实现一个asrefborrow方法即可从Ref得到RefBorrow。implTRefT{Returnsa〔RefBorrow〕fromthegiven〔Ref〕。Thisisusefulwhentheargumentofafunctioncallisa〔RefBorrow〕(e。g。,inamethodreceiver),butwehavea〔Ref〕instead。Gettinga〔RefBorrow〕isfreewhenoptimised。〔inline〕pubfnasrefborrow(self)RefBorrow,T{SAFETY:TheconstraintthatlifetimeofthesharedreferencemustoutlivethatofthereturnedRefBorrowensuresthattheobjectremainsalive。unsafe{RefBorrow::new(self。ptr)}}}复制代码 其实按Rust命名规范,此处asrefborrow改为asref更好。但是这里其实asref另有用处:implT:?SizedAsRefTforRefT{fnasref(self)T{SAFETY:Bythetypeinvariant,thereisnecessarilyareferencetotheobject,soitissafetodereferenceit。unsafe{self。ptr。asref()。data}}}复制代码 要通过asref方法从Ref得到T。 然后为RefBorrow实现Dereftrait,也可以从RefBorrow拿到T。implT:?SizedDerefforRefBorrow,T{typeTargetT;fnderef(self)Self::Target{SAFETY:Bythetypeinvariant,theunderlyingobjectisstillalivewithnomutablereferencestoit,soitissafetocreateasharedreference。unsafe{self。inner。asref()。data}}}复制代码唯一引用类型UniqueRef 除了Ref之外,还实现了一个UniqueRef类型。顾名思义,该类型表示只有唯一一个引用计数的情况。pubstructUniqueRefT:?Sized{inner:RefT,}implTUniqueRefT{Triestoallocateanew〔UniqueRef〕instance。pubfntrynew(value:T)ResultSelf{Ok(Self{INVARIANT:Thenewlycreatedobjecthasarefcountof1。inner:Ref::trynew(value)?,})}Triestoallocateanew〔UniqueRef〕instancewhosecontentsarenotinitialisedyet。pubfntrynewuninit()ResultUniqueRefMaybeUninitT{Ok(UniqueRef::MaybeUninitT{INVARIANT:Thenewlycreatedobjecthasarefcountof1。inner:Ref::trynew(MaybeUninit::uninit())?,})}}复制代码 没有为其实现Clone和Drop这两个trait,所以它只能持有唯一一个引用。引入该类型也许可以为内核开发提供更多便利。其他 Ref还实现了其他trait,比如FromTryFrom,可以从裸指针和Ref之间相互转换。 一个值得注意的地方是:implTRefT{Deconstructsa〔Ref〕objectintoarawpointer。Itcanbereconstructedoncevia〔Ref::fromraw〕。pubfnintoraw(obj:Self)constT{letretobjasconstT;core::mem::forget(obj);ret}}复制代码 将Ref转换为裸指针时,注意使用core::mem::forget(obj)避免调用obj的Drop,否则会让引用计数减少而引起问题。小结 从RustforLinux源码中可以学习很多UnsafeRust的相关技巧,尤其是和C语言打交道的一些比较好的实践。如果你感兴趣,还能学习Linux内核相关的一些内容,为将来是要Rust编写Linux内核驱动做一些准备。最后 如果你觉得此文对你有一丁点帮助,点个赞。或者可以加入我的开发交流群:1025263163相互学习,我们会有专业的技术答疑解惑 如果你觉得这篇文章对你有点用的话,麻烦请给我们的开源项目点点star:http:github。crmeb。netudefu不胜感激! PHP学习手册:https:doc。crmeb。com 技术交流论坛:https:q。crmeb。com