5GC开源5G核心网(Open5GS)架构详解

Posted 从善若水

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了5GC开源5G核心网(Open5GS)架构详解相关的知识,希望对你有一定的参考价值。

博主未授权任何人或组织机构转载博主任何原创文章,感谢各位对原创的支持!
博主链接

本人就职于国际知名终端厂商,负责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.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-amf0

nrf:
    sbi:
      - addr:
          - 127.0.0.10
          - ::1
        port: 7777

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

// ~/open5gs-main/lib/app/ogs-init.c

int 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;

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: 1
        name: company1
        price: 200W
    -
        id: 2
        name: 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: ~  #使用~表示null
string:
    - 哈哈
    - 'Hello world'  #可以使用双引号或者单引号包裹特殊字符
    - newline
      newline2    #字符串可以拆成多行,每一行会被转化成一个空格
date:
    - 2018-02-17    #日期必须使用ISO 8601格式,即yyyy-MM-dd
datetime: 
    -  2018-02-17T15:02:31+08:00    #时间使用ISO 8601格式,时间和日期之间使用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
int 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;

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 */
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;


下面是不同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);

下面是每个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);

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;

    5G核心网技术基础自学系列 | 5GC架构概述

5G5G网络架构及网元功能

5G核心网技术基础自学系列 | 5G核心网的两种观点

[5GC]《5G核心网-赋能数字化时代》| 6.1 PDU会话的概念

5G核心网手把手教你将Open5gs托管到k8s(KubeSphere)

5G核心网手把手教你将Open5gs托管到k8s(KubeSphere)