Free5GC源码研究(12) - AMF研究(一)
本文研究核心网控制面中最重要的功能,Access and Mobility Management Function(AMF)所主要实现的功能
AMF主要负责管理用户设备的注册流程及跟踪其移动位置。虽然听起来很轻巧,但AMF的处理逻辑是所有NF中最复杂琐碎的,其代码量占到了free5gc所有代码的四分之一。下面的是一张从详细描述AMF服务的TS29.518摘取的AMF与其他NF交互的接口图示

由此可见AMF也是与其他NF交互最多的NF,牵涉到了我们此前研究过的所有NF。
AMF的概念
AMF的核心流程是为用户设备完成注册流程,在核心网和接入网中建立设备的上下文数据结构,以及追踪设备的位置移动,在核心网和接入网中的各个AMF和基站中传递该设备的上下文数据结构。
注册流程
AMF提供了4种不同的注册流程
- 初始注册:用户设备开机后第一次连接网络时的注册流程,完成标识(Identification)、鉴权(Authentication)和授权(Authorisation)等步骤,建立初始的上下文数据结构,用于后续业务。
- 移动性注册:告知网络该设备移出当前注册区域,应当更新注册区域和网络中UE的能力和协议参数等。
- 周期性注册:周期性地与网络同步UE所在位置。
- 紧急注册: 处于限制服务状态的设备请求紧急服务,也就是说没有充钱也能打110和120.
下图是从TS23.502种摘取的一般注册流程,囊括了上述4种注册。

对注册流程的详细解读可以参考free5gc官方团队对于注册流程的解释文章。图中的20多个步骤可以大致归纳为4个部分:
- 连接的建立与请求:建立从设备UE到核心网AMF的连接,并将RegistrationRequest消息发送给AMF。
- 鉴权与安全建立:对UE进行身份验证,并完成安全机制的建立(NAS层)。如果是周期性注册或紧急注册,这一部分则跳过不执行。
- 签约数据与策略获取:从UDM和UDR种获取用户的签约数据,比如能访问的数据网络DN有哪些、能访问的网络切片NSSAI由哪些等,还有向PCF获取策略数据,例如各种AmPolicy和SmPolicy。
- 后续步骤:主要包括了建立PDU session、发送Registration Accept和Registration Complete来结束注册流程
移动性管理
当用户携带着设备从一个地方移动到另一个地方,超出了原本基站和AMF的服务覆盖范围后,它需要钱换到新的基站和AMF。此时网络需要把该设备的上下文数据从原基站和原AMF移交(handover)到新基站和新AMF。这就是移动性管理的核心任务。移交过程涉及源基站、目标基站、源AMF、目标AMF等多个实体的协同,需要确保UE的所有上下文数据正确的迁移,处理各种失败场景,如切换失败、回退等,所以也是一个复杂的过程。一个典型的移交过程一半会涉及这几个子过程:
- 源基站决定需要切换
- 源AMF选择新AMF
- 新AMF创建UE上下文(CreateUEContext)
- 目标基站准备网络资源
- 执行切换
- 释放源基站网络资源
- 更新源AMF和新AMF与其他NF的关联关系
这每个子过程都可以摊开来详细说明,又是一张张十几个步骤的时序图,因而不在这里赘述,详细可参考TS23.503-4.9。
一个合理的疑问是,基站间切换可以理解,为什么AMF也需要切换?AMF不只是云核心网中的一个软件进程吗,理应可以服务任何地理位置的设备啊?为什么不用一个AMF实例从头到尾管理一个设备,这样还能略去AMF之间进行handover的复杂过程?
其实这在理论上是可以的,技术上也没有硬性限制一个AMF不能覆盖全网所有地理位置。AMF之间进行切换更多是服务质量的考虑。虽说AMF是运行在云上,但一朵云本质上还是一个分布式计算机网络,其中各个计算机节点部署在不同的地理位置。所以一个AMF实例本质上也是某个地理节点上的软件进程。当设备移动距离较远时,与原AMF的通信延迟会增加,这会影响到服务质量。所以一般来说一个AMF会有一个地理上的服务范围(Tracking Area,TA)。当设备移动到该AMF的TA以外时,就需要进行切换,旧AMF回把对该设备的管理移交到距离它更近的新AMF。除此以外,负载均衡、故障处理、漫游等原因也会触发AMF之间的切换。
不仅是AMF需要移动性管理,SMF和PCF等对时延要求高的NF也会需要。对于我们研究过的各NF来说,时延要求的严格程度排序是AMF > SMF > PCF > UDM/UDR,而对数据一致性的要求则恰好相反UDM/UDR > PCF > NRF > AMF/SMF。正是这不同的业务和型能要求,核心网中不同NF的部署方式会有所不同。AMF和SMF一般按照地理区域分布式部署
区域1: 区域2: AMF1 --> SMF1 AMF2 --> SMF2 | | UPF1(本地) UPF2(本地) | | 本地数据网络 本地数据网络
UDM和UDR则更多会使用集中式部署
主数据中心: 备用数据中心: UDM1/UDR1 <----> UDM2/UDR2 (主) (备) ^ | AMF1 AMF2 AMF3 AMF4
解与他们之间的PCF和NRF则通常会使用混合部署
区域PCF/NRF: 中心PCF/NRF: PCF1/NRF1 --> PCF/NRF-Central | \ | AMF1 AMF2 全局管理 | | 本地数据 本地数据
AMF的实现
AMF的Context承载了许多重要数据:
// https://github.com/free5gc/amf/blob/main/internal/context/context.go#L54 type AMFContext struct { EventSubscriptionIDGenerator *idgenerator.IDGenerator EventSubscriptions sync.Map UePool sync.Map // map[supi]*AmfUe RanUePool sync.Map // map[AmfUeNgapID]*RanUe AmfRanPool sync.Map // map[net.Conn]*AmfRan SupportTaiLists []models.Tai SupportDnnLists []string SecurityAlgorithm SecurityAlgorithm NgapIpList []string // NGAP Server IP // omitted more lines ...... }
其中UePool
存储了该AMF管理的所有设备上下文数据,RanUePool
维护者接入网基站与设备之间的连接关系,而AmfRanPool
则维护者AMF与各接入网基站的关系,SupportTaiLists
说明了该AMF的服务范围,SupportDnnLists
说明了该AMF支持接入哪些数据网络。
Namf_Communication
AMF的sbi接口中最重要的一组服务是Namf_Communication
,为用户设备提供了接入和移动性管理的功能。这一组服务里包含的操作多大20+个,在TS29.518的表格中详细列出:
其中包含在free5gc/amf@v1.2.5中的操作有15个,其中3个还是尚未实现的。
// https://github.com/free5gc/amf/blob/v1.2.5/internal/sbi/api_communication.go func (s *Server) HTTPAMFStatusChangeSubscribeModify(c *gin.Context) func (s *Server) HTTPAMFStatusChangeUnSubscribe(c *gin.Context) func (s *Server) HTTPCreateUEContext(c *gin.Context) func (s *Server) HTTPEBIAssignment(c *gin.Context) func (s *Server) HTTPRegistrationStatusUpdate(c *gin.Context) func (s *Server) HTTPReleaseUEContext(c *gin.Context) func (s *Server) HTTPUEContextTransfer(c *gin.Context) func (s *Server) HTTPN1N2MessageUnSubscribe(c *gin.Context) func (s *Server) HTTPN1N2MessageTransfer(c *gin.Context) func (s *Server) HTTPN1N2MessageTransferStatus(c *gin.Context) func (s *Server) HTTPN1N2MessageSubscribe(c *gin.Context) func (s *Server) HTTPNonUeN2InfoUnSubscribe(c *gin.Context) func (s *Server) HTTPNonUeN2MessageTransfer(c *gin.Context) func (s *Server) HTTPNonUeN2InfoSubscribe(c *gin.Context) func (s *Server) HTTPAMFStatusChangeSubscribe(c *gin.Context) func (s *Server) HTTPNonUeN2InfoUnSubscribe(c *gin.Context) { logger.CommLog.Warnf("Handle Non Ue N2 Info UnSubscribe is not implemented.") c.JSON(http.StatusNotImplemented, gin.H{}) } func (s *Server) HTTPNonUeN2MessageTransfer(c *gin.Context) { logger.CommLog.Warnf("Handle Non Ue N2 Message Transfer is not implemented.") c.JSON(http.StatusNotImplemented, gin.H{}) } func (s *Server) HTTPNonUeN2InfoSubscribe(c *gin.Context) { logger.CommLog.Warnf("Handle Non Ue N2 Info Subscribe is not implemented.") c.JSON(http.StatusNotImplemented, gin.H{}) }
free5gc官方团队在AMF设计文档中用一张图表示出了几个可能是他们认为最重要的几个操作:

我们先研究一下与UEContext相关的3个:创建HTTPCreateUEContext
、释放HTTPReleaseUEContext
、以及转移HTTPUEContextTransfer
。
创建操作是先初始化一个设备上下文结构ue := amfSelf.NewAmfUe(ueContextID)
,然后把createUeContextRequest
中的数据逐渐填入到新的ueContext中
// https://github.com/free5gc/amf/blob/v1.2.5/internal/sbi/processor/ue_context.go#L136 func (p *Processor) HandleCreateUEContextRequest(c *gin.Context, createUeContextRequest models.CreateUeContextRequest) { ueContextID := c.Param("ueContextId") createUeContextResponse, ueContextCreateError := p.CreateUEContextProcedure(ueContextID, createUeContextRequest) c.JSON(http.StatusCreated, createUeContextResponse) } // https://github.com/free5gc/amf/blob/v1.2.5/internal/sbi/processor/ue_context.go#L33C1-L133C2 func (p *Processor) CreateUEContextProcedure(ueContextID string, createUeContextRequest models.CreateUeContextRequest) ( *models.CreateUeContextResponse, *models.UeContextCreateError, ) { amfSelf := context.GetSelf() ueContextCreateData := createUeContextRequest.JsonData ue := amfSelf.NewAmfUe(ueContextID) // create the UE context in target amf ue.HandoverNotifyUri = ueContextCreateData.N2NotifyUri amfSelf.AmfRanFindByRanID(*ueContextCreateData.TargetId.RanNodeId) supportedTAI := context.NewSupportedTAI() supportedTAI.Tai.Tac = ueContextCreateData.TargetId.Tai.Tac supportedTAI.Tai.PlmnId = ueContextCreateData.TargetId.Tai.PlmnId ue.UnauthenticatedSupi = ueContextCreateData.UeContext.SupiUnauthInd ue.RoutingIndicator = ueContextCreateData.UeContext.RoutingIndicator // optional ue.UdmGroupId = ueContextCreateData.UeContext.UdmGroupId ue.AusfGroupId = ueContextCreateData.UeContext.AusfGroupId // ueContextCreateData.UeContext.HpcfId ue.RatType = ueContextCreateData.UeContext.RestrictedRatList[0] // minItem = -1 createUeContextResponse := new(models.CreateUeContextResponse) createUeContextResponse.JsonData = &models.UeContextCreatedData{ UeContext: &models.UeContext{ Supi: ueContextCreateData.UeContext.Supi, }, } createUeContextResponse.JsonData.PduSessionList = ueContextCreateData.PduSessionList createUeContextResponse.JsonData.PcfReselectedInd = false return createUeContextResponse, nil }
释放操作则相当简单,只是找出来,然后调用相关删除函数,删除UDM中设备的注册信息。
// https://github.com/free5gc/amf/blob/v1.2.5/internal/sbi/processor/ue_context.go#L149C1-L197C1 func (p *Processor) ReleaseUEContextProcedure(ueContextID string, ueContextRelease models.UeContextRelease, ) { amfSelf := context.GetSelf() ue, ok := amfSelf.AmfUeFindByUeContextID(ueContextID) gmm_common.RemoveAmfUe(ue, false) // 最终会调用 PurgeSubscriberData return nil } // https://github.com/free5gc/amf/blob/v1.2.5/internal/gmm/common/user_profile.go#L84 func PurgeSubscriberData(ue *context.AmfUe, accessType models.AccessType) error { // Purge of subscriber data in AMF described in TS 23.502 4.5.3 if ue.SdmSubscriptionId != "" { problemDetails, err := consumer.GetConsumer().SDMUnsubscribe(ue) ue.SdmSubscriptionId = "" } if ue.UeCmRegistered[accessType] { problemDetails, err := consumer.GetConsumer().UeCmDeregistration(ue, accessType) ue.UeCmRegistered[accessType] = false } return nil }
而Transfer则是旧AMF先把对应ueContextID
的数据找出来,然后将相关数据打包在发送出去新AMF。
// https://github.com/free5gc/amf/blob/v1.2.5/internal/sbi/processor/ue_context.go#L228 func (p *Processor) UEContextTransferProcedure(ueContextID string, ueContextTransferRequest models.UeContextTransferRequest) ( *models.UeContextTransferResponse, *models.ProblemDetails, ) { amfSelf := context.GetSelf() UeContextTransferReqData := ueContextTransferRequest.JsonData ue, ok := amfSelf.AmfUeFindByUeContextID(ueContextID) ueContextTransferResponse := &models.UeContextTransferResponse{ JsonData: new(models.UeContextTransferRspData), } ueContextTransferRspData := ueContextTransferResponse.JsonData switch UeContextTransferReqData.Reason { case models.TransferReason_INIT_REG: ueContextTransferRspData.UeContext = p.buildUEContextModel(ue, UeContextTransferReqData.Reason) // TODO: handle condition of TS 29.518 5.2.2.2.1.1 step 2a case b case models.TransferReason_MOBI_REG: ueContextTransferRspData.UeContext = p.buildUEContextModel(ue, UeContextTransferReqData.Reason) p.HandleMobiRegUe(ue, ueContextTransferRspData, ueContextTransferResponse) case models.TransferReason_MOBI_REG_UE_VALIDATED: ueContextTransferRspData.UeContext = p.buildUEContextModel(ue, UeContextTransferReqData.Reason) p.HandleMobiRegUe(ue, ueContextTransferRspData, ueContextTransferResponse) default: logger.ProducerLog.Warnf("Invalid Transfer Reason: %+v", UeContextTransferReqData.Reason) problemDetails := &models.ProblemDetails{...} return nil, problemDetails } return ueContextTransferResponse, nil }
buildUEContextModel
func (p *Processor) buildUEContextModel(ue *context.AmfUe, reason models.TransferReason) *models.UeContext { ueContext := new(models.UeContext) ueContext.Supi = ue.Supi ueContext.SupiUnauthInd = ue.UnauthenticatedSupi if reason == models.TransferReason_INIT_REG || reason == models.TransferReason_MOBI_REG { var mmContext models.MmContext mmContext.AccessType = models.AccessType__3_GPP_ACCESS NasSecurityMode := new(models.NasSecurityMode) switch ue.IntegrityAlg { case security.AlgIntegrity128NIA0: NasSecurityMode.IntegrityAlgorithm = models.IntegrityAlgorithm_NIA0 case security.AlgIntegrity128NIA1: NasSecurityMode.IntegrityAlgorithm = models.IntegrityAlgorithm_NIA1 case security.AlgIntegrity128NIA2: NasSecurityMode.IntegrityAlgorithm = models.IntegrityAlgorithm_NIA2 case security.AlgIntegrity128NIA3: NasSecurityMode.IntegrityAlgorithm = models.IntegrityAlgorithm_NIA3 } switch ue.CipheringAlg { case security.AlgCiphering128NEA0: NasSecurityMode.CipheringAlgorithm = models.CipheringAlgorithm_NEA0 case security.AlgCiphering128NEA1: NasSecurityMode.CipheringAlgorithm = models.CipheringAlgorithm_NEA1 case security.AlgCiphering128NEA2: NasSecurityMode.CipheringAlgorithm = models.CipheringAlgorithm_NEA2 case security.AlgCiphering128NEA3: NasSecurityMode.CipheringAlgorithm = models.CipheringAlgorithm_NEA3 } NgKsi := new(models.NgKsi) NgKsi.Ksi = ue.NgKsi.Ksi NgKsi.Tsc = ue.NgKsi.Tsc KeyAmf := new(models.KeyAmf) KeyAmf.KeyType = models.KeyAmfType_KAMF KeyAmf.KeyVal = ue.Kamf SeafData := new(models.SeafData) SeafData.NgKsi = NgKsi SeafData.KeyAmf = KeyAmf if ue.NH != nil { SeafData.Nh = hex.EncodeToString(ue.NH) } SeafData.Ncc = int32(ue.NCC) SeafData.KeyAmfChangeInd = false SeafData.KeyAmfHDerivationInd = false ueContext.SeafData = SeafData mmContext.NasSecurityMode = NasSecurityMode if ue.UESecurityCapability.Buffer != nil { mmContext.UeSecurityCapability = base64.StdEncoding.EncodeToString(ue.UESecurityCapability.Buffer) } mmContext.NasDownlinkCount = int32(ue.DLCount.Get()) mmContext.NasUplinkCount = int32(ue.ULCount.Get()) if ue.AllowedNssai[models.AccessType__3_GPP_ACCESS] != nil { for _, allowedSnssai := range ue.AllowedNssai[models.AccessType__3_GPP_ACCESS] { mmContext.AllowedNssai = append(mmContext.AllowedNssai, *(allowedSnssai.AllowedSnssai)) } } ueContext.MmContextList = append(ueContext.MmContextList, mmContext) } if reason == models.TransferReason_MOBI_REG_UE_VALIDATED || reason == models.TransferReason_MOBI_REG { sessionContextList := &ueContext.SessionContextList ue.SmContextList.Range(func(key, value interface{}) bool { smContext := value.(*context.SmContext) snssai := smContext.Snssai() pduSessionContext := models.PduSessionContext{ PduSessionId: smContext.PduSessionID(), SmContextRef: smContext.SmContextRef(), SNssai: &snssai, Dnn: smContext.Dnn(), AccessType: smContext.AccessType(), HsmfId: smContext.HSmfID(), VsmfId: smContext.VSmfID(), NsInstance: smContext.NsInstance(), } *sessionContextList = append(*sessionContextList, pduSessionContext) return true }) } if ue.Gpsi != "" { ueContext.GpsiList = append(ueContext.GpsiList, ue.Gpsi) } if ue.Pei != "" { ueContext.Pei = ue.Pei } if ue.UdmGroupId != "" { ueContext.UdmGroupId = ue.UdmGroupId } if ue.AusfGroupId != "" { ueContext.AusfGroupId = ue.AusfGroupId } if ue.RoutingIndicator != "" { ueContext.RoutingIndicator = ue.RoutingIndicator } if ue.AccessAndMobilitySubscriptionData != nil { if ue.AccessAndMobilitySubscriptionData.SubscribedUeAmbr != nil { ueContext.SubUeAmbr = &models.Ambr{ Uplink: ue.AccessAndMobilitySubscriptionData.SubscribedUeAmbr.Uplink, Downlink: ue.AccessAndMobilitySubscriptionData.SubscribedUeAmbr.Downlink, } } if ue.AccessAndMobilitySubscriptionData.RfspIndex != 0 { ueContext.SubRfsp = ue.AccessAndMobilitySubscriptionData.RfspIndex } } if ue.PcfId != "" { ueContext.PcfId = ue.PcfId } if ue.AmPolicyUri != "" { ueContext.PcfAmPolicyUri = ue.AmPolicyUri } if ue.AmPolicyAssociation != nil { if len(ue.AmPolicyAssociation.Triggers) > 0 { ueContext.AmPolicyReqTriggerList = p.buildAmPolicyReqTriggers(ue.AmPolicyAssociation.Triggers) } } for _, eventSub := range ue.EventSubscriptionsInfo { if eventSub.EventSubscription != nil { ueContext.EventSubscriptionList = append(ueContext.EventSubscriptionList, *eventSub.EventSubscription) } } if ue.TraceData != nil { ueContext.TraceData = ue.TraceData } return ueContext }
乍看一下都挺简单,但魔鬼都在细节中。一开始我理所应当地以为CreateUEContext
是在初始注册时被调用地操作,然而查阅TS29.518有关CreateUEContext
的解释,会发现这么一段话:
The CreateUEContext service operation is used during the following procedure:
- Inter NG-RAN node N2 based handover (see 3GPP TS 23.502 [3], clause 4.9.1.3, and clause 4.23.7
The CreateUEContext service operation is invoked by a NF Service Consumer, e.g. a source AMF, towards the AMF (acting as target AMF), when the source AMF can't serve the UE and selects the target AMF during the handover procedure, to create the UE Context in the target AMF.
也就是说,CreateUEContext
只能用于新旧AMF移交的过程,且是接入网基站切换中的AMF切换过程,此时旧AMF调用新AMF的CreateUEContext
来创建一个新的结构。
说到底,sbi接口是核心网控制面内个NF之间交互的接口,只能是NF之间相互调用,而UE和RAN是不可以通过sbi接口使用http协议与核心网通信的。因此,由UE和RAN发起的的初始注册请求,以及其他注册和移动性请求,肯定不是通过sbi接口与AMF通信,而是通过N1和N2接口使用nas和ngap协议向AMF发送消息。我们会在后文详细研究。
AMF的sbi接口模块,虽然看起来代码和函数很多,但其基本功能也就两个,一是上文研究的提供对UE Context的操作,这功能还基本上旨在新旧AMF之间切换时才会用到;第二个功能是作为UE/RAN与核心网其他NF之间的通信中继,毕竟只有核心网控制面中只有AMF能够与UE/RAN之间通信。这个功能我们也会在后文研究过nas和ngap协议后再讨论。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性