app开发定制公司【5GC】开源5G核心网(Open5GS)架构详解

app开发定制公司博主未授权任何人或组app开发定制公司织机构转载博主任何原创文章,app开发定制公司感谢各位对原创的支持!

app开发定制公司本人就职于国际知名终端厂商,负责modem芯片研发。
app开发定制公司在早期负责终端数据业务层、app开发定制公司核心网相关的开发工作,目前牵头6Gapp开发定制公司算力网络技术标准研究。


app开发定制公司博客内容主要围绕:
       5G/6G协议讲解
       app开发定制公司算力网络讲解(云计算,边缘计算,端计算)
       高级C语言讲解
       Rust语言讲解



文章目录

Open5GS 架构详解

:https://open5gs.org/

:https://github.com/open5gs/open5gs

:https://open5gs.org/open5gs/docs/guide/01-quickstart/

Open5GS 项目介绍

      app开发定制公司下图是来自官方网站的app开发定制公司软件架构图,app开发定制公司图中展示了一些软件容器和一些NFs,Open5GS实现了 4G/5G NSA 和 5G SA 功能。

4G/ 5G NSA Core

Open5GS 4G/ 5G NSA app开发定制公司核心网包括以下组件:

  • 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 Coreapp开发定制公司实现了控制面和用户面的分离(CUPS)。

MMEapp开发定制公司是主要的控制平面,app开发定制公司负责管理会话、移动性、Paging和承载(bearers)。MME与HSS相连接,HSS会生成SIMapp开发定制公司卡的鉴权矢量以及签约用户的Profile。MMEapp开发定制公司也会与网关服务器的控制平面 SGWC 和 PGWC/SMF相连接。在4G中的所有eNodeBs都会与MME相连接。app开发定制公司控制平面的最后一个节点是PCRF,它位于PGWC/SMF和HSS之间,app开发定制公司处理收费和执行订阅用户策略。

app开发定制公司用户平面用于承载eNB/ NSA gNB (5G NSA基站)app开发定制公司与外部广域网之间的用app开发定制公司户数据报文。app开发定制公司两个用户平面核心组件是SGWU和PGWU/UPF,app开发定制公司每一个都与它们的控制平面连接。eNB / NSA gNG连接到SGWU, SGWU连接到PGWU/UPF,再连接到WAN。app开发定制公司通过在物理上将控制面app开发定制公司和用户面分开,app开发定制公司这样就可以在现场部署app开发定制公司多个用户面服务器(例如,app开发定制公司有高速互联网连接的地方),app开发定制公司同时还能保持集中的控制面功能。app开发定制公司这有利于支持MECapp开发定制公司的应用场景。

app开发定制公司上面的控制面和数据面分离,app开发定制公司以及控制面集中,app开发定制公司数据面可以分布式部署app开发定制公司的思想其实就是SDN的思想,关于SDNapp开发定制公司的介绍可以参考我的博客《》

所有这些Open5GSapp开发定制公司组件都有配置文件。app开发定制公司每个配置文件包含组件的IP绑定地址/app开发定制公司本地接口名称,app开发定制公司以及需要连接的其它组件的IP地址/ DNS名称。

5G SA Core

Open5GS 5G SA Coreapp开发定制公司包括以下功能:

  • 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 SAapp开发定制公司核心网的工作方式与4G核心不同——app开发定制公司它使用了基于服务的体系结构(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的介绍可以参考我的博客《》

下面是项目的目录以及目录中的主要内容,


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.5        port: 7777    ngap:      - addr: 127.0.0.5    guami:      - plmn_id:          mcc: 901          mnc: 70        amf_id:          region: 2          set: 1    tai:      - plmn_id:          mcc: 901          mnc: 70        tac: 1    plmn_support:      - plmn_id:          mcc: 901          mnc: 70        s_nssai:          - sst: 1    security:        integrity_order : [ NIA2, NIA1, NIA0 ]        ciphering_order : [ NEA0, NEA1, NEA2 ]    network_name:        full: Open5GS    amf_name: open5gs-amf0nrf:    sbi:      - addr:          - 127.0.0.10          - ::1        port: 7777
  • 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

在每个NF启动之前会进行初始化操作,在初始化过程中会解析配置文件参数,并保存起来。Open5GS使用开源的LibYAML库()对.yaml文件进行解析,相关代码如下:

// ~/open5gs-main/lib/app/ogs-init.cint ogs_app_initialize(        const char *version, const char *default_config,        const char *const argv[]){    ......    /**************************************************************************     * Stage 2 : Load Configuration File     */    if (optarg.config_file)        ogs_app()->file = optarg.config_file; // 用户自定义的配置文件路径    else        ogs_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;}
  • 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

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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

可以看到这里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    ...
  • 1
  • 2
  • 3
  • 4

较为复杂的对象格式,可以使用问号加一个空格代表一个复杂的 key,配合一个冒号加一个空格代表一个 value:

?      - complexkey1    - complexkey2:    - complexvalue1    - complexvalue2
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

意思即对象的属性是一个数组[complexkey1,complexkey2],对应的值也是一个数[complexvalue1,complexvalue2]

YAML 数组

- 开头的行表示构成一个数组:

- A- B- C
  • 1
  • 2
  • 3

YAML 支持多维数组,可以使用行内表示:
key: [value1, value2, ...]

数据结构的子成员是一个数组,则可以在该项下面缩进一个空格。

- - A - B - C
  • 1
  • 2
  • 3
  • 4

一个相对复杂的例子:

companies:    -        id: 1        name: company1        price: 200W    -        id: 2        name: company2        price: 500W
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

意思是 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

转换为 json 为:

{   languages: [ 'Ruby', 'Perl', 'Python'],  websites: {    YAML: 'yaml.org',    Ruby: 'ruby-lang.org',    Python: 'python.org',    Perl: 'use.perl.org'   } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

纯量

纯量是最基本的,不可再分的值,包括:

  • 字符串
  • 布尔值
  • 整数
  • 浮点数
  • 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    #日期必须使用ISO 8601格式,即yyyy-MM-dddatetime:     -  2018-02-17T15:02:31+08:00    #时间使用ISO 8601格式,时间和日期之间使用T连接,最后使用+代表时区
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

引用

& 锚点和 * 别名,可以用来引用:

defaults: &defaults  adapter:  postgres  host:     localhostdevelopment:  database: myapp_development  <<: *defaultstest:  database: myapp_test  <<: *defaults
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

相当于:

defaults:  adapter:  postgres  host:     localhostdevelopment:  database: myapp_development  adapter:  postgres  host:     localhosttest:  database: myapp_test  adapter:  postgres  host:     localhost
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

& 用来建立锚点(defaults),<< 表示合并到当前数据,* 用来引用锚点。

下面是另一个例子:

- &showell Steve - Clark - Brian - Oren - *showell 
  • 1
  • 2
  • 3
  • 4
  • 5

转为 JavaScript 代码如下:

[ 'Steve', 'Clark', 'Brian', 'Oren', 'Steve' ]
  • 1

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

Open5GS有一个通用的main.c文件,用于初始化容器环境,然后调用通用的接口app_initialize()来启动一个NF。下面是这个main.c的部分关键代码:

# ~/open5gs-main/open5gs-main/src/main.cint main(int argc, const char *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;}
  • 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

OPEN5GS_VERSION 是由位于 ~/open5gs-main/src/meson.build在编译时生成的,具体代码如下:

	......	version_conf.set_quoted('OPEN5GS_VERSION', package_version)	configure_file(output : 'version.h', configuration : version_conf)	......
  • 1
  • 2
  • 3
  • 4

上面代码中提到的app_initialize()其实就是一个抽象的接口,每个NF必须实现这个接口,以完成NF的创建。每个NF都会有一个app.c文件,在这个文件中实现了接口函数app_initialize(),下面是AMF的例子:

/* ~/open5gs-main/src/amf/app.c */int app_initialize(const char *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;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

下面是不同NF的app.c文件路径:

NF 启动过程(AMF为例)

我们上面介绍了容器环境的初始化以及NF创建接口函数的实现,下面以AMF实体为例子,介绍一下NF守护进程的创建过程。每个NF会有一个init.c文件,在这个文件中定义NF的初始化流程,代码如下:

/* ~/open5gs-main/src/amf/init.c *//**  初始化AMF实体*/int amf_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 的初始化参数*/static void amf_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);}
  • 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
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101

下面是每个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()获取下一个即将超时的定时器的剩余时间,如果没有定时器在运行,则返回 INFINITE
ogs_timer_mgr_expire()处理超时的定时器,执行超时定时器的回调函数

定时器的时间从哪里来?

这里使用的是系统时间,是由I\O多路复用技术来帮我们记录定时器运行时间的,还记得前面介绍的AMF实体的amf_main()函数吗?代码片段如下:

static void amf_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);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

ogs_timer_mgr_next()函数获取即将超时的定时器的剩余超时时间,并作为参数传入ogs_pollset_poll()函数,这个函数就是我们上面提及的I\O多路复用技术的实现,在Open5GS中可以选择使用selectepollkqueue这三种I\O多路复用技术,但是我们一般都是以epoll技术,因为epoll相比其它的I\O多路复用技术性能更好。下面是epoll函数针对ogs_pollset_poll()的具体实现过程(详细内容会在 消息通知机制 中介绍):

static int epoll_process(ogs_pollset_t *pollset, ogs_time_t timeout){    struct epoll_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;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

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.cint main(int argc, const char *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.cint ogs_app_initialize(        const char *version, const char *default_config,        const char *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;}
  • 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

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(unsigned int 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) {#if defined(HAVE_KQUEUE)        ogs_pollset_actions = ogs_kqueue_actions;  /* kqueue 模式 */#elif defined(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;}
  • 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

如果需要监听某个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:1259static void session_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 对应的回调函数,如果消息通知机制发现现在可以无阻塞的发送数据,* 则会触发此回调函数,进而将数据发送出去 */static void session_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);}
  • 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

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#if OGS_USE_TALLOC/***************************************** * Memory Pool - Use talloc library *****************************************/#define ogs_malloc(size) \    ogs_talloc_size(__ogs_talloc_core, size, __location__)#define ogs_calloc(nmemb, size) \    ogs_talloc_zero_size(__ogs_talloc_core, (nmemb) * (size), __location__)#define ogs_realloc(oldptr, size) \    ogs_talloc_realloc_size(__ogs_talloc_core, oldptr, size, __location__)#define ogs_free(ptr) ogs_talloc_free(ptr, __location__)#else/***************************************** * Memory Pool - Use pkbuf library *****************************************/#define ogs_malloc(size) ogs_malloc_debug(size, OGS_FILE_LINE)#define ogs_calloc(nmemb, size) ogs_calloc_debug(nmemb, size, OGS_FILE_LINE)#define ogs_realloc(ptr, size) ogs_realloc_debug(ptr, size, OGS_FILE_LINE)#define ogs_free(ptr) ogs_free_debug(ptr)#endif
  • 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

上述函数与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#define ogs_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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

通过这样的方式,不仅预分配了内存,还可以限制内存资源的消耗(ogs_pool_init()在初始化一个池的时候已经限制了可以同时实例化的对象上限)。

还需要特别说明的,Open5GS重写了STD C中的一些函数,例如strdup()strndup()等,相关的定义在~/open5gs-main/lib/core/ogs-strings.c~/open5gs-main/lib/core/ogs-strings.h中,进行二次开发时一定要使用这些重写的函数以保证内存管理的一致性。

Open5GS重写STD C
ogs_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.cvoid ogs_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}void ogs_log_vprintf(ogs_log_level_e level, int id,    ogs_err_t err, const char *file, int line, const char *func,    int content_only, const char *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    ......}
  • 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

下面介绍一下日志系统的运行时参数:

  • -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分隔;

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.cstatic void amf_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);   ......}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

上面的code中我们看到了FSM的初始化和终止相关的回调函数,那具体执行业务流程的回调函数在哪里,我们先看一下ogs_fsm_t结构体以及ogs_fsm_create()函数:

// ~/open5gs-main/lib/core/ogs-fsm.htypedef struct _ogs_fsm_t {    ogs_fsm_handler_t init;  // FSM 初始化相关的回调函数    ogs_fsm_handler_t fini;  // FSM 终止相关的回调函数    ogs_fsm_handler_t state; // FSM 业务执行相关的回调函数} ogs_fsm_t;#define ogs_fsm_create(__s, __i, __f) \    (((__s)->init = (__s)->state = (ogs_fsm_handler_t)(__i)), \     (__s)->fini = (ogs_fsm_handler_t)(__f))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

可以看到在创建FSM时,将初始化回调函数赋值给了initstate对象,之后执行FSM的初始化程序ogs_fsm_init(),代码如下:

// ~/open5gs-main/lib/core/ogs-fsm.cvoid ogs_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);            }        }    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

可以看到这里首先执行了FSM的初始化函数,然后判断s->init != s->state条件是否成立,如果成立则执行state回调函数。其实在执行FSM初始化回调函数时会改变state函数指针的值,使其指向真正处理业务的回调函数,amf FSM初始化代码如下:

// ~/open5gs-main/src/amf/amf-sm.cvoid amf_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#define OGS_FSM_TRAN(__s, __target) \    ((ogs_fsm_t *)__s)->state = (ogs_fsm_handler_t)(__target)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

可以看到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.cstatic void amf_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);}
  • 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

可以看到通过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.cint amf_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;}
  • 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

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的介绍可以参考我的博客《》

      Open5GS中关于sbi的code位于~/open5gs-main/lib/sbi/目录中,它包含了server.cclient.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,感兴趣的同学可以参考我下面给出的两个链接:


附录

部分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)接口相互通信。


网站建设定制开发 软件系统开发定制 定制软件开发 软件开发定制 定制app开发 app开发定制 app开发定制公司 电商商城定制开发 定制小程序开发 定制开发小程序 客户管理系统开发定制 定制网站 定制开发 crm开发定制 开发公司 小程序开发定制 定制软件 收款定制开发 企业网站定制开发 定制化开发 android系统定制开发 定制小程序开发费用 定制设计 专注app软件定制开发 软件开发定制定制 知名网站建设定制 软件定制开发供应商 应用系统定制开发 软件系统定制开发 企业管理系统定制开发 系统定制开发