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

Posted 从善若水

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了5GS开源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;

    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.c
int main(int argc, cons

以上是关于5GS开源5G核心网(Open5GS)架构详解的主要内容,如果未能解决你的问题,请参考以下文章

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

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

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

5G核心网信令流程 | 5GS会话管理

5G核心网信令流程 | 5GS会话支持LADN

5G核心网信令流程 | 5GS会话支持ULCL