博主未授权任何人或组织机构转载博主任何原创文章,感谢各位对原创的支持!
博主链接
本人就职于国际知名终端厂商,负责modem芯片研发。
在5G早期负责终端数据业务层、核心网相关的开发工作,目前牵头6G算力网络技术标准研究。博客内容主要围绕:
5G/6G协议讲解
算力网络讲解(云计算,边缘计算,端计算)
高级C语言讲解
Rust语言讲解
文章目录
Open5GS 架构详解
官方网站:https://open5gs.org/
项目code地址:https://github.com/open5gs/open5gs
Open5GS安装步骤:https://open5gs.org/open5gs/docs/guide/01-quickstart/
Open5GS 项目介绍
下图是来自官方网站的软件架构图,图中展示了一些软件容器和一些NFs,Open5GS实现了 4G/5G NSA 和 5G SA 核心网功能。
4G/ 5G NSA Core
Open5GS 4G/ 5G NSA 核心网包括以下组件:
- MME - Mobility Management Entity
- HSS - Home Subscriber Server
- PCRF - Policy and Charging Rules Function
- SGWC - Serving Gateway Control Plane
- SGWU - Serving Gateway User Plane
- PGWC/SMF - Packet Gateway Control Plane / (component contained in Open5GS SMF)
- PGWU/UPF - Packet Gateway User Plane / (component contained in Open5GS UPF)
在4G/ 5G NSA Core实现了控制面和用户面的分离(CUPS)。
MME是主要的控制平面,负责管理会话、移动性、Paging和承载(bearers)。MME与HSS相连接,HSS会生成SIM卡的鉴权矢量以及签约用户的Profile。MME也会与网关服务器的控制平面 SGWC 和 PGWC/SMF相连接。在4G中的所有eNodeBs都会与MME相连接。控制平面的最后一个节点是PCRF,它位于PGWC/SMF和HSS之间,处理收费和执行订阅用户策略。
用户平面用于承载eNB/ NSA gNB (5G NSA基站)与外部广域网之间的用户数据报文。两个用户平面核心组件是SGWU和PGWU/UPF,每一个都与它们的控制平面连接。eNB / NSA gNG连接到SGWU, SGWU连接到PGWU/UPF,再连接到WAN。通过在物理上将控制面和用户面分开,这样就可以在现场部署多个用户面服务器(例如,有高速互联网连接的地方),同时还能保持集中的控制面功能。这有利于支持MEC的应用场景。
上面的控制面和数据面分离,以及控制面集中,数据面可以分布式部署的思想其实就是SDN的思想,关于SDN的介绍可以参考我的博客《【SDN vs. NFV】纠缠不清的SDN和NFV》
所有这些Open5GS组件都有配置文件。每个配置文件包含组件的IP绑定地址/本地接口名称,以及需要连接的其它组件的IP地址/ DNS名称。
5G SA Core
Open5GS 5G SA Core包括以下功能:
- AMF - Access and Mobility Management Function
- SMF - Session Management Function
- UPF - User Plane Function
- AUSF - Authentication Server Function
- NRF - NF Repository Function
- UDM - Unified Data Management
- UDR - Unified Data Repository
- PCF - Policy and Charging Function
- NSSF - Network Slice Selection Function
- BSF - Binding Support Function
5G SA核心网的工作方式与4G核心不同——它使用了基于服务的体系结构(SBI)。将控制面功能配置为向NRF注册,然后由NRF帮助控制面发现其需要的核心网服务。AMF处理连接和移动性管理,是4G MME任务的一个子集。gnb (5G基站)连接到AMF。UDM、AUSF和UDR执行与4G HSS类似的操作,生成SIM认证向量并保存用户配置文件。会话管理全部由SMF处理(以前是由4G MME/ SGWC/ PGWC负责)。NSSF提供了一种选择网络片的方法。最后是PCF,用于收费和执行订阅者策略。
5G SA核心网用户平面简单得多,只包含单一功能。UPF用于承载gNB与外网之间的用户数据报文,它也连接回SMF。除SMF和UPF外,所有5G SA核心网功能的配置文件中只包含该功能的IP绑定地址/本地接口名和NRF的IP地址/ DNS名。
上面5G核心网基于服务的架构,有一部分NFV的思想,关于NFV的介绍可以参考我的博客《【SDN vs. NFV】纠缠不清的SDN和NFV》
下面是项目的目录以及目录中的主要内容,
Open5GS 软件架构
Open5GS 主体由两部分组成,分别是**容器环境**和**NF实体**。在容器环境中包含了NF运行需要的最基本的服务,例如消息通知机制、内存管理和定时器等。NF实体就是具体运行的网络功能,架构中提供了统一的接口,每个NF只需要实现这个接口就能放入容器环境中运行。
在NF实体中,每个NF在初始化时都会创建一些池,这些池限定了实例(instance)的数量,例如socket句柄、激活的session、注册的ue等。同时还会初始化一个
sbi
实例,用于在不同NFs之间的通信功能。每个NF有一个有限状态机(FSM),状态机是消息驱动的。一般一个NF会提供多个services,NF首先从service池中获取一个service实例,然后初始化这个service,并存入hash表中方便查找。针对不同的service可能还有与这个service相关的FSM。
内存管理这里可以选择开源的内存池库
talloc
或者使用自开发的
ogs_pool
内存池,总之内存都会被提前开辟,尽量减少内存分配、释放甚至发生缺页异常时对性能的影响。
消息通知机制使用
I/O多路复用技术
实现消息接收、发送,并结合
queue
实现向FSM传递消息。
定时器机制也是借用
I/O多路复用技术
实现的,使用
红黑树
存储定时器,位于树根最左边的节点是最快超时的定时器。
Open5GS 配置文件
Open5GS 的配置文件格式为
.yaml
格式,每个网络功能(NF)都有一个配置文件,每个NF配置文件中必须要有绑定IP的地址,也就是这个NF自己的SBI地址和端口,一般还会有一个NRF的SBI地址和端口,用于NF向NRF注册自己以及服务发现,下面是一个AMF的配置文件:
amf:sbi:- addr:127.0.0.5port:7777ngap:- addr:127.0.0.5guami:- plmn_id:mcc:901mnc:70amf_id:region:2set:1tai:- plmn_id:mcc:901mnc:70tac:1plmn_support:- plmn_id:mcc:901mnc:70s_nssai:- sst:1security:integrity_order:[NIA2,NIA1,NIA0]ciphering_order:[NEA0,NEA1,NEA2]network_name:full: Open5GS
amf_name: open5gs-amf0
nrf:sbi:- addr:-127.0.0.10-::1port:7777
在每个NF启动之前会进行初始化操作,在初始化过程中会解析配置文件参数,并保存起来。Open5GS使用开源的
LibYAML
库(官方网站)对
.yaml
文件进行解析,相关代码如下:
// ~/open5gs-main/lib/app/ogs-init.cintogs_app_initialize(constchar*version,constchar*default_config,constchar*const argv[]){....../**************************************************************************
* Stage 2 : Load Configuration File
*/if(optarg.config_file)ogs_app()->file = optarg.config_file;// 用户自定义的配置文件路径elseogs_app()->file = default_config;//默认配置文件路径
rv =ogs_app_config_read();// 解析NF配置文件if(rv != OGS_OK)return rv;
rv =ogs_app_context_parse_config();// 解析NF配置文件if(rv != OGS_OK)return rv;......return rv;}
Open5GS所有默认的配置文件都放置在
~/open5gs-main/configs
中,如下图:
如果没有设置用户自定义的配置文件,则每个NF启动的时候会自动读取默认的配置文件,默认的配置文件路径在编译Open5GS的时候由meson配置文件自动生成,下图是AMF的meson配置文件:
# ~/open5gs-main/src/amf/meson.build......
executable('open5gs-amfd',
sources : amf_sources,
c_args :'-DDEFAULT_CONFIG_FILENAME="@0@/amf.yaml"'.format(open5gs_sysconfdir),
include_directories : srcinc,
dependencies : libamf_dep,
install_rpath : libdir,
install:true)
可以看到这里
c_args : '-DDEFAULT_CONFIG_FILENAME="@0@/amf.yaml"'.format(open5gs_sysconfdir)
定义配置文件的默认路径。
如何传递用户自定义的配置文件?
通过添加运行时参数-c custom_confg_file_path
即可
YAML的github项目链接
一些简单的yaml语法
YAML 是 “YAML Ain’t a Markup Language”(YAML 不是一种标记语言)的递归缩写。在开发的这种语言时,YAML 的意思其实是:“Yet Another Markup Language”(仍是一种标记语言)。YAML 的语法和其他高级语言类似,并且可以简单表达清单、散列表,标量等数据形态。
YAML 的配置文件后缀为
.yml
,如:runoob.yml 。
基本语法
- 大小写敏感
- 使用缩进表示层级关系
- 缩进不允许使用tab,只允许空格
- 缩进的空格数不重要,只要相同层级的元素左对齐即可
- '#'表示注释
数据类型
YAML 支持以下几种数据类型:
- 对象:键值对的集合,又称为映射(mapping)/ 哈希(hashes) / 字典(dictionary)
- 数组:一组按次序排列的值,又称为序列(sequence) / 列表(list)
- 纯量(scalars):单个的、不可再分的值
YAML 对象
对象键值对使用冒号结构表示 key: value,冒号后面要加一个空格。也可以使用
key:{key1: value1, key2: value2, ...}
。还可以使用缩进表示层级关系,如下
key:
child-key: value
child-key2: value2
...
较为复杂的对象格式,可以使用问号加一个空格代表一个复杂的 key,配合一个冒号加一个空格代表一个 value:
?- complexkey1
- complexkey2
:- complexvalue1
- complexvalue2
意思即对象的属性是一个数组
[complexkey1,complexkey2]
,对应的值也是一个数
[complexvalue1,complexvalue2]
YAML 数组
以
-
开头的行表示构成一个数组:
-A-B-C
YAML 支持多维数组,可以使用行内表示:
key: [value1, value2, ...]
数据结构的子成员是一个数组,则可以在该项下面缩进一个空格。
--A-B-C
一个相对复杂的例子:
companies:-id:1name: company1
price: 200W
-id:2name: company2
price: 500W
意思是 companies 属性是一个数组,每一个数组元素又是由 id、name、price 三个属性构成。
数组也可以使用流式(flow)的方式表示:
companies: [{id: 1,name: company1,price: 200W},{id: 2,name: company2,price: 500W}]
复合结构
数组和对象可以构成复合结构,例:
languages:- Ruby
- Perl
- Python
websites:YAML: yaml.org
Ruby: ruby-lang.org
Python: python.org
Perl: use.perl.org
转换为 json 为:
{languages:['Ruby','Perl','Python'],websites:{YAML:'yaml.org',Ruby:'ruby-lang.org',Python:'python.org',Perl:'use.perl.org'}}
纯量
纯量是最基本的,不可再分的值,包括:
- 字符串
- 布尔值
- 整数
- 浮点数
- Null
- 时间
- 日期
使用一个例子来快速了解纯量的基本使用:
boolean:-TRUE #true,True都可以
-FALSE #false,False都可以
float:-3.14-6.8523015e+5 #可以使用科学计数法
int:-123-0b1010_0111_0100_1010_1110 #二进制表示
null:nodeName:'node'parent:~ #使用~表示nullstring:- 哈哈
-'Hello world' #可以使用双引号或者单引号包裹特殊字符
- newline
newline2 #字符串可以拆成多行,每一行会被转化成一个空格
date:-2018-02-17 #日期必须使用ISO8601格式,即yyyy-MM-dd
datetime:-2018-02-17T15:02:31+08:00 #时间使用ISO8601格式,时间和日期之间使用T连接,最后使用+代表时区
引用
&
锚点和
*
别名,可以用来引用:
defaults:&defaults
adapter: postgres
host: localhost
development:database: myapp_development
<<:*defaults
test:database: myapp_test
<<:*defaults
相当于:
defaults:adapter: postgres
host: localhost
development:database: myapp_development
adapter: postgres
host: localhost
test:database: myapp_test
adapter: postgres
host: localhost
&
用来建立锚点(defaults),
<<
表示合并到当前数据,
*
用来引用锚点。
下面是另一个例子:
-&showell Steve
- Clark
- Brian
- Oren
-*showell
转为 JavaScript 代码如下:
['Steve','Clark','Brian','Oren','Steve']
Open5GS 初始化过程
在Open5GS中,每个NF都是一个守护进程,所以每个NF Instance都可以单独启动/关闭,下面是官方教程中给出的参考命令:
$ sudo systemctl restart open5gs-mmed
$ sudo systemctl restart open5gs-sgwcd
$ sudo systemctl restart open5gs-smfd
$ sudo systemctl restart open5gs-amfd
$ sudo systemctl restart open5gs-sgwud
$ sudo systemctl restart open5gs-upfd
$ sudo systemctl restart open5gs-hssd
$ sudo systemctl restart open5gs-pcrfd
$ sudo systemctl restart open5gs-nrfd
$ sudo systemctl restart open5gs-ausfd
$ sudo systemctl restart open5gs-udmd
$ sudo systemctl restart open5gs-pcfd
$ sudo systemctl restart open5gs-nssfd
$ sudo systemctl restart open5gs-bsfd
$ sudo systemctl restart open5gs-udrd
$ sudo systemctl restart open5gs-webui
Open5GS有一个通用的
main.c
文件,用于初始化容器环境,然后调用通用的接口
app_initialize()
来启动一个NF。下面是这个
main.c
的部分关键代码:
# ~/open5gs-main/open5gs-main/src/main.c
intmain(int argc,constchar*const argv[]){//程序入参解析......ogs_signal_init();ogs_setup_signal_thread();/*
* ogs_app_initialize() 初始化容器环境
* OPEN5GS_VERSION Open5GS版本号,在编译Open5GS时动态生成
* DEFAULT_CONFIG_FILENAME NF的默认配置文件路径
*/
rv =ogs_app_initialize(OPEN5GS_VERSION, DEFAULT_CONFIG_FILENAME, argv_out);if(rv != OGS_OK){if(rv == OGS_RETRY)return EXIT_SUCCESS;ogs_fatal("Open5GS initialization failed. Aborted");return OGS_ERROR;}/*
* app_initialize()创建NF守护进程实例
*/
rv =app_initialize(argv_out);if(rv != OGS_OK){if(rv == OGS_RETRY)return EXIT_SUCCESS;ogs_fatal("Open5GS initialization failed. Aborted");return OGS_ERROR;}atexit(terminate);ogs_signal_thread(check_signal);ogs_info("Open5GS daemon terminating...");return OGS_OK;}
OPEN5GS_VERSION 是由位于
~/open5gs-main/src/meson.build
在编译时生成的,具体代码如下:
...... version_conf.set_quoted('OPEN5GS_VERSION', package_version) configure_file(output : 'version.h', configuration : version_conf) ......
上面代码中提到的
app_initialize()
其实就是一个抽象的接口,每个NF必须实现这个接口,以完成NF的创建。每个NF都会有一个
app.c
文件,在这个文件中实现了接口函数
app_initialize()
,下面是AMF的例子:
/* ~/open5gs-main/src/amf/app.c */intapp_initialize(constchar*const argv[]){int rv;ogs_sctp_init(ogs_app()->usrsctp.udp_port);/*
* amf_initialize() AMF具体创建函数
*/
rv =amf_initialize();if(rv != OGS_OK){ogs_error("Failed to intialize AMF");return rv;}ogs_info("AMF initialize...done");return OGS_OK;}
下面是不同NF的
app.c
文件路径:
NF 启动过程(AMF为例)
我们上面介绍了容器环境的初始化以及NF创建接口函数的实现,下面以AMF实体为例子,介绍一下NF守护进程的创建过程。每个NF会有一个
init.c
文件,在这个文件中定义NF的初始化流程,代码如下:
/* ~/open5gs-main/src/amf/init.c *//*
* 初始化AMF实体
*/intamf_initialize(){int rv;amf_context_init();amf_event_init();ogs_sbi_context_init();// 创建AMF和NRF之间的SBI接口
rv =ogs_sbi_context_parse_config("amf","nrf");if(rv != OGS_OK)return rv;// 初始化AMF上下文
rv =amf_context_parse_config();if(rv != OGS_OK)return rv;
rv =amf_m_tmsi_pool_generate();if(rv != OGS_OK)return rv;
rv =ogs_log_config_domain(ogs_app()->logger.domain,ogs_app()->logger.level);if(rv != OGS_OK)return rv;
rv =amf_sbi_open();if(rv != OGS_OK)return rv;
rv =ngap_open();if(rv != OGS_OK)return rv;//初始化完成,启动AMF程序(创建了一个守护进程)
thread =ogs_thread_create(amf_main,NULL);if(!thread)return OGS_ERROR;
initialized =1;return OGS_OK;}/*
* AMF的守护进程,这部分代码每个NF基本都是相似的,唯一不同的是 FSM 的初始化参数
*/staticvoidamf_main(void*data){ogs_fsm_t amf_sm;int rv;// AMF 有限状态机初始化ogs_fsm_create(&amf_sm, amf_state_initial, amf_state_final);ogs_fsm_init(&amf_sm,0);// polling 等待消息for(;;){// 等待消息ogs_pollset_poll(ogs_app()->pollset,ogs_timer_mgr_next(ogs_app()->timer_mgr));/*
* After ogs_pollset_poll(), ogs_timer_mgr_expire() must be called.
*
* The reason is why ogs_timer_mgr_next() can get the corrent value
* when ogs_timer_stop() is called internally in ogs_timer_mgr_expire().
*
* You should not use event-queue before ogs_timer_mgr_expire().
* In this case, ogs_timer_mgr_expire() does not work
* because 'if rv == OGS_DONE' statement is exiting and
* not calling ogs_timer_mgr_expire().
*/// 处理超时定时器ogs_timer_mgr_expire(ogs_app()->timer_mgr);for(;;){amf_event_t*e =NULL;//获取消息
rv =ogs_queue_trypop(ogs_app()->queue,(void**)&e);ogs_assert(rv != OGS_ERROR);if(rv == OGS_DONE)goto done;if(rv == OGS_RETRY)break;ogs_assert(e);//将消息送入FSM进行处理ogs_fsm_dispatch(&amf_sm, e);amf_event_free(e);}}
done:ogs_fsm_fini(&amf_sm,0);ogs_fsm_delete(&amf_sm);}
下面是每个NF的
init.c
文件路径:
Open5GS 定时器机制
Open5GS使用
红黑树
来管理定时器,因为
红黑树
的性质,即使在最差的情况下定时器的查找、插入、删除操作也能有一个较好的性能。
Open5GS中红黑树的实现code位于
~/open5gs-main/lib/core/ogs-rbtree.c
几个关键的定时器函数介绍
定时器函数名功能ogs_timer_add()创建一个定时器ogs_timer_delete()删除一个定时器ogs_timer_start()启动一个定时器ogs_timer_stop()停止定时器ogs_timer_mgr_next()获取下一个即将超时的定时器的剩余时间,如果没有定时器在运行,则返回 INFINITEogs_timer_mgr_expire()处理超时的定时器,执行超时定时器的回调函数
定时器的时间从哪里来?
这里使用的是系统时间,是由
I\O多路复用技术
来帮我们记录定时器运行时间的,还记得前面介绍的AMF实体的
amf_main()
函数吗?代码片段如下:
staticvoidamf_main(void*data){ogs_fsm_t amf_sm;int rv;// AMF 有限状态机初始化ogs_fsm_create(&amf_sm, amf_state_initial, amf_state_final);ogs_fsm_init(&amf_sm,0);// polling 等待消息for(;;){// 等待消息ogs_pollset_poll(ogs_app()->pollset,ogs_timer_mgr_next(ogs_app()->timer_mgr));......}
done:ogs_fsm_fini(&amf_sm,0);ogs_fsm_delete(&amf_sm);}
ogs_timer_mgr_next()
函数获取即将超时的定时器的剩余超时时间,并作为参数传入
ogs_pollset_poll()
函数,这个函数就是我们上面提及的
I\O多路复用技术
的实现,在Open5GS中可以选择使用
select
、
epoll
、
kqueue
这三种
I\O多路复用技术
,但是我们一般都是以
epoll
技术,因为
epoll
相比其它的
I\O多路复用技术
性能更好。下面是
epoll
函数针对
ogs_pollset_poll()
的具体实现过程(详细内容会在 消息通知机制 中介绍):
staticintepoll_process(ogs_pollset_t*pollset,ogs_time_t timeout){structepoll_context_s*context =NULL;int num_of_poll;int i;ogs_assert(pollset);
context = pollset->context;ogs_assert(context);
num_of_poll =epoll_wait(context->epfd, context->event_list,
pollset->capacity,
timeout == OGS_INFINITE_TIME ? OGS_INFINITE_TIME :ogs_time_to_msec(timeout));//省略了与定时器无关的code......}return OGS_OK;}
epoll_wait()
函数的原型如下
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
,调用
epoll_wait()
函数之后程序会被阻塞,直到遇到下面的情况函数才会返回:
- 触发了感兴趣的事件通知;
- 阻塞
timeout
毫秒; - 被
signal
中断。
如果函数返回并不是因为超时,那么在轮询下一次事件通知时会重新计算即将超时的定时器的剩余时间,并不会影响定时准确度。
这里有个问题,就是
epoll_wait()
只能提供毫秒级的定时,同时时钟精度还取决于Linux Kernel是否开启高精度定时,时钟频率的大小以及CPU工作负载。不过对于核心网来说毫秒级的精度绝对是足够了。
Open5GS 消息通知机制
Open5GS中NFs之间通过socket通信,具体使用什么通信协议进行通信,我们这里不讨论,但是无论使用什么通信协议,下面的机制都是适用的。
在初始化容器环境的时候,我们会初始化Open5GS的消息通知机制,代码如下:
// ~/open5gs-main/src/main.cintmain(int argc,constchar*const argv[]){......// 初始化容器环境
rv =ogs_app_initialize(OPEN5GS_VERSION, DEFAULT_CONFIG_FILENAME, argv_out);if(rv != OGS_OK){if(rv == OGS_RETRY)return EXIT_SUCCESS;ogs_fatal("Open5GS initialization failed. Aborted");return OGS_ERROR;}......return OGS_OK;}// ~/open5gs-main/lib/app/ogs-init.cintogs_app_initialize(constchar*version,constchar*default_config,constchar*const argv[]){....../**************************************************************************
* Stage 7 : Queue, Timer and Poll
*/// 初始化消息队列ogs_app()->queue =ogs_queue_create(ogs_app()->pool.event);ogs_assert(ogs_app()->queue);ogs_app()->timer_mgr =ogs_timer_mgr_create(ogs_app()->pool.timer);ogs_assert(ogs_app()->timer_mgr);//初始化消息通知机制ogs_app()->pollset =ogs_pollset_create(ogs_app()->pool.socket);ogs_assert(ogs_app()->pollset);return rv;}
socket消息的收发
在
ogs_app()->pollset = ogs_pollset_create(ogs_app()->pool.socket);
这条语句中初始化了消息通知机制,消息通知功能的入口文件是
~/open5gs-main/lib/core/ogs-poll.c
。我们之前已经介绍了Open5GS的消息通知机制有多种选择,包括:
- select模式;
- epoll模式;
- kqueue模式。
这三种机制在编译时通过宏来控制具体使用哪一种,代码如下:
// ~/open5gs-main/lib/core/ogs-poll.cogs_pollset_t*ogs_pollset_create(unsignedint capacity){ogs_pollset_t*pollset =ogs_calloc(1,sizeof*pollset);ogs_expect_or_return_val(pollset,NULL);
pollset->capacity = capacity;ogs_pool_init(&pollset->pool, capacity);if(ogs_pollset_actions_initialized == false){#ifdefined(HAVE_KQUEUE)
ogs_pollset_actions = ogs_kqueue_actions;/* kqueue 模式 */#elifdefined(HAVE_EPOLL)
ogs_pollset_actions = ogs_epoll_actions;/* epoll 模式 */#else
ogs_pollset_actions = ogs_select_actions;/* select 模式 */#endif
ogs_pollset_actions_initialized = true;}
ogs_pollset_actions.init(pollset);return pollset;}
如果需要监听某个socket是否收到数据或者是否可以发送数据时,可以通过
ogs_pollset_add()
函数将这个socket注册到内核的监听队列。当某个NF给这个socket发送了数据,此时消息通知机制就会触发回调函数去接收这个消息,在消息回调函数中如果需要将这个消息传递给FSM,则通过
ogs_queue_push()
函数将消息放入FSM消息队列中等待FSM去处理。在FSM中可能需要发送消息,可以通过
ogs_pollset_add
函数向监听队列注册一个
write事件
来发送消息。下面举一个例子,不同的NF发送消息的函数可能不一样,但是大致流程是一样的,首先向
write_queue
写入需要发送的数据,然后使用
ogs_pollset_add()
函数注册
write事件
。
// ~/open5gs-main/lib/sbi/nghttp2-server.c:1259staticvoidsession_write_to_buffer(ogs_sbi_session_t*sbi_sess,ogs_pkbuf_t*pkbuf){ogs_sock_t*sock =NULL;ogs_socket_t fd = INVALID_SOCKET;ogs_assert(pkbuf);ogs_assert(sbi_sess);
sock = sbi_sess->sock;ogs_assert(sock);
fd = sock->fd;ogs_assert(fd != INVALID_SOCKET);/* 将需要发送的消息放入 write_queue list中 */ogs_list_add(&sbi_sess->write_queue, pkbuf);/* 如果之前没有注册过write事件,则注册write事件 */if(!sbi_sess->poll.write){
sbi_sess->poll.write =ogs_pollset_add(ogs_app()->pollset,
OGS_POLLOUT, fd, session_write_callback, sbi_sess);ogs_assert(sbi_sess->poll.write);}}// ~/open5gs-main/lib/sbi/nghttp2-server.c:1235/*
* write 对应的回调函数,如果消息通知机制发现现在可以无阻塞的发送数据,
* 则会触发此回调函数,进而将数据发送出去
*/staticvoidsession_write_callback(short when,ogs_socket_t fd,void*data){ogs_sbi_session_t*sbi_sess = data;ogs_pkbuf_t*pkbuf =NULL;ogs_assert(sbi_sess);if(ogs_list_empty(&sbi_sess->write_queue)== true){ogs_assert(sbi_sess->poll.write);ogs_pollset_remove(sbi_sess->poll.write);
sbi_sess->poll.write =NULL;return;}
pkbuf =ogs_list_first(&sbi_sess->write_queue);ogs_assert(pkbuf);ogs_list_remove(&sbi_sess->write_queue, pkbuf);ogs_send(fd, pkbuf->data, pkbuf->len,0);ogs_log_hexdump(OGS_LOG_DEBUG, pkbuf->data, pkbuf->len);ogs_pkbuf_free(pkbuf);}
ogs_poll_t *ogs_pollset_add(ogs_pollset_t *pollset, short when, ogs_socket_t fd, ogs_poll_handler_f handler, void *data)
函数的参数
when
定义了感兴趣的事件类型,目前Open5GS的实现中只支持两种事件:
- OGS_POLLIN:表示监听socket 读事件,就是监听其它NFs是不是给我发数据了;
- OGS_POLLOUT:监听socket是否可以非阻塞的发送数据。
queue消息的收发
ogs_app()->queue = ogs_queue_create(ogs_app()->pool.event);
中初始化了
消息队列
,
消息队列
的实现位于
~/open5gs-main/lib/core/ogs-queue.c
文件中。下面介绍一个关键的函数:
函数名功能ogs_queue_push()向queue中插入一个消息,会阻塞ogs_queue_trypush()尝试向queue中插入一个消息,不会阻塞ogs_queue_timedpush()向queue中插入一个消息,会阻塞一段时间,这个时间由用户定义ogs_queue_pop获取一个消息,如果没有消息会阻塞ogs_queue_trypop获取一个消息,如果没有消息不会阻塞ogs_queue_timedpop获取一个消息,如果没有消息会阻塞一段时间,这个时间由用户定义
Overview消息通知机制
Open5GS 内存管理
Open5GS 在内存管理这边使用了内存池技术,在实际的内存管理中有两种方式(使用宏
OGS_USE_TALLOC
控制):
- 使用开源的
libtalloc
库,相关的文件~/open5gs-main/lib/core/ogs-memory.c
,官网链接; - 使用
ogs_pool
实现的libpkbuf
,相关的文件~/open5gs-main/lib/core/ogs-pool.h
、~/open5gs-main/lib/core/ogs-pkbuf.c
。
下面是具体的内存分配函数:
// ~/open5gs-main/lib/core/ogs-memory.h#ifOGS_USE_TALLOC/*****************************************
* Memory Pool - Use talloc library
*****************************************/#defineogs_malloc(size)\ogs_talloc_size(__ogs_talloc_core, size, __location__)#defineogs_calloc(nmemb, size)\ogs_talloc_zero_size(__ogs_talloc_core,(nmemb)*(size), __location__)#defineogs_realloc(oldptr, size)\ogs_talloc_realloc_size(__ogs_talloc_core, oldptr, size, __location__)#defineogs_free(ptr)ogs_talloc_free(ptr, __location__)#else/*****************************************
* Memory Pool - Use pkbuf library
*****************************************/#defineogs_malloc(size)ogs_malloc_debug(size, OGS_FILE_LINE)#defineogs_calloc(nmemb, size)ogs_calloc_debug(nmemb, size, OGS_FILE_LINE)#defineogs_realloc(ptr, size)ogs_realloc_debug(ptr, size, OGS_FILE_LINE)#defineogs_free(ptr)ogs_free_debug(ptr)#endif
上述函数与C标准库中的函数对照表:
Open5GS内存函数C标准库函数ogs_malloc()malloc()ogs_calloc()calloc()ogs_realloc()realloc()ogs_free()free()
我们这里再提及一下Open5GS的
ogs_pool
机制,Open5GS code中经常能看到
ogs_pool_init()
函数的调用,用来预分配一些实例池,在每次实例化一个对象时首先调用
ogs_pool_alloc()
函数从实例池中获取一个已经预分配好内存的实例对象,如果实例池资源已经耗尽,则返回
NULL
,代码如下:
// ~/open5gs-main/lib/core/ogs-pool.h#defineogs_pool_alloc(pool, node)do{\*(node)=NULL;\if((pool)->avail >0){\(pool)->avail--;\*(node)=(void*)(pool)->free[(pool)->head];\(pool)->free[(pool)->head]=NULL;\(pool)->head =((pool)->head +1)%((pool)->size);\(pool)->index[ogs_pool_index(pool,*(node))-1]=*(node);\}\}while(0)
通过这样的方式,不仅预分配了内存,还可以限制内存资源的消耗(
ogs_pool_init()
在初始化一个池的时候已经限制了可以同时实例化的对象上限)。
还需要特别说明的,Open5GS重写了STD C中的一些函数,例如
strdup()
、
strndup()
等,相关的定义在
~/open5gs-main/lib/core/ogs-strings.c
和
~/open5gs-main/lib/core/ogs-strings.h
中,进行二次开发时一定要使用这些重写的函数以保证内存管理的一致性。
Open5GS重写STD Cogs_strdup()strdup()ogs_strndup()strndup()
Open5GS 日志系统
Open5GS日志功能相对比较简陋,没有单独的线程对日志进行处理,所有日志的处理都在业务线程中,日志相关的文件是
~/open5gs-main/lib/core/ogs-log.c
和
~/open5gs-main/lib/core/ogs-log.h
。
Open5GS默认会通过
stderr(标准错误输出)
将日志输出,即使通过运行时参数配置了将日志输出到文件,Open5GS也会将日志输出到
stderr
,如果只想让日志输出到文件,必须修改下面的code:
// ~/open5gs-main/lib/core/ogs-log.cvoidogs_log_init(void){ogs_pool_init(&log_pool,ogs_core()->log.pool);ogs_pool_init(&domain_pool,ogs_core()->log.domain_pool);ogs_log_add_domain("core",ogs_core()->log.level);// ogs_log_add_stderr(); 注释掉这段code,不创建 err log}voidogs_log_vprintf(ogs_log_level_e level,int id,ogs_err_t err,constchar*file,int line,constchar*func,int content_only,constchar*format, va_list ap){ogs_log_t*log =NULL;ogs_log_domain_t*domain =NULL;char logstr[OGS_HUGE_LEN];char*p,*last;//int wrote_stderr = 0;int wrote_stderr =1;// 修改wrote_stderr为 1......}
下面介绍一下日志系统的运行时参数:
-l
:设置日志文件名。日志在文件中会滚动存储;-e
:设置日志输出级别,默认的日志级别为OGS_LOG_INFO
,默认日志级别设置的code位置在~/open5gs-main/lib/core/ogs-core.c:28
支持的日志级别相关函数含义OGS_LOG_FATALogs_fatal()严重的错误事件将会导致应用程序的退出OGS_LOG_ERRORogs_error()虽然发生错误事件,但不影响系统的继续运行OGS_LOG_WARNogs_warn()表明出现潜在错误OGS_LOG_INFOogs_info()常规的事件,表明软件运行正常OGS_LOG_DEBUGogs_debug()输出调试信息OGS_LOG_TRACEogs_trace()比 OGS_LOG_DEBUG 打印更多调试信息-m
:设置特定日志模块的日志级别,如果没有-m
参数只有-e
参数,则表示将所有的日志级别都设置为-e
参数所指定的值。在传递-m
参数时如果有多个模块需要使用\t\n
分隔;
Open5GS的日志输出默认包含
输出时间
、
日志域
、
日志级别
、
具体日志内容
、
输出日志对应的文件名
、
输出日志对应的行号
、
输出日志对应的函数
。如果不需要上面的这些辅助信息可以调用
ogs_log_print()
输出日志。
Open5GS日志系统中
ogs_log_message()
函数在输出socket日志时比较有用,通过输入socket错误number,这个函数会自动将错误号翻译成对应的错误字符串,方便我们定位问题。
代码中对每条日志的长度做了限制,最长为
8192
,通过宏
OGS_HUGE_LEN
控制,相关宏位于
~/open5gs-main/lib/core/ogs-strings.h
。
这里吐槽一下Open5GS的日志系统,首先就是没有一个专门的日志线程来处理日志消息,所有的日志消息都在工作线程处理,如果有太多的日志输出可能会影响业务线程的性能。其次,就是日志的输出级别控制粒度太粗,不能通过运行时参数给多个模块设置不同的日志级别,而且没有运行时参数来关闭控制台窗口的日志输出。
Open5GS 有限状态机(FSM)介绍
上面已经多次提及Open5GS的FSM机制,每个NF都有自己独特的与具体业务相关的一套FSM机制,同时每个NF都有一个
上下文(context)
,FSM就是根据当前的上下文状态以及收到的消息来决策下一步怎么执行,如下图:
Open5GS中文件名后缀为
xxx_sm.c
的文件就是状态机文件,其中“xxx”表示具体的实例,例如“amf”,“upf”等。如果是amf_sm.c则表示这是一个amf实例的状态机。(sm 即 state machine)
每个NF的context文件:
下面我们来看一下每个NF的状态机是如何注册以及如何运行的。相关的函数位于
~/open5gs-main/lib/core/ogs-fsm.c
、
~/open5gs-main/lib/core/ogs-fsm.h
中,在创建NF守护进程时对FSM进行注册和初始化,下面是AMF状态机初始化相关的一段code:
// ~/open5gs-main/src/amf/init.cstaticvoidamf_main(void*data){ogs_fsm_t amf_sm;int rv;/*
* amf_state_initial 是AMF状态机初始化回调函数;
* amf_state_final 是AMF状态机终止回调函数;
*
* ogs_fsm_create() 用于注册AMF状态机;
* ogs_fsm_init() 执行AMF状态机的初始化操作;
*/ogs_fsm_create(&amf_sm, amf_state_initial, amf_state_final);ogs_fsm_init(&amf_sm,0);......}
上面的code中我们看到了FSM的初始化和终止相关的回调函数,那具体执行业务流程的回调函数在哪里,我们先看一下
ogs_fsm_t
结构体以及
ogs_fsm_create()
函数:
// ~/open5gs-main/lib/core/ogs-fsm.htypedefstruct_ogs_fsm_t{ogs_fsm_handler_t init;// FSM 初始化相关的回调函数ogs_fsm_handler_t fini;// FSM 终止相关的回调函数ogs_fsm_handler_t state;// FSM 业务执行相关的回调函数}ogs_fsm_t;#defineogs_fsm_create(__s, __i, __f)\(((__s)->init =(__s)->state =(ogs_fsm_handler_t)(__i)),\(__s)->fini =(ogs_fsm_handler_t)(__f))
可以看到在创建FSM时,将初始化回调函数赋值给了
init
和
state
对象,之后执行FSM的初始化程序
ogs_fsm_init()
,代码如下:
// ~/open5gs-main/lib/core/ogs-fsm.cvoidogs_fsm_init(void*sm,void*event){ogs_fsm_t*s = sm;fsm_event_t*e = event;if(s->init !=NULL){(*s->init)(s, e);if(s->init != s->state){if(e){
e->id = OGS_FSM_ENTRY_SIG;(*s->state)(s, e);}else{(*s->state)(s,&entry_event);}}}}
可以看到这里首先执行了FSM的初始化函数,然后判断
s->init != s->state
条件是否成立,如果成立则执行
state
回调函数。其实在执行FSM初始化回调函数时会改变
state
函数指针的值,使其指向真正处理业务的回调函数,amf FSM初始化代码如下:
// ~/open5gs-main/src/amf/amf-sm.cvoidamf_state_initial(ogs_fsm_t*s,amf_event_t*e){amf_sm_debug(e);ogs_assert(s);OGS_FSM_TRAN(s,&amf_state_operational);}// ~/open5gs-main/lib/core/ogs-fsm.h#defineOGS_FSM_TRAN(__s, __target)\((ogs_fsm_t*)__s)->state =(ogs_fsm_handler_t)(__target)
可以看到
amf_state_operational
才是amf FSM真正执行业务的回调函数,通过
OGS_FSM_TRAN()
将其赋值给
state
函数指针。初始化回调函数执行完成之后,此时
s->init != s->state
条件成立,然后继续执行
state
回调函数,将状态机的执行状态切换为
OGS_FSM_ENTRY
(通过发送
OGS_FSM_ENTRY_SIG
事件),至此整个状态机的注册和初始化流程完成。
Open5GS的状态机是消息驱动的,当
消息队列
收到消息后会传递给FSM进一步处理消息,如下是 amf FSM code:
// ~/open5gs-main/src/amf/init.cstaticvoidamf_main(void*data){ogs_fsm_t amf_sm;int rv;ogs_fsm_create(&amf_sm, amf_state_initial, amf_state_final);ogs_fsm_init(&amf_sm,0);for(;;){ogs_pollset_poll(ogs_app()->pollset,ogs_timer_mgr_next(ogs_app()->timer_mgr));/*
* After ogs_pollset_poll(), ogs_timer_mgr_expire() must be called.
*
* The reason is why ogs_timer_mgr_next() can get the corrent value
* when ogs_timer_stop() is called internally in ogs_timer_mgr_expire().
*
* You should not use event-queue before ogs_timer_mgr_expire().
* In this case, ogs_timer_mgr_expire() does not work
* because 'if rv == OGS_DONE' statement is exiting and
* not calling ogs_timer_mgr_expire().
*/ogs_timer_mgr_expire(ogs_app()->timer_mgr);for(;;){amf_event_t*e =NULL;
rv =ogs_queue_trypop(ogs_app()->queue,(void**)&e);ogs_assert(rv != OGS_ERROR);if(rv == OGS_DONE)goto done;if(rv == OGS_RETRY)break;ogs_assert(e);ogs_fsm_dispatch(&amf_sm, e);amf_event_free(e);}}
done:ogs_fsm_fini(&amf_sm,0);ogs_fsm_delete(&amf_sm);}
可以看到通过
ogs_queue_trypop()
函数将消息从队列中取出后,做为参数传递给
ogs_fsm_dispatch()
函数,进入FSM中对消息进行进一步处理。
OGS_DONE:当守护进程退出时,会发送这个消息,进而终止FSM;
OGS_RETRY:当队列为空且ogs_queue_trypop()为非阻塞模式时返回此值;
为什么进入这一步之后队列还有可能为空?因为I/O多路复用技术,可能会被定时器、中断等唤醒,并不一定总是被socket消息唤醒。
在NF初始化时还会初始化
context
,包括初始化与这个NF相关的日志、实例池、
hash
列表、SBI实例、特定业务相关的状态变量等。下面是amf
context
的初始化code:
// ~/open5gs-main/src/amf/init.cintamf_initialize(){int rv;//一些 context 的初始化amf_context_init();amf_event_init();ogs_sbi_context_init();// 解析SBI相关的yaml配置文件参数
rv =ogs_sbi_context_parse_config("amf","nrf");if(rv != OGS_OK)return rv;// 解析amf相关的yaml配置文件参数
rv =amf_context_parse_config();if(rv != OGS_OK)return rv;
rv =amf_m_tmsi_pool_generate();if(rv != OGS_OK)return rv;
rv =ogs_log_config_domain(ogs_app()->logger.domain,ogs_app()->logger.level);if(rv != OGS_OK)return rv;
rv =amf_sbi_open();if(rv != OGS_OK)return rv;
rv =ngap_open();if(rv != OGS_OK)return rv;
thread =ogs_thread_create(amf_main,NULL);if(!thread)return OGS_ERROR;
initialized =1;return OGS_OK;}
Open5GS SBI介绍
5G核心网控制平面最突出的变化是将基于服务的接口(Service based Interface, SBI)或基于服务的架构(Service based Architecture, SBA)引入到传统的点对点网络架构中。有了这个新的变化,除了一些接口,如N2和N4,几乎每个接口现在都被定义为使用统一的接口,即使用HTTP/2协议。
这一变化使NFs之间的通信类似于一个服务网格函数,而不是串行连接,这有助于减少每个接口之间的依赖性,并有助于每个函数的独立扩展。因此,增加了跨网络功能服务和新功能扩展的灵活性。
- 基于服务的架构(SBA)是由一组网络功能(NFs)组成的;
- NFs向其他NFs 通过SBI提供服务;
- 参考点接口被一条连接所有NFs的公共总线取代。
SBA下的服务注册,发现和请求流程:
- 首先生产者会向NRF注册自己的服务,流程(1);
- 当消费者需要某一个服务的时候,首先向NRF发送服务发现请求,流程(2);
- 获取到相关服务的位置后,直接向提供服务的NF发起服务请求消息,流程(3)。
下面是SBI的协议栈结构:
- 基于服务的接口采用HTTP/2作为应用层协议;
- 采用TCP或者QUIC(Quick UDP Internet Connections)作为传输层协议;
- 采用JSON作为序列化协议;
关于SBA的介绍可以参考我的博客《【5G架构】5G 核心网——基于服务的网络架构》
Open5GS中关于
sbi
的code位于
~/open5gs-main/lib/sbi/
目录中,它包含了
server.c
、
client.c
相关的code,当NF作为服务提供者时,它就是server;当作为服务请求者时,其又是client。所以在初始化一个NF实例的时候会同时创建一个或多个server对象(一般每个NF会提供多种服务,这里有点
微服务
的意思),为其它NFs提供服务;同时也会至少创建一个client对象用于向NRF注册。
上面已经介绍了
sbi
的协议栈,在应用层使用了
http/2
协议。在Open5GS中server和client的实现有所不同,server这边使用了开源的
libnghttp2
库(官方网址),相关的实现文件
~/open5gs-main/lib/sbi/nghttp2-server.c
;而client这边则使用了一个开源的
libcurl
库(官方网址)这个库实现了很多应用层协议(包括http/2),通过这个库可以将客户端的非HTTP/2协议转换成能够被server处理的HTTP/2协议。
以下是 Curl 官网的一段介绍:
libcurl is a free and easy-to-use client-side URL transfer library, supporting DICT, FILE, FTP, FTPS, GOPHER, GOPHERS, HTTP, HTTPS, IMAP, IMAPS, LDAP, LDAPS, MQTT, POP3, POP3S, RTMP, RTMPS, RTSP, SCP, SFTP, SMB, SMBS, SMTP, SMTPS, TELNET and TFTP. libcurl supports SSL certificates, HTTP POST, HTTP PUT, FTP uploading, HTTP form based upload, proxies, HTTP/2, HTTP/3, cookies, user+password authentication (Basic, Digest, NTLM, Negotiate, Kerberos), file transfer resume, http proxy tunneling and more!
因为不同NFs之间交互的
sbi
消息可能有所不同或者对消息的处理方式不同,所以每个NFs都定义了一些与自己相关的sbi消息流程,如下图:
在
sbi-path.c
中有两个回调函数比较重要,所有使用
sbi
的NFs必须实现这两个回调函数,分别是
server_cb()
和
client_cb()
。当NF收到来自其它NFs的服务请求后,会触发
server_cb()
进行处理;当其作于client向其它NFs请求服务时会触发
client_cb()
进行处理。
其实
sbi
通信中还涉及到一个知识点就是
RESTful API
,感兴趣的同学可以参考我下面给出的两个链接:
《What is REST API (RESTful API) 》
《赏心悦目的RESTful API这样来设计!》
附录
部分NF功能简介
NF简介5G Equipment Identity Register (EIR)5G-EIR维护了一份黑名单上的永久设备标识,并在AMF要求时提供该信息。Access and Mobility Management Function (AMF)AMF通过N2接口与NG RAN和N3IWF控制平面连接,通过N1接口与UE NAS连接。它还提供了核心网络功能(如SMS、PCF、NEF或位置管理)与终端或接入网之间的消息传输。此外,AMF通常负责访问认证和授权,同时管理UE注册、连接、可达性、移动性,以及执行与访问和移动性相关的策略。源amf和目标amf通过N14接口相互通信。Application Function (AF)AF为用户提供应用服务。它可以与PCF交互,以便影响流量路由,通知PCF应用程序带宽要求或使用阈值,或注册PDU会话事件。如果一个AF是可信的,它可以直接与PCF交互;否则它必须与NEF交互。Authentication Server Function (AUSF)AUSF的唯一职责是验证通过3GPP网络接入和通过不可信、非3GPP wlan接入的UE。Binding Support Function (BSF)BSF为特定的PDU会话存储ue、数据网络和服务pcf之间的关联。因此,任何NF都可以快速检索与PDU会话相关联的PCF。外部AF可以通过NEF检索绑定。PCF按照会话事件的指示注册、更新和删除存储的绑定。BSF可以是独立的,也可以与其他网络功能(如PCF、UDR、NRF、SMF)搭配使用。Cell Broadcast Center Function (CBCF)广播消息,例如来自公共警告系统的广播消息,通过使用Namf_Communication服务CBCF将消息传递到接入网,并最终传递到终端。Charging Function (CHF)与4G网络中在线和离线付费分开的功能不同,5G引入了融合付费系统CCS (Converged charge System)。CHF是CCS和5GC之间的接口。它向PCF和SMF提供服务的支出限制和配额,并从SMF收集使用信息。Gateway Mobile Location Center (GMLC)GMLC通过Le接口(上面没有显示)与外部位置服务(LCS)客户端交互,向他们提供指定终端的当前位置。如果GMLC实现了位置检索功能(LRF),它将从UDM获取该UE的服务AMF的身份,然后从该AMF获得UE的位置。LRF也可以是一个独立的节点,向多个gmlc提供服务。gNodeB (gNB)从NG RAN访问5GC是通过gNodeB建立的。通过N1/N2接口和AMF连接、通过N3接口和UPF连接。此外,当UE无法提供所需信息时,它还负责选择AMF。Home Subscriber Server (HSS)一个 RESTful HSS为IMS和EPC-5GC之间的互操作提供签约用户的数据管理、终端鉴权和终端上下文管理服务。Location Management Function (LMF)LMF使用来自UE和/或NG RAN的信息确定UE的当前位置,并根据请求提供该位置。Network Data Analytics Function (NWDAF)负责收集和分析当前的网络状态,并提供给订阅的NFs。例如,NSSF可以在切片选择时考虑网络负载信息,PCF也可以在制定动态策略规则时使用该信息。NWDAF可以提供网络切片级的网络负载信息。Network Exposure Function (NEF)NEF向5GC中的其他网络功能公开网络功能和事件,还为外部应用程序功能提供了与PCF等核心网络功能交互的安全方法。5GC中的任何NF都可以与NEF相互作用。在处理外部实体时,NEF可以屏蔽敏感的网络和用户信息。此外,NEF可以管理包流描述(作为PFDF运行),并根据请求向SMF提供pfd或将pfd推送给SMF作为管理功能的一部分。Network Repository Function (NRF)5G通过NRF扩展了服务发现的概念。任何网络功能都可以查询NRF,以获得提供特定服务的其他网络功能的身份和位置。一个网络中可以部署多个nrf,它们可以被部署在不同的级别——提供关于整个网络、一组网络片或一个网络片实例的信息。nrf之间通过N27接口进行通信。Network Slice Selection Function (NSSF)5GC中最值得注意的创新之一是引入了网络切片—— 一组逻辑上分离的NFs,提供完整的PLMN服务。NSSF选择将服务于一个UE的网络切片实例集。选择过程由订阅数据和网络负载信息通知。在动态网络中,NSSF可以通过查询NRF来发现服务生产者。h-nssf和 v-nssf通过N31接口进行通信。Network Slice Specific Authentication and Authorization Function (NSSAAF)NSSAAF在使用AAA服务器的网络切片上提供认证和授权服务。NSSAAF向AAA转发EAP消息,并向服务的AMF提供需要对终端进行重新认证和授权或撤销 E 授权的通知。Non-3GPP Interworking Function (N3IWF)N3IWF提供对不受信任的、非3gpp wlan的5GC的访问。与终端建立IPSec隧道,与5GC通过N1/N2和N3连接。与gNodeB类似,N3IWF在UE和AMF之间中继N1/N2信令,以及在UE和UPF之间中继用户平面数据包。Policy Control Function (PCF)PCF主要确定哪些规则将控制UE会话管理和用户平面流量,包括授权的QoS、网关规则和流量转发控制。AMF利用接入和移动性规则。SMF使用会话管理规则。UPF通过SMF接收业务数据流和PDU会话的规则。pcf通过amf将终端数据的路由规则发送给ue。PCF做出的决策可能基于从UDR获得的订阅信息,以及来自NWDAF的当前网络切片的负载信息,以及可能来自CHF的在线收费信息。
如果一个AF为会话指定了一个使用率阈值,无论它是IMS中的P-CSCF还是外部AF, PCF都可以在SMF上调用使用率监控,并在达到阈值时通知AF。AF还可以从PCF注册会话事件通知,或者请求PCF报告接入网络信息(当支持Rx时)。当AF是一个外部实体时,它通过NEF与PCF相互作用。h-pcf 和 v-pcf 通过N24接口相互通信。Session Management Function (SMF)SMF主要负责管理UE的PDU会话。它的职责包括建立、修改和释放PDU会话,以及维护UPF和gNodeB或N3IWF之间的隧道(N3)。SMF将选择一个合适的UPF来处理会话的用户面业务,并配置UPF数据流转发规则。它还可以处理终端IP地址分配和DHCP服务,以及确定终端会话的会话和服务连续性(SSC)模式。最后,在策略和收费方面,SMF执行与收费相关的策略决策,向UPF推送关于流量处理和报告的规则,并收集使用数据,然后报告给CHF。SMF之间通过N16接口(H-SMF到V-SMF)、N16a接口(SMF到I-SMF)或N38接口(I-SMF到I-SMF或V-SMF到V-SMF)进行通信。Short Message Service Function (SMSF)SMSF通过NAS管理短消息业务,与终端实现SM-RP/SM-CP。向短信路由器转发MO消息,向终端转发MT消息。它还与AMF就手机短信传输的可用性进行互动。UE Radio Capability Management Function (UCMF)各种应用程序的无线电能力要求都存储在UCMF中。需求由制造商分配的标识符(可能由AF/NEF提供)或plmn分配的标识符(UCMF负责分配)来标识。UCMF会管理UE Radio Capability Identifiers和UE Radio Access Capability Information之间的映射关系。Unified Data Management (UDM)UDM服务为各种网络功能提供订阅者、会话和订阅信息——用户标识符和身份验证凭证、服务NF标识符、访问授权。UDM还管理订阅数据,可以自己存储这些信息,也可以管理存储在UDR中的信息。Unified Data Repository (UDR)UDR为PLMN中的结构化数据提供存储和检索服务。UDR存储UDM和PCF使用的订阅数据、NEF使用的应用程序数据(包括pfd)以及NEF公开的数据。User Plane Function (UPF)为了与控制和用户平面分离(CUPS)体系结构保持一致,UPF只关心处理用户数据。包括路由、转发、下行报文缓冲、ARP和IPv6邻居请求代理以及报文检测。SMF通知UPF它必须执行的策略。UPF将收集到的流量使用数据报告给SMF。upf通过N9 / N19 (PSA upf)接口相互通信。
版权归原作者 从善若水 所有, 如有侵权,请联系我们删除。