Skip to content

OAI搭建步骤(EPC+eNB)

一、系统概述

OAI开源代码分为两部分:核心网EPC(openair-cn)+基站eNB(openairinterface5g)。EPC中包括HSS/MME/SPGW等核心网的功能模块;eNB中实现了基站处理的逻辑模块,基站的射频发射模块由USRP B210完成。

本文EPC搭建在Ubuntu 16.04.2 TLS上,通过手动部署的方式运行;eNB搭建在Ubuntu 14.04.3 LTS上,通过启动Docker镜像直接运行。EPC可以使用虚拟机,但是eNB必须使用物理机,因为eNB对时序要求非常严格,虚拟机无法做到实时,即使是物理机,也是不能百分之百满足实时需要的,真正的基站要依赖物理芯片保证实时性。

二、搭建核心网EPC(openair-cn)

前排提示:如果你不想手动搭建,可以下载我已经搭好的虚拟机直接使用。

链接: https://pan.baidu.com/s/1BsG5dpORoLzLZWxGusJO9A 提取码: swmh

2.1 准备主机

安装Ubuntu 16.04.2 TLS系统,注意主机的命名,因为oai中大多使用的是nano,建议主机和用户名都设置为nano。

通过sudo vim /etc/apt/sources.list进入源文件,替换源。推荐使用阿里源(国内速度较快,也可根据实际情更换其他源)。

修改完之后使用 sudo apt-get update更新软件包列表。

2.2 更换内核

Ubuntu 16.04.2 TLS本身自带generic内核,EPC对内核非常敏感,我们在这里把内核换成4.7.7-oaiepc内核。有博客说generic可以直接使用,反而不推荐4.7.7-oaiepc内核,会出现问题。如果后面出现编译问题,可以尝试更换一下内核。

先使用sudo apt-get install git vim net-tools 安装gitvimnet-tools

shell
git clone https://gitlab.eurecom.fr/oai/linux-4.7.x.git

cd linux-4.7.x
# 安装内核
sudo dpkg -i linux-headers-4.7.7-oaiepc_4.7.7-oaiepc-10.00.Custom_amd64.deb linux-image-4.7.7-oaiepc_4.7.7-oaiepc-10.00.Custom_amd64.deb
# 重启
sudo reboot
# 查看内核是否更换完成
uname -a

uname -a查看当前的内核版本,出现4.7.7-oaiepc,方可进行下一步操作。

2.3 获取openair-cn源码

shell
cd ~
git clone https://gitlab.eurecom.fr/oai/openair-cn.git

克隆完成源码后,执行

shell
cd ~/openair-cn
git checkout -b v0.5.0

2.4 编辑host

先使用hostname查询主机名,如果你得到的是ubuntu

执行:

shell
sudo vim /etc/hosts

观察前几行,把自带的127.0.1.1 ubuntu(这里是你的主机名)这行前面加上#注释掉,然后在下面加上新的两行

yaml
127.0.1.1       ubuntu.openair4G.eur   ubuntu #这一行的两个ubuntu都要改成你的主机名
127.0.1.1       hss.openair4G.eur   hss

编辑完之后执行hostname -f,看到输出为你的主机名.openair4G.eur,说明更改成功。

2.5 安装必要的软件

2.5.1 安装Mysql

shell
sudo apt-get install mysql-server mysql-client

密码自己设置,但是要记住,笔者将其设置为了Linux

2.5.2 安装Apache2

shell
sudo apt-get install apache2

测试Apache2,浏览器访问虚拟机的IP会出现apache2主页 (注:不可开VPN,否则不会出现)

2.5.3 安装PHP

shell
sudo apt-get install php7.0
sudo apt-get install libapache2-mod-php7.0
sudo service apache2 restart

测试PHP,创建新文件info.php

sudo vim /var/www/html/info.php

在该文件里添加php语句:

php
<?php
echo "<P>Hello World!</P> "?>

保存退出,然后浏览器访问 虚拟机ip/info.php,浏览器会显示hello world页面(不可挂VPN!)

2.5.4 安装 phpmyadmin

shell
sudo apt-get install phpmyadmin
sudo ln -s /usr/share/phpmyadmin /var/www/html

测试phpmyadmin:

浏览器访问虚拟机ip/phpmyadmin,浏览器会显示数据库登录页面。

如果报错:mbstringextension is missing.Please check your PHP configuration.

解决办法:

shell
sudo vim /etc/php/7.0/mods-available/json.ini

把文件里第二行的分号去掉,保存退出,重启网络
# 重启网络
sudo service apache2 restart

在数据库登录页面输入用户名root,密码Linux(你自己设置的密码),即可进入数据库,后面会进来修改一个值。

2.6 编译EPC

shell
cd ~/openair-cn
git checkout develop 
git pull
cd ~/openair-cn/scripts
./build_mme -i 
./build_hss -i 
./build_spgw -i

无红色报错说明安装成功,一般来说比较容易,不是出错的地方。 注1:以上命令只需跑一次就行了,命令的意思是补充未安装到的包(Need to run only once to install missing packages) 注2:build的时候请不要挂机。如果网速很差,中间很可能遇到EOF(因为安装freeDiameter的时候要从外网把freeDiameter源码克隆过来编译安装)

2.7 配置EPC

2.7.1 文件拷贝

shell
sudo mkdir -p /usr/local/etc/oai/freeDiameter
sudo cp ~/openair-cn/etc/mme.conf /usr/local/etc/oai
sudo cp ~/openair-cn/etc/hss.conf /usr/local/etc/oai
sudo cp ~/openair-cn/etc/spgw.conf /usr/local/etc/oai
sudo cp ~/openair-cn/etc/acl.conf /usr/local/etc/oai/freeDiameter
sudo cp ~/openair-cn/etc/mme_fd.conf /usr/local/etc/oai/freeDiameter
sudo cp ~/openair-cn/etc/hss_fd.conf /usr/local/etc/oai/freeDiameter

2.7.2 MME配置

执行ifconfig查询自己的网卡名、IP地址和掩码,记下来。

ifconfig

上图中,ens33就是网卡名,192.168.1.111就是IP地址,255.255.255.0就是掩码,255化成二进制是8个1,3个255就是24了,所以掩码也是24

执行

shell
sudo vim /usr/local/etc/oai/mme.conf

核对和修改成以下信息,其中

shell
MME_INTERFACE_NAME_FOR_S1_MME     = "ens33";             # YOUR NETWORK CONFIG HERE
MME_IPV4_ADDRESS_FOR_S1_MME      = "192.168.1.111/24";       # YOUR NETWORK CONFIG HERE

把这两行改成上面我们查到的网卡名,IP地址和掩码。

再检查第25行左右和第41行左右,是否均有这一行:

shell
PID_DIRECTORY               = "/var/run";

如果找到了两处一模一样的,把其中一行注释掉,不然后面启动MME会报错。

注意,在文件第87行左右,会有如下内容,定义了MCC,MNC和TAC:

shell
TAI_LIST = (
      {MCC="208" ; MNC="93"; TAC = "1"; }                 # YOUR TAI CONFIG HERE
   );

目前先不用修改这三个值,后面会提到。

上面要修改的内容均修改完毕后,即可保存文件。

2.7.3 SPGW配置

shell
sudo vim /usr/local/etc/oai/spgw.conf

请在

shell
PGW_INTERFACE_NAME_FOR_SGI = "ens33"; # 此处的ens33要改成你的网卡名称

这行代码之后添加以下代码(这行代码源码是没有的,需要手动添加!)。

shell
PGW_IPV4_ADDRESS_FOR_SGI = "192.168.1.111/24" #此处的IP地址也要改成你的IP地址

另外,下面两处的网卡名和IP地址也要改成你自己的

shell
SGW_INTERFACE_NAME_FOR_S1U_S12_S4_UP  = "ens33";

SGW_IPV4_ADDRESS_FOR_S1U_S12_S4_UP   = "192.168.1.111/24";

上面要修改的内容均修改完毕后,即可保存文件。

2.7.4 HSS freediameter 配置

shell
sudo vim /usr/local/etc/oai/freeDiameter/hss_fd.conf

确认信息如下:

shell
Identity = "hss.openair4G.eur";
Realm = "openair4G.eur";

2.7.5 MME freediameter 配置

shell
sudo vim /usr/local/etc/oai/freeDiameter/mme_fd.conf

确认信息如下:

shell
Identity = "ubuntu.openair4G.eur"; #此处的ubuntu要改成你自己的主机名
Realm = "openair4G.eur";
ConnectPeer= "hss.openair4G.eur" { ConnectTo = "127.0.0.1"; No_SCTP ; No_IPv6; Prefer_TCP; No_TLS; port = 3868; realm = "openair4G.eur";};

这里确认Identity就行。Identity就是hostname -f显示的名字,上面也修改过了。

2.7.6 HSS 配置

shell
sudo vim /usr/local/etc/oai/hss.conf

把下面两行进行修改,用户名为root,密码是当初自己设置的值

shell
MYSQL_user  = "root"; # Database server login
MYSQL_pass  = "Linux"; # Database server password

下面有一个OPERATOR_key可以先不管,用这个默认的就可以,是后面SIM卡相关信息。

2.8 编译 EPC

2.8.1 注册证书

shell
cd ~/openair-cn/scripts
./check_hss_s6a_certificate /usr/local/etc/oai/freeDiameter/ hss.openair4G.eur
./check_mme_s6a_certificate /usr/local/etc/oai/freeDiameter/ ubuntu.openair4G.eur #这里的ubuntu要改成自己的主机名

2.8.2 编译HSS

shell
cd ~/openair-cn/scripts
sudo ./build_hss -c

第一次运行hss需要使用如下命令

shell
./run_hss -i ~/openair-cn/src/oai_hss/db/oai_db.sql

首次运行时需要注册数据库,后面不需要运行这个了,以后再运行直接./run_hss即可。

2.8.3 编译MME

shell
cd ~/openair-cn/scripts
sudo ./build_mme

2.8.4 编译SPGW

shell
cd ~/openair-cn/scripts
sudo ./build_spgw

2.9 运行 EPC

请注意运行的顺序,先HSS,再MME,再SPGW。其实只要保证HSS最先运行就行。

开三个终端,均cd ~/openair-cn/scripts来到此目录下。

2.9.1 运行 HSS

在2.8.2中提到了首次运行hss需要注册数据库,如果刚才没有注册过,就执行

shell
./run_hss -i ~/openair-cn/src/oai_hss/db/oai_db.sql

注册过后就执行

shell
./run_hss

2.9.2 运行 MME

shell
./run_mme

2.9.3 运行SPGW

shell
./run_spgw

2.9.4 修改数据库

在浏览器中输入:

shell
虚拟机的IP地址/phpmyadmin

使用用户名root和设置的密码进入Mysql。进入之后,点击左侧oai_db->mmeidentity,先看下有没有你的主机名.openair4G.eur,如果有,就不用修改。如果没有,应该可以看到有条记录是yang.openair4G.eur,点击编辑,把这个yang.openair4G.eur改成你的主机名.openair4G.eur,点击执行即可。这步很关键,不然HSS会把你当做僵尸用户。

因为EPC的启动不依赖于eNB,所以我们现在已经把EPC启动起来了,接下来就配置并启动eNB。

三、搭建基站eNB(openairinterface5g)

笔者尝试在Ubuntu手动搭建eNB,但是没有搭起来,原因在最后会提到。下面说一下我成功用docker启动eNB的过程。

在本文最开始,已经提到了eNB必须使用物理机来搭,我使用的是Ubuntu 14.04.3 LTS。

3.1 准备主机

在物理机安装Ubuntu 14.04.3 TLS系统,注意主机的命名,因为oai中大多使用的是nano,建议主机和用户名都设置为nano。

通过sudo vim /etc/apt/sources.list进入源文件,替换源。推荐使用阿里源(国内速度较快,也可根据实际情更换其他源)。

修改完之后使用 sudo apt-get update更新软件包列表。

3.2 更换内核

Ubuntu 14.04.3 TLS本身自带的内核不能满足eNB对实时性的要求,虽然我们使用Docker来运行eNB,但是宿主机依然需要低时延,所以我们执行如下命令,更换3.19.0-61低延迟内核。

shell
# 安装内核
sudo apt-get install linux-image-3.19.0-61-lowlatency linux-headers-3.19.0-61-lowlatency
# 重启
sudo reboot
# 查看内核是否更换完成
uname -r

重启之后,看到系统内核为3.19.0-61-lowlatency,方可进行下一步操作。

3.3 安装Docker

安装一些必要的系统工具

shell
sudo apt-get -y install apt-transport-https ca-certificates curl software-properties-common

安装GPG证书

shell
curl -fsSL http://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg | sudo apt-key add -

添加docker仓库

shell
sudo add-apt-repository "deb [arch=amd64] http://mirrors.aliyun.com/docker-ce/linux/ubuntu $(lsb_release -cs) stable"

更新并安装 Docker-CE

shell
sudo apt-get -y update
sudo apt-get -y install docker-ce

执行下面的命令,出现docker的版本信息,即可认为安装成功

shell
sudo docker version

因为我们没有把当前用户加入到Docker用户组,所以接下来的操作请使用root用户操作。

3.4 拉取eNB镜像

OAI官网的OAI in docker中,官网只是给出了构建镜像的方法,最多也只是给了一个Dockerfile让用户自己构建,并没有给出一个已经构建好的镜像给我们用。

我在Docker Hub找到一个亲测可以使用的镜像sofianinho/docker-openairinterface-enb,在其Docker Hub的介绍页,给出了镜像的使用方法,镜像Dockerfile和Docker-compose文件所在的GitHub仓库,接下来我们使用这个镜像来启动eN节点。

拉取镜像

shell
docker pull sofianinho/docker-openairinterface-enb

3.5 创建配置文件

注:下文的enb.conf文件可以直接去镜像的GitHub仓库下载

/root目录下,新建文件enb.conf,内容如下

yaml
Active_eNBs = ( "eNB_Eurecom_LTEBox");
# Asn1_verbosity, choice in: none, info, annoying
Asn1_verbosity = "none";

eNBs =
(
 {
    ////////// Identification parameters:
    eNB_ID    =  0xe00;

    cell_type =  "CELL_MACRO_ENB";

    eNB_name  =  "eNB_Eurecom_LTEBox";

    // Tracking area code, 0x0000 and 0xfffe are reserved values
    tracking_area_code  =  "1";

    mobile_country_code =  "208";

    mobile_network_code =  "95";

       ////////// Physical parameters:

    component_carriers = (
      {
        node_function                             = "eNodeB_3GPP";
	    node_timing                               = "synch_to_ext_device";
	    node_synch_ref                            = 0;
        frame_type					              = "FDD";
        tdd_config 					              = 3;
        tdd_config_s            			      = 0;
        prefix_type             			      = "NORMAL";
        eutra_band              			      = 5;
        downlink_frequency      			      = 881500000L;
        uplink_frequency_offset 			      = -45000000;
        Nid_cell					              = 0;
        # N_RB_DL下行RB数量,代表带宽,可选1,3,4,5,10,15,20,25,50,100
        N_RB_DL                 			      = 25;
        Nid_cell_mbsfn          			      = 0;
        nb_antenna_ports          			      = 1;
        nb_antennas_tx          			      = 2;
        nb_antennas_rx          			      = 2;
        tx_gain                                   = 90;
        rx_gain                                   = 125;
        prach_root              			      = 0;
        prach_config_index      			      = 0;
        prach_high_speed        			      = "DISABLE";
        prach_zero_correlation  			      = 1;
        prach_freq_offset       			      = 2;
        pucch_delta_shift       			      = 1;
        pucch_nRB_CQI           			      = 1;
        pucch_nCS_AN            			      = 0;
        pucch_n1_AN             			      = 32;
        pdsch_referenceSignalPower 			      = -24;
        pdsch_p_b                  			      = 0;
        pusch_n_SB                 			      = 1;
        pusch_enable64QAM          			      = "DISABLE";
        pusch_hoppingMode                                  = "interSubFrame";
        pusch_hoppingOffset                                = 0;
        pusch_groupHoppingEnabled  			      = "ENABLE";
        pusch_groupAssignment      			      = 0;
        pusch_sequenceHoppingEnabled		   	      = "DISABLE";
        pusch_nDMRS1                                       = 1;
        phich_duration                                     = "NORMAL";
        phich_resource                                     = "ONESIXTH";
        srs_enable                                         = "DISABLE";
        /*  srs_BandwidthConfig                                =;
        srs_SubframeConfig                                 =;
        srs_ackNackST                                      =;
        srs_MaxUpPts                                       =;*/

        pusch_p0_Nominal                                   = -96;
        pusch_alpha                                        = "AL1";
        pucch_p0_Nominal                                   = -103;
        msg3_delta_Preamble                                = 6;
        pucch_deltaF_Format1                               = "deltaF2";
        pucch_deltaF_Format1b                              = "deltaF3";
        pucch_deltaF_Format2                               = "deltaF0";
        pucch_deltaF_Format2a                              = "deltaF0";
        pucch_deltaF_Format2b		    	      = "deltaF0";

        rach_numberOfRA_Preambles                          = 64;
        rach_preamblesGroupAConfig                         = "DISABLE";
        /*
        rach_sizeOfRA_PreamblesGroupA                      = ;
        rach_messageSizeGroupA                             = ;
        rach_messagePowerOffsetGroupB                      = ;
        */
        rach_powerRampingStep                              = 4;
        rach_preambleInitialReceivedTargetPower            = -104;
        rach_preambleTransMax                              = 10;
        rach_raResponseWindowSize                          = 10;
        rach_macContentionResolutionTimer                  = 48;
        rach_maxHARQ_Msg3Tx                                = 4;

        pcch_default_PagingCycle                           = 128;
        pcch_nB                                            = "oneT";
        bcch_modificationPeriodCoeff			      = 2;
        ue_TimersAndConstants_t300			      = 1000;
        ue_TimersAndConstants_t301			      = 1000;
        ue_TimersAndConstants_t310			      = 1000;
        ue_TimersAndConstants_t311			      = 10000;
        ue_TimersAndConstants_n310			      = 20;
        ue_TimersAndConstants_n311			      = 1;

	ue_TransmissionMode				      = 1;
      }
    );

    srb1_parameters :
    {
        # timer_poll_retransmit = (ms) [5, 10, 15, 20,... 250, 300, 350, ... 500]
        timer_poll_retransmit    = 80;

        # timer_reordering = (ms) [0,5, ... 100, 110, 120, ... ,200]
        timer_reordering         = 35;

        # timer_reordering = (ms) [0,5, ... 250, 300, 350, ... ,500]
        timer_status_prohibit    = 0;

        # poll_pdu = [4, 8, 16, 32 , 64, 128, 256, infinity(>10000)]
        poll_pdu                 =  4;

        # poll_byte = (kB) [25,50,75,100,125,250,375,500,750,1000,1250,1500,2000,3000,infinity(>10000)]
        poll_byte                =  99999;

        # max_retx_threshold = [1, 2, 3, 4 , 6, 8, 16, 32]
        max_retx_threshold       =  4;
    }

    # ------- SCTP definitions
    SCTP :
    {
        # Number of streams to use in input/output
        SCTP_INSTREAMS  = 2;
        SCTP_OUTSTREAMS = 2;
    };

    ////////// MME parameters:

    mme_ip_address      = ( { ipv4       = "192.168.1.111"; #修改为你的EPC主机的IP地址
                              ipv6       = "192:168:30::17";
                              active     = "yes";
                              preference = "ipv4";
                            }
                          );

    NETWORK_INTERFACES :
    {

        ENB_INTERFACE_NAME_FOR_S1_MME            = "eth0"; #改为你的enb主机的网卡
        ENB_IPV4_ADDRESS_FOR_S1_MME              = "192.168.1.111/24"; #修改为你的EPC主机的IP地址
        ENB_INTERFACE_NAME_FOR_S1U               = "lo";
        ENB_IPV4_ADDRESS_FOR_S1U                 = "172.17.0.1/24";
        ENB_PORT_FOR_S1U                         = 2152; # Spec 2152
    };

    log_config :
    {
      global_log_level                      ="info";
      global_log_verbosity                  ="medium";
      hw_log_level                          ="info";
      hw_log_verbosity                      ="medium";
      phy_log_level                         ="info";
      phy_log_verbosity                     ="medium";
      mac_log_level                         ="info";
      mac_log_verbosity                     ="high";
      rlc_log_level                         ="info";
      rlc_log_verbosity                     ="medium";
      pdcp_log_level                        ="info";
      pdcp_log_verbosity                    ="medium";
      rrc_log_level                         ="info";
      rrc_log_verbosity                     ="medium";
   };
  }
);

注意要将mme_ip_address 位置处的ipv4修改为你的EPC主机的IP地址, ENB_INTERFACE_NAME_FOR_S1_MMEENB_IPV4_ADDRESS_FOR_S1_MME分别要改为enb主机的网卡和EPC主机的IP地址。

此文件的tracking_area_code mobile_country_code mobile_network_code要与2.7.2提到的文件第87行左右的TAC、MCC和MNC保持一致。

3.6 启动eNB节点

先保证EPC的三个服务已经全部启动,然后将USRP B210设备插入到eNB主机的USB 3.0端口,运行

shell
docker run -it --privileged --net=host -v /root:/config -v /dev/bus/usb:/dev/bus/usb sofianinho/docker-openairinterface-enb:latest

等到终端保持如下输出

eNB

同时也可以观察到USRP B210的指示灯亮起,即可证明eNB启动成功。另外在MME终端窗口,也可以看到已连接的eNB节点数量由0变为了1。

EPC_MME

EPC和eNB都跑起来之后,即可使用UE(也就是我们使用的手机等可以连接基站的设备)去搜索基站发出的信号。大致操作就是进入手机设置-移动网络-手动选择运营商,等待手机搜索运营商,会搜出来除中国移动、中国电信、中国联通之外的一个由数字命名的运营商,这个数字就是我们设置的MCC和MNC的组合。在笔者亲测过程中,发现如果使用中国电信的SIM卡,手机不允许用户自动选择运营商,使用中国移动的SIM卡可以手动选择。

根据下面的介绍,你可以修改自行上面的MCC、MNC。

3.7 MCC、MNC、TAC和IMSI的介绍

3.7.1 MCC、MNC和TAC

MCC(Mobile Country Code):移动设备国家代码

MNC(Mobile Network Code):移动设备网络代码

TAC(Tracking Area Code):跟踪区编码

MCC是用于区分国家、MNC用于区分运营商、TAC用于区分地域

中国的MCC、MNC情况如下表

MCCMNC运营商使用状态频段(MHz)
46000中国移动(China Mobile)营运中GSM 900 / GSM 1800 / TD-SCDMA 1880 / TD-SCDMA 2010 / TD-LTE 1800/2300/2600
46001中国联通(China Unicom)营运中GSM 900 / GSM 1800 / UMTS 2100 / TD-LTE 2300/2600 / FDD-LTE 1800/2100
46002中国移动(China Mobile)营运中GSM 900 / GSM 1800 / TD-SCDMA 1880 / TD-SCDMA 2010
46003中国电信(China Telecom)营运中CDMA2000 800 / CDMA2000 2100 / TD-LTE 2300/2600 / FDD-LTE 1800/2100 / EV-DO / eHRPD
46005中国电信(China Telecom)营运中
46006中国联通(China Unicom)营运中GSM 900 / GSM 1800 / UMTS 2100
46007中国移动(China Mobile)营运中GSM 900 / GSM 1800 / TD-SCDMA 1880 / TD-SCDMA 2010
46009中国联通(China Unicom)营运中
46011中国电信(China Telecom)营运中CDMA2000 800 / CDMA2000 2100 / TD-LTE 2300/2600 / FDD-LTE 1800/2100 / EV-DO / eHRPD
46020中国铁通(China Tietong)营运中GSM-R

3.7.2 IMSI

IMSI(International Mobile Subscriber Identity):国际移动用户识别码

IMSI是用于区分蜂窝网络中不同用户的、在所有蜂窝网络中不重复的识别码。

在GSM、UMTS和LTE网络中,IMSI来自SIM卡,在CDMA2000网络中则是直接来自手机,或者RUIM。

IMSI

为了避免被监听者识别并追踪特定的用户,大部分情形下手机和网络之间的通信会使用随机产生的临时移动用户识别码(TMSI,Temporary Mobile Subscriber Identity)代替IMSI。

3.8 为什么手动部署eNB没跑起来

在手动部署eNB时,最后使用命令./build_oai --eNB -c -w USRP编译eNB时,编译过程中会下载固件镜像,curl总是会报ssl_handshake错误,我尝试挂梯子,或者python3直接运行uhd_images_downloader.py文件手动下载镜像后再编译,都会出现ssl_handshake错误。我更新了Ubuntu上面的OpenSSL,Cmake,都会ssl_handshake错误,所以没跑起来,最后用了Docker一步到位,如果有大神知道怎么解决,希望可以多多交流。