首页 文章 精选 留言 我的

精选列表

搜索[openharmony],共389篇文章
优秀的个人博客,低调大师

OpenHarmony 项目实战:基于全志 XR806 实现的简易四足行走机器狗

本文整理自:https://bbs.aw-ol.com/topic/932/ 作者 @ 堇花还没开吗 小米在2021年发布的自研四足仿生机器人——CyberDog,使得“仿生机器人”这个话题 再次站在风口之上 ,伴随芯片、舵机、减速器等核心部件的发展,也期待更多更“生动”的“仿生机器人”出现 ,全志XR806是一款支持WiFi和BLE的芯片可满足智能机器人等领域的无线连接需求,今天就先带大家看一款基于全志XR806开源鸿蒙开发板做的四足行走机器狗。 一、机器狗外观图 二、PCA9685开发流程 狗子身上有12个舵机,一条腿接3个舵机,连接到一个pca9685舵机驱动上,舵机驱动通过I2C与XR806通信,XR806有两个I2C接口,我主要是用第1个(B14, B15),通过控制PCA9685上的寄存器来简介控制舵机。 pca9685参数: I2C接口,支持高达16路PWM输出,每路12位分辨率(4096级); 内置25MHz晶振,可不连接外部晶振,也可以连接外部晶振,最大50MHz; 支持2.3V-5.5V电压,最大耐压值5.5V,逻辑电平3.3V; 具有上电复位,以及软件复位等功能。 引脚图示: 1. pca初始化 首先要进行i2c初始化: i2c_init(i2c_id); 导入相关库: #include "driver/chip/hal_i2c.h" #define i2c_id 1 # 使用第1个i2c i2c初始化函数: void i2c_init(unsigned int id){ I2C_InitParam initParam; initParam.addrMode = I2C_ADDR_MODE_7BIT; initParam.clockFreq = 40000; if (HAL_I2C_Init(id, &initParam) != HAL_OK) { printf("i2c init fail!\n"); while(1); } else { printf("i2c init success!\n"); } } 主要是调用HAL_I2C_Init函数对第1个i2c接口进行初始化。 初始化pca9685: pca9685_init(60); pca9685_init函数: void pca9685_init(float hz) { pca_write(pca_mode1, 0x0); pca_setfreq(hz); OS_MSleep(500); } 2. pca操作 1. pca9685写寄存器 void pca_write(uint8_t reg_addr, uint8_t data) { uint8_t buf[1]; buf[0] = data; HAL_I2C_Master_Transmit_Mem_IT(i2c_id, pca_adrr, reg_addr, I2C_MEMADDR_SIZE_8BIT, buf, 1); } 2. pca9685读寄存器 uint8_t pca_read(uint8_t reg_addr) { uint8_t buf[1]; HAL_I2C_Master_Receive_Mem_IT(i2c_id, pca_adrr, reg_addr, I2C_MEMADDR_SIZE_8BIT, buf, 1); return buf[0]; } 3. pca9685设置频率 void pca_setfreq(float freq) { uint8_t prescale,old_mode,new_mode; double prescaleval; freq *= 0.92; prescaleval = 25000000; prescaleval /= 4096; prescaleval /= freq; prescaleval -= 1; prescale =(uint8_t)(prescaleval + 0.5f); old_mode = pca_read(pca_mode1); new_mode = (old_mode & 0x7F) | 0x10; pca_write(pca_mode1, new_mode); pca_write(pca_pre, prescale); pca_write(pca_mode1, old_mode); OS_MSleep(2); pca_write(pca_mode1, old_mode | 0xa1); } 这里的代码不像读函数和写函数一样好理解,主要是一般情况下,在用pca9685内置晶振,为25MHZ,通过配置PRE_SCALE寄存器进行配置。如果在舵机控制中,采用内置晶振,取osc_clock=25000000,update_rate=50(舵机控制频率50Hz)。 而且在写PRESCALE寄存器的时候,要先设置为Sleep模式,也就是将mode1寄存器的SLEEP标志位设置为1,具体可参考我上面写的代码。 3. pca设置pwm void pca_setpwm(uint8_t num, uint32_t on, uint32_t off) { pca_write(LED0_ON_L+4*num,on); pca_write(LED0_ON_H+4*num,on>>8); pca_write(LED0_OFF_L+4*num,off); pca_write(LED0_OFF_H+4*num,off>>8); } pwm通道寄存器如下图: 由图可知,对于每一个通道,有4个寄存器,在设置PWM占空比的时候,首先配置舵机的示例如下图所示(ON < OFF的情况): 狗子运行图: 核心代码: #include <stdio.h> #include "ohos_init.h" #include "kernel/os/os.h" // #include "iot_i2c.h" #include "driver/chip/hal_i2c.h" #include "iot_errno.h" #include "math.h" #define i2c_id 1 #define pca_adrr 0x40 // pca9685设备地址 #define pca_mode1 0x0 #define pca_pre 0xFE #define LED0_ON_L 0x6 #define LED0_ON_H 0x7 #define LED0_OFF_L 0x8 #define LED0_OFF_H 0x9 static OS_Thread_t g_main_thread; void pca_write(uint8_t reg_addr, uint8_t data) { uint8_t buf[1]; buf[0] = data; HAL_I2C_Master_Transmit_Mem_IT(i2c_id, pca_adrr, reg_addr, I2C_MEMADDR_SIZE_8BIT, buf, 1); } uint8_t pca_read(uint8_t reg_addr) { uint8_t buf[1]; HAL_I2C_Master_Receive_Mem_IT(i2c_id, pca_adrr, reg_addr, I2C_MEMADDR_SIZE_8BIT, buf, 1); return buf[0]; } void pca_setfreq(float freq) { uint8_t prescale,old_mode,new_mode; double prescaleval; freq *= 0.92; prescaleval = 25000000; prescaleval /= 4096; prescaleval /= freq; prescaleval -= 1; prescale =(uint8_t)(prescaleval + 0.5f); old_mode = pca_read(pca_mode1); new_mode = (old_mode & 0x7F) | 0x10; pca_write(pca_mode1, new_mode); pca_write(pca_pre, prescale); pca_write(pca_mode1, old_mode); OS_MSleep(2); pca_write(pca_mode1, old_mode | 0xa1); } void pca_setpwm(uint8_t num, uint32_t on, uint32_t off) { pca_write(LED0_ON_L+4*num,on); pca_write(LED0_ON_H+4*num,on>>8); pca_write(LED0_OFF_L+4*num,off); pca_write(LED0_OFF_H+4*num,off>>8); } void pca9685_init(float hz) { pca_write(pca_mode1, 0x0); pca_setfreq(hz); OS_MSleep(500); } void pca_rotate(uint8_t num,uint8_t angle) { uint32_t off=0; off=floor(angle * 2 + angle / 5 + 158); pca_setpwm(num, 0, off); } void i2c_init(unsigned int id){ I2C_InitParam initParam; initParam.addrMode = I2C_ADDR_MODE_7BIT; initParam.clockFreq = 40000; if (HAL_I2C_Init(id, &initParam) != HAL_OK) { printf("i2c init fail!\n"); while(1); } else { printf("i2c init success!\n"); } } static void MainThread(void *arg) { uint8_t i = 0; i2c_init(i2c_id); pca9685_init(60); printf("i2c and pca9685 init done.\n"); while(1){ pca_rotate(0, 0); OS_MSleep(1000); pca_rotate(0, 180); OS_MSleep(1000); } } void PCAMain(void) { printf("PCA9685 Motor Start.\n"); if (OS_ThreadCreate(&g_main_thread, "MainThread", MainThread, NULL, OS_THREAD_PRIO_APP, 10 * 1024) != OS_OK) { printf("[ERR] Create MainThread Failed\n"); } } SYS_RUN(PCAMain); 狗子的相关步态算法还在调试,使用的舵机是MG90S,走得还不是很流畅,狗腿子会抖,后面需要继续调试,而且供电也是个大问题,不过已经焊了个供电模块稍微解决了,后面还要给狗子加个壳。

优秀的个人博客,低调大师

OpenHarmony分布式软总线流程分析v1.0丨1.被发现端,发布服务

更新时间:2021年6月26日 @亮子力 这次的软总线流程分析我分成了2个部分,具体如下: [1.被发现端,发布服务](https://harmonyos.51cto.com/posts/6295#bkwz) [2.开启软总线,建立连接](https://harmonyos.51cto.com/posts/6296#bkwz) 完整版可以下载文末的pdf。 # 源码目录: 主目录\foundation\communication\softbus_lite ``` ├── authmanager【提供设备认证机制和设备知识库管理】 ├── discovery【提供基于coap协议的设备发现机制】 ├── interfaces ├── os_adapter【操作系统适配层】 ├── trans_service【提供认证和数据传输通道】 └── BUILD.gn 通过BUILD.gn的分析,我们知道整个softbus_lite目录下的所有源码文件将被编译到一个动态库中。 其他依赖软总线的模块在编译的时候加上这个动态库的依赖就可以了。 例如:分布式调度子系统所在的foundation这个bin文件的编译就依赖这个动态库。 ``` ## authmanager【提供设备认证机制和设备知识库管理】 ``` 当发现有请求时,调用ProcessDataEvent函数,收包,检验包头,根据数据包的类型确定不同的处理方式。类型主要包括以下三种: MODULE_AUTH_SDK 加密数据类型 MODULE_TRUST_ENGINE 可信类型,直接进行数据传输 MODULE_CONNECTION 进行ip及设备认证 ├── BUILD.gn ├── include │ ├── auth_conn.h │ ├── auth_interface.h │ ├── bus_manager.h │ ├── msg_get_deviceid.h │ └── wifi_auth_manager.h └── source ├── auth_conn.c【提供发送、接收、认证、获取秘钥功能】 ├── auth_interface.c【管理各个会话节点、各个链接节点、各个秘钥节点,提供包括增删改查等功能】 ├── bus_manager.c【主要通过deviceIp创建两个不同的listen,主要用来监听系统上有哪些device及新的device节点的创建;其中有两个回调函数OnConnectEvent和OnDataEvent,分别是用来处理设备节点的基本操作及节点数据的处理】 ├── msg_get_deviceid.c【提供以cJSON格式获取各个设备的信息,包括设备id、链接信息、设备名、设备类型等】 └── wifi_auth_manager.c【主要实现了连接管理和数据接收功能。连接管理包括连接的建立、断开及连接的查找。数据接收包括数据获取、包头及包长等的校验,并且为了简化包头数据读取,单独实现了对一个int型和一个long型数据的接收函数】 ``` ## discovery【提供基于coap协议的设备发现机制】 ``` ├── BUILD.gn ├── coap【目录主要是负责COAP协议的部分】 │ ├── include │ │ ├── coap_adapter.h │ │ ├── coap_def.h │ │ ├── coap_discover.h │ │ ├── coap_socket.h │ │ ├── json_payload.h │ │ ├── nstackx_common.h │ │ ├── nstackx_database.h │ │ ├── nstackx_device.h │ │ ├── nstackx_error.h │ │ └── nstackx.h │ └── source │ ├── coap_adapter.c │ ├── coap_discover.c【实现了基于COAP的设备发现功能】 │ ├── coap_socket.c │ ├── json_payload.c │ ├── nstackx_common.c │ └── nstackx_device.c └── discovery_service【基于coap协议实现了轻量设备端的服务发布的能力】 ├── include │ ├── coap_service.h │ ├── common_info_manager.h │ └── discovery_error.h └── source ├── coap_service.c ├── common_info_manager.c └── discovery_service.c discovery的实现前提是确保发现端设备与接收端设备在同一个局域网内且能互相收到对方的报文。大致流程是: 发现端设备,使用coap协议在局域网内发送广播; 接收端设备使用PublishService接口发布服务,接收端收到广播后,发送coap协议单播给发现端; 发现端设备收到报文会更新设备信息。 ``` ## os_adapter【操作系统适配层】 ``` ├── include │ └── os_adapter.h └── source ├── L0 │ └── os_adapter.c └── L1 └── os_adapter.c ``` ## trans_service【提供认证和数据传输通道】 ``` 它主要封装了socket、cJSON、线程锁接口,实现了用户的创建、监听、会话管理,以及设备、指令、数据等信息的获取,最终提供加密和解密传输两种传输通道。 ├── BUILD.gn ├── include │ ├── libdistbus │ │ ├── auth_conn_manager.h │ │ └── tcp_session_manager.h │ └── utils │ ├── aes_gcm.h │ ├── comm_defs.h │ ├── data_bus_error.h │ ├── message.h │ └── tcp_socket.h └── source ├── libdistbus │ ├── auth_conn_manager.c 【用户创建,监听,连接等服务管理】 │ ├── tcp_session.c 【会话管理】 │ ├── tcp_session.h │ ├── tcp_session_manager.c │ ├── trans_lock.c 【互斥锁初始化以及互斥锁资源获取与释放】 │ └── trans_lock.h └── utils ├── aes_gcm.c 【提供加密传输和解密传输接口】 ├── message.c 【用于获取以cJSON格式管理的设备(包括设备名、设备类型、设备ID等)、指令、数据、会话(包括用户端口、会话端口等)等信息】 └── tcp_socket.c 【端口号管理以及数据传输管理】 ``` # 一、如何初始化软总线 调用软总线模块并不复杂,需要准备2个结构体就可以召唤神龙,简称StartBus()。以下我们以Hi3861为例写一个简单的例子。 ```c static void InitSoftbus(void) {printf(">>>>>%s:%d:%s()\n",__FILE__,__LINE__,__FUNCTION__); static PublishInfo g_publishInfo = { .capabilityData = (unsigned char *)"1", .capability = "ddmpCapability", .dataLen = 1, .publishId = 1, .mode = DISCOVER_MODE_ACTIVE, .medium = COAP, .freq = MID, }; static IPublishCallback g_publishCallback = { .onPublishSuccess = OnSuccess, .onPublishFail = OnFail, }; int ret = PublishService(g_demoModuleName, &g_publishInfo, &g_publishCallback); if (ret != 0) { printf("PublishService err\n"); } sleep(5); ret = CreateSessionServer(g_demoModuleName, g_demoSessionName, &g_sessionCallback); if (ret != 0) { printf("CreateSessionServer err\n"); } printf("InitSoftbus ok\n"); } ``` # 一、被发现端,发布服务。【discovery目录】 在分布式软总线子系统中,设备分为发现端和被发现端。这里我们使用手机作为发现端,Hi3861开发板发布服务,作为被发现端。 发布服务主要是通过PublishService()这个函数,相关的代码在:主目录\foundation\communication\softbus_lite\discovery_service文件夹下。 如果设备要发布软总线服务,就需要调用下面这个函数。 ```c //moduleName:调用模块名 //info:PublishInfo结构体 //cb:发布成功或者失败的回调函数 int PublishService(const char *moduleName, const struct PublishInfo *info, const struct IPublishCallback *cb) {printf(">>>>>%s:%d:%s()[soft bus init start]\n",__FILE__,__LINE__,__FUNCTION__); //许可检查,首先将检查是否有发布权限。这里调用的os_adapter【操作系统适配层】相关部分。 if (SoftBusCheckPermission(SOFTBUS_PERMISSION) != 0 || info == NULL || cb == NULL) { SOFTBUS_PRINT("[DISCOVERY] PublishService invalid para(info or cb)\n"); return ERROR_INVALID; } //参数有效性检查,检查了一些发布参数的合法性,其次确认发布协议是不是COAP。 if (moduleName == NULL || strlen(moduleName) >= MAX_PACKAGE_NAME || info->publishId <= 0 || info->dataLen > MAX_CAPABILITY_DATA_LEN) { SOFTBUS_PRINT("[DISCOVERY] PublishService invliad para\n"); PublishCallback(info->publishId, PUBLISH_FAIL_REASON_PARAMETER_INVALID, NULL, cb); return ERROR_INVALID; }//如果参数检查失败,调用PublishCallback()来回调cb里面的失败回调函数 if (info->medium != COAP) { PublishCallback(info->publishId, PUBLISH_FAIL_REASON_NOT_SUPPORT_MEDIUM, NULL, cb); return ERROR_INVALID; } //用g_discoveryMutex防止多个设备对外发布服务产生的冲突 if (g_discoveryMutex == NULL) { g_discoveryMutex = MutexInit(); if (g_discoveryMutex == NULL) { PublishCallback(info->publishId, PUBLISH_FAIL_REASON_UNKNOWN, NULL, cb); return ERROR_FAIL; } } MutexLock(g_discoveryMutex);//解除互斥锁 //InitService()这个函数比较重要。参考1.初始化InitService()发现服务 if (InitService() != ERROR_SUCCESS) { SOFTBUS_PRINT("[DISCOVERY] PublishService InitService fail\n"); PublishCallback(info->publishId, PUBLISH_FAIL_REASON_UNKNOWN, NULL, cb); MutexUnlock(g_discoveryMutex); return ERROR_FAIL; } //AddPublishModule()函数,参考2.模块增加到g_publishModule PublishModule *findModule = AddPublishModule(moduleName, info); if (findModule == NULL) { SOFTBUS_PRINT("[DISCOVERY] PublishService AddPublishModule fail\n"); PublishCallback(info->publishId, PUBLISH_FAIL_REASON_UNKNOWN, NULL, cb); MutexUnlock(g_discoveryMutex); return ERROR_FAIL; } //注册COAP服务,参考3.CoapRegisterDefualtService() int ret = ERROR_SUCCESS; if (info->capability == NULL || info->capabilityData == NULL) { (void)CoapRegisterDefualtService(); } else { ret = DoRegistService(info->medium); } MutexUnlock(g_discoveryMutex); //这个PublishCallback()就很简单了,上面出现好多次了,初始化成功或者失败的回调函数。 if (ret != ERROR_SUCCESS) { PublishCallback(info->publishId, PUBLISH_FAIL_REASON_UNKNOWN, findModule, cb); return ERROR_FAIL; } else { PublishCallback(info->publishId, ERROR_SUCCESS, findModule, cb); return ERROR_SUCCESS; } } ``` ## 1.初始化InitService()发现服务 ```c Z:\harmony110\foundation\communication\softbus_lite\discovery\discovery_service\source\discovery_service.c int InitService(void) { if (g_isServiceInit != 0) {//全局变量g_isServiceInit:确认服务是否启动。如果其他模块启动过,那么直接返回成功。 return ERROR_SUCCESS; } //初始化g_deviceInfo结构体。参考1.1.初始化g_deviceInfo结构体,相关文件【common_info_manager.c】 if (InitCommonManager() != 0) { DeinitService(); return ERROR_FAIL; } //分配内存,参考:1.2.初始化全局g_publishModule和g_capabilityData g_publishModule = calloc(1, sizeof(PublishModule) * MAX_MODULE_COUNT); if (g_publishModule == NULL) { DeinitService(); return ERROR_NOMEMORY; } g_capabilityData = calloc(1, MAX_SERVICE_DATA_LEN); if (g_capabilityData == NULL) { DeinitService(); return ERROR_NOMEMORY; } //软总线注册第一个监听函数WifiEventTrigger(),当接入网络时trigger //注册wifi Callback,将WifiEventTrigger()↓函数,赋值给全局变量g_wifiCallback,参考b.COAP初始化wifi事件 RegisterWifiCallback(WifiEventTrigger); //COAP初始化。参考1.3.初始化COAP协议服务,相关文件【coap_service.c】 int ret = CoapInit(); if (ret != ERROR_SUCCESS) { SOFTBUS_PRINT("[DISCOVERY] InitService CoapInit fail\n"); DeinitService(); return ret; }printf(">>>>>%s:%d[Register WifiEventTring()]\n",__FILE__,__LINE__);//添加打印调试 //COAP写入消息队列,这样会触发上面↑消息回调函数,WifiEventTrigger(),这个函数比较重要,TODO 1.4. CoapWriteMsgQueue(UPDATE_IP_EVENT); //COAP注册设备信息 ret = CoapRegisterDeviceInfo(); if (ret != ERROR_SUCCESS) { SOFTBUS_PRINT("[DISCOVERY] InitService CoapRegisterDeviceInfo fail\n"); DeinitService(); return ret; } g_isServiceInit = 1; SOFTBUS_PRINT("[DISCOVERY] InitService ok\n"); return ERROR_SUCCESS; } ``` ### 1.1.初始化g_deviceInfo结构体 ```c Z:\harmony110\foundation\communication\softbus_lite\discovery\discovery_service\source\common_info_manager.c int InitCommonManager(void) { if (InitLocalDeviceInfo() != 0) { //初始化本地设备信息 SOFTBUS_PRINT("[DISCOVERY] InitCommonManager fail\n"); return ERROR_FAIL; } return ERROR_SUCCESS; } ================================================================================================ int InitLocalDeviceInfo(void) //初始化本地设备信息 { char deviceId[DEVICEID_MAX_NUM] = {0}; if (g_deviceInfo != NULL) { //初始化一个g_deviceInfo的结构体。包含本地设备的各种信息。 memset_s(g_deviceInfo, sizeof(DeviceInfo), 0, sizeof(DeviceInfo)); } else { g_deviceInfo = (DeviceInfo *)calloc(1, sizeof(DeviceInfo)); if (g_deviceInfo == NULL) { return ERROR_FAIL; } } g_deviceInfo->devicePort = -1; //默认,g_deviceInfo.设备端口-1 g_deviceInfo->isAccountTrusted = 1; //默认,g_deviceInfo.账户是可信的 unsigned int ret; //从文件获取设备id,这个函数还有好几层,就不继续深挖了。大体就是从DEVICE_ID_FILE文件中读取。 //#define DEVICE_ID_FILE "/storage/data/softbus/deviceid" ret = GetDeviceIdFromFile(deviceId, MAX_VALUE_SIZE); if (ret != ERROR_SUCCESS) { SOFTBUS_PRINT("[DISCOVERY] Get device fail\n"); return ERROR_FAIL; } #if defined(__LITEOS_M__) || defined(__LITEOS_RISCV__) //g_deviceInfo.设备的类型,目前分为L0和L1 g_deviceInfo->deviceType = L0; ret = (unsigned int)strcpy_s(g_deviceInfo->deviceName, sizeof(g_deviceInfo->deviceName), L0_DEVICE_NAME); #else g_deviceInfo->deviceType = L1; ret = (unsigned int)strcpy_s(g_deviceInfo->deviceName, sizeof(g_deviceInfo->deviceName), L1_DEVICE_NAME); #endif ret |= (unsigned int)strcpy_s(g_deviceInfo->deviceId, sizeof(g_deviceInfo->deviceId), deviceId);//g_deviceInfo.设备id ret |= (unsigned int)strcpy_s(g_deviceInfo->version, sizeof(g_deviceInfo->version), "1.0.0");//g_deviceInfo.版本号 if (ret != 0) { return ERROR_FAIL; } SOFTBUS_PRINT("[DISCOVERY] InitLocalDeviceInfo ok\n"); return ERROR_SUCCESS; } ``` ### 1.2.初始化全局g_publishModule和g_capabilityData ```c Z:\harmony110\foundation\communication\softbus_lite\discovery\discovery_service\source\discovery_service.c //首先,discovery_service.c开头定义了这两个全局变量。 PublishModule *g_publishModule = NULL; //存放发布的模块 char *g_capabilityData = NULL; //能力描述数据 typedef struct { char package[MAX_PACKAGE_NAME]; int publishId; unsigned short medium; unsigned short capabilityBitmap; char *capabilityData; unsigned short dataLength; unsigned short used; } PublishModule;//发布模块结构体 //初始化InitService()发现服务后,会将模块注册到全局变量g_publishModule中,服务注册到g_capabilityData中。 ``` ### 1.3.初始化COAP协议服务 ```c Z:\harmony110\foundation\communication\softbus_lite\discovery\discovery_service\source\coap_service.c int CoapInit(void) { int ret = NSTACKX_Init(); if (ret != 0) { SOFTBUS_PRINT("[DISCOVERY] CoapInit NSTACKX_Init fail\n"); return ERROR_FAIL; } return ERROR_SUCCESS; } ================================================================================================ Z:\harmony110\foundation\communication\softbus_lite\discovery\coap\source\nstackx_common.c int NSTACKX_Init() { int ret; if (g_nstackInitState != NSTACKX_INIT_STATE_START) { return NSTACKX_EOK; } g_nstackInitState = NSTACKX_INIT_STATE_ONGOING; cJSON_InitHooks(NULL); ret = CoapInitDiscovery(); //启动COAP端口监听 if (ret != NSTACKX_EOK) { goto L_ERR_INIT; } g_nstackInitState = NSTACKX_INIT_STATE_DONE; return NSTACKX_EOK; L_ERR_INIT: ret = NSTACKX_Deinit(); if (ret != NSTACKX_EOK) { SOFTBUS_PRINT("[DISCOVERY] deinit fail\n"); } return NSTACKX_EFAILED; } ================================================================================================ Z:\harmony110\foundation\communication\softbus_lite\discovery\coap\source\coap_discover.c int CoapInitDiscovery(void) { int ret = CoapInitSocket(); //参考a.COAP初始化Socket //启动监听端口 if (ret != NSTACKX_EOK) { SOFTBUS_PRINT("[DISCOVERY] Init socket fail\n"); return ret; } ret = CoapInitWifiEvent(); //参考b.COAP初始化wifi事件 if (ret != NSTACKX_EOK) { SOFTBUS_PRINT("[DISCOVERY] Init wifi event fail\n"); return ret; } #if defined(__LITEOS_A__) ret = CreateQueryIpThread(); if (ret != NSTACKX_EOK) { SOFTBUS_PRINT("[DISCOVERY] Init query Ip fail\n"); return ret; } #endif if (CreateMsgQueThread() != NSTACKX_EOK) { return NSTACKX_EFAILED; } return CreateCoapListenThread(); //参考c.创建COAP监听线程 } ================================================================================================ ``` #### a.COAP初始化Socket ```c //这个函数跳转到了coap【目录主要是负责COAP协议的部分】 Z:\harmony110\foundation\communication\softbus_lite\discovery\coap\source\coap_socket.c int CoapInitSocket(void) { if (g_serverFd >= 0) { return NSTACKX_EOK; } struct sockaddr_in sockAddr; (void)memset_s(&sockAddr, sizeof(sockAddr), 0, sizeof(sockAddr)); sockAddr.sin_port = htons(COAP_DEFAULT_PORT); g_serverFd = CoapCreateUdpServer(&sockAddr); //创建一个udp服务器,这个socket赋值给g_serverFd if (g_serverFd < 0) { return NSTACKX_OVERFLOW; } COAP_SoftBusInitMsgId(); return NSTACKX_EOK; } ================================================================================================ int CoapCreateUdpServer(const struct sockaddr_in *sockAddr) //创建一个udp服务器 { if (sockAddr == NULL) { return NSTACKX_EINVAL; } struct sockaddr_in localAddr; socklen_t len = sizeof(localAddr); int sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd < 0) { return NSTACKX_OVERFLOW; } (void)memset_s(&localAddr, sizeof(localAddr), 0, sizeof(localAddr)); localAddr.sin_family = AF_INET; //结构体sockaddr_in.sin_family = 2 localAddr.sin_port = sockAddr->sin_port; //结构体sockaddr_in.sin_port = 5684,定义在coap_socket.h if (sockAddr->sin_addr.s_addr != 0) { localAddr.sin_addr.s_addr = sockAddr->sin_addr.s_addr; //测试发现这里执行的是else } else { localAddr.sin_addr.s_addr = htonl(INADDR_ANY); //结构体sockaddr_in.sin_addr.s_addr = 0 打印测试结果是0 } if (bind(sockfd, (struct sockaddr *)&localAddr, len) == -1) { //创建一个socket,然后用bind()绑定到指定的IP+PORT CloseSocket(&sockfd); return NSTACKX_EFAILED; } if (getsockname(sockfd, (struct sockaddr *)&localAddr, &len) == -1) { //获取套接字名称 CloseSocket(&sockfd); return NSTACKX_EFAILED; } return sockfd; } ``` #### b.COAP初始化wifi事件 ```c //基本原理就是,像wifi_lite注册事件回调函数 //最终调用调用WifiEventTrigger()函数 Z:\harmony110\foundation\communication\softbus_lite\discovery\coap\source\coap_discover.c int CoapInitWifiEvent(void) { SOFTBUS_PRINT("[DISCOVERY] CoapInitWifiEvent\n"); unsigned int ret; if (g_wifiQueueId == -1) { ret = CreateMsgQue("/wifiQue", //创建一个消息队列 WIFI_QUEUE_SIZE, (unsigned int*)&g_wifiQueueId, 0, sizeof(AddressEventHandler)); if (ret != 0) { SOFTBUS_PRINT("[DISCOVERY]CreateMsgQue fail\n"); (void)CoapDeinitWifiEvent(); return ret; } #if defined(__LITEOS_M__) || defined(__LITEOS_RISCV__) g_coapEventHandler.OnWifiConnectionChanged = CoapConnectionChangedHandler;//参考下面解析 WifiErrorCode error = RegisterWifiEvent(&g_coapEventHandler); //注册wifi事件,全局变量g_coapEventHandler if (error != WIFI_SUCCESS) { SOFTBUS_PRINT("[DISCOVERY]RegisterWifiEvent fail, error:%d\n", error); (void)CoapDeinitWifiEvent(); g_wifiQueueId = -1; return error; } #endif } return NSTACKX_EOK; } ================================================================================================ #if defined(__LITEOS_M__) || defined(__LITEOS_RISCV__) static void CoapConnectionChangedHandler(int state, WifiLinkedInfo* info) { (void)info; CoapWriteMsgQueue(state);//参考下面解析 } ================================================================================================ void CoapWriteMsgQueue(int state) { SOFTBUS_PRINT("[DISCOVERY] CoapWriteMsgQueue\n"); AddressEventHandler handler; handler.handler = CoapHandleWifiEvent;//参考下面解析 handler.state = state; /* while a new event coming, it must stop the previous loop 当一个新的事件出现时,它必须停止之前的循环 */ g_queryIpFlag = 0; (void)WriteMsgQue(g_wifiQueueId, &handler, sizeof(AddressEventHandler)); } ================================================================================================ void CoapHandleWifiEvent(unsigned int para) { if (g_wifiCallback != NULL) { //g_wifiCallback在int InitService(void){RegisterWifiCallback(WifiEventTrigger);} g_wifiCallback(para); //WifiEventTrigger() } } ``` #### c.创建COAP监听线程, HandleReadEvent() ```c //创建了CoapReadHandle线程,处理COAP_DEFAULT_PORT端口上的UDP socket的数据(也就是基于COAP协议的discover广播消息) Z:\harmony110\foundation\communication\softbus_lite\discovery\coap\source\coap_discover.c int CreateCoapListenThread(void) { g_terminalFlag = 1; #if defined(__LITEOS_M__) || defined(__LITEOS_RISCV__) if (g_coapTaskId != NULL) { return NSTACKX_EOK; } osThreadAttr_t attr; //创建一个osThreadAttr_t结构体 attr.name = "coap_listen_task"; attr.attr_bits = 0U; attr.cb_mem = NULL; attr.cb_size = 0U; attr.stack_mem = NULL; attr.stack_size = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE; attr.priority = osPriorityNormal4; // COAP_DEFAULT_PRIO -> cmsis prio //新建一个系统线程,全局变量g_coapTaskId g_coapTaskId = osThreadNew((osThreadFunc_t)CoapReadHandle, NULL, &attr); //参考下面解析CoapReadHandle if (g_coapTaskId == NULL) { g_terminalFlag = 0; SOFTBUS_PRINT("[DISCOVERY] create task fail\n"); return NSTACKX_EFAILED; } #else if (g_coapTaskId != -1) { return NSTACKX_EOK; } ThreadAttr attr = {"coap_listen_task", 0x800, 20, 0, 0}; int error = CreateThread((Runnable)CoapReadHandle, NULL, &attr, (unsigned int *)&g_coapTaskId); if (error != 0) { g_terminalFlag = 0; SOFTBUS_PRINT("[DISCOVERY] create task fail\n"); return NSTACKX_EFAILED; } #endif return NSTACKX_EOK; } ================================================================================================ #define TIME_MICRO_SEC 10000 static void CoapReadHandle(unsigned int uwParam1, unsigned int uwParam2, unsigned int uwParam3, unsigned int uwParam4) { (void)uwParam1; (void)uwParam2; (void)uwParam3; (void)uwParam4; int ret; fd_set readSet; int serverFd = GetCoapServerSocket(); SOFTBUS_PRINT("[DISCOVERY] CoapReadHandle coin select begin\n"); while (g_terminalFlag) { FD_ZERO(&readSet); FD_SET(serverFd, &readSet); //通过调用select实现了异步通讯 ret = select(serverFd + 1, &readSet, NULL, NULL, NULL); if (ret > 0) { if (FD_ISSET(serverFd, &readSet)) { //在端口监听到包是通过HandleReadEvent函数来处理,在这个函数中对收到的数据包进行处理 HandleReadEvent(serverFd);//参考【三、当发现端(比如手机)发送广播】 } } else { SOFTBUS_PRINT("[DISCOVERY]ret:%d,error:%d\n", ret, errno); } } SOFTBUS_PRINT("[DISCOVERY] CoapReadHandle exit\n"); } ``` ## 2.模块增加到g_publishModule ```c //g_publishModule是啥,要参考上面解析过的:1.2.初始化全局g_publishModule和g_capabilityData //传入参数有PublishInfo *info,g_publishModule大部分内容继承PublishInfo //而PublishInfo结构体的内容,是最早调用PublishService()就需要提供的参数 //通俗讲就是将加入软总线的模块名和它的信息,添加到系统g_publishModule中。 PublishModule *AddPublishModule(const char *packageName, const PublishInfo *info) { //验证参数 if (packageName == NULL || g_publishModule == NULL || info == NULL) { return NULL; } if (info->dataLen > MAX_SERVICE_DATA_LEN) { return NULL; } if (FindExistModule(packageName, info->publishId) != NULL) { return NULL; } if (FindFreeModule() == NULL) { return NULL; } // int ret; for (int i = 0; i < MAX_MODULE_COUNT; i++) { //#define MAX_MODULE_COUNT 3 if (g_publishModule[i].used == 1) { continue; } if (ParseCapability(info->capability, &g_publishModule[i].capabilityBitmap)) { return NULL; } g_publishModule[i].used = 1; g_publishModule[i].capabilityData = calloc(1, info->dataLen + 1); if (g_publishModule[i].capabilityData == NULL) { memset_s(&g_publishModule[i], sizeof(g_publishModule[i]), 0, sizeof(g_publishModule[i])); return NULL; } g_publishModule[i].dataLength = info->dataLen + 1; ret = memcpy_s(g_publishModule[i].capabilityData, g_publishModule[i].dataLength, info->capabilityData, info->dataLen); if (ret != 0) { free(g_publishModule[i].capabilityData); g_publishModule[i].capabilityData = NULL; memset_s(&g_publishModule[i], sizeof(g_publishModule[i]), 0, sizeof(g_publishModule[i])); return NULL; } g_publishModule[i].medium = info->medium; g_publishModule[i].publishId = info->publishId; ret = memcpy_s(g_publishModule[i].package, MAX_PACKAGE_NAME, packageName, strlen(packageName)); if (ret != 0) { free(g_publishModule[i].capabilityData); g_publishModule[i].capabilityData = NULL; memset_s(&g_publishModule[i], sizeof(g_publishModule[i]), 0, sizeof(g_publishModule[i])); return NULL; } return &g_publishModule[i]; } return NULL; } ``` ## 3.CoapRegisterDefualtService() ```c Z:\harmony110\foundation\communication\softbus_lite\discovery\discovery_service\source\coap_service.c int CoapRegisterDefualtService(void) { DeviceInfo *info = GetCommonDeviceInfo(); //info继承全局变量g_deviceInfo结构体 if (info == NULL) { return ERROR_FAIL; } char serviceData[MAX_DEFAULT_SERVICE_DATA_LEN] = {0}; if (sprintf_s(serviceData, sizeof(serviceData), "port:%d", info->devicePort) == -1) { return ERROR_FAIL; } return NSTACKX_RegisterServiceData(serviceData); } ============================================================================== Z:\harmony110\foundation\communication\softbus_lite\discovery\coap\source\nstackx_common.c int NSTACKX_RegisterServiceData(const char* serviceData) { if (serviceData == NULL) { return NSTACKX_EINVAL; } if (g_nstackInitState != NSTACKX_INIT_STATE_DONE) { return NSTACKX_EFAILED; } unsigned int serviceLen = strlen(serviceData); if (serviceLen >= NSTACKX_MAX_SERVICE_DATA_LEN) { return NSTACKX_EINVAL; } if (RegisterServiceData(serviceData, serviceLen + 1) != NSTACKX_EOK) { return NSTACKX_EINVAL; } return NSTACKX_EOK; } ============================================================================== Z:\harmony110\foundation\communication\softbus_lite\discovery\coap\source\nstackx_device.c int RegisterServiceData(const char* serviceData, int length) { if (serviceData == NULL) { return NSTACKX_EINVAL; } (void)memset_s(g_localDeviceInfo.serviceData, sizeof(g_localDeviceInfo.serviceData), 0, sizeof(g_localDeviceInfo.serviceData)); if (strcpy_s(g_localDeviceInfo.serviceData, NSTACKX_MAX_SERVICE_DATA_LEN, serviceData) != EOK) { return NSTACKX_EFAILED; } (void)length; return NSTACKX_EOK; } ============================================================================== Z:\harmony110\foundation\communication\softbus_lite\discovery\coap\source\nstackx_device.c int RegisterServiceData(const char* serviceData, int length) { if (serviceData == NULL) { return NSTACKX_EINVAL; } (void)memset_s(g_localDeviceInfo.serviceData, sizeof(g_localDeviceInfo.serviceData), 0, sizeof(g_localDeviceInfo.serviceData)); if (strcpy_s(g_localDeviceInfo.serviceData, NSTACKX_MAX_SERVICE_DATA_LEN, serviceData) != EOK) { return NSTACKX_EFAILED; } (void)length; return NSTACKX_EOK; } ``` ## 总结: 通过模块信息注册发布服务后,系统会产生2个线程,1个监听网络,另一个监听局域网内UDP的发现端请求。 接入网络执行【二、当接入网络,触发WifiEventTrigger(),开启软总线】 监听到UDP广播【三、当发现端(比如手机)发送广播】 文章相关附件可以点击下面的原文链接前往下载 原文链接:https://harmonyos.51cto.com/posts/6295#bkwz

优秀的个人博客,低调大师

v87.01 鸿蒙内核源码分析 (内核启动篇) | 从汇编到main() | 百篇博客分析 OpenHarmony 源码

本篇关键词:内核重定位、MMU、SVC栈、热启动、内核映射表 内核汇编相关篇为: v74.01 鸿蒙内核源码分析(编码方式) | 机器指令是如何编码的 v75.03 鸿蒙内核源码分析(汇编基础) | CPU上班也要打卡 v76.04 鸿蒙内核源码分析(汇编传参) | 如何传递复杂的参数 v77.01 鸿蒙内核源码分析(链接脚本) | 正在制作中 ... v78.01 鸿蒙内核源码分析(内核启动) | 从汇编到main() v79.01 鸿蒙内核源码分析(进程切换) | 正在制作中 ... v80.03 鸿蒙内核源码分析(任务切换) | 看汇编如何切换任务 v81.05 鸿蒙内核源码分析(中断切换) | 系统因中断活力四射 v82.06 鸿蒙内核源码分析(异常接管) | 社会很单纯 复杂的是人 v83.01 鸿蒙内核源码分析(缺页中断) | 正在制作中 ... 这应该是系列篇最难写的一篇,全是汇编代码,需大量的底层知识,涉及协处理器,内核镜像重定位,创建内核映射表,初始化 CPU 模式栈,热启动,到最后熟悉的 main() 。 内核入口 在链接文件 liteos.ld 中可知内核的入口地址为 ENTRY(reset_vector) , 分别出现在reset_vector_mp.S (多核启动) 和 reset_vector_up.S(单核启动),系列篇研究多核启动的情况。代码可结合 (协处理器篇) 看更容易懂。 reset_vector: //鸿蒙开机代码 /* clear register TPIDRPRW */ mov r0, #0 //r0 = 0 mcr p15, 0, r0, c13, c0, 4 //复位线程标识符寄存器TPIDRPRW , 不复位将导致系统不能启动 /* do some early cpu setup: i/d cache disable, mmu disabled */ mrc p15, 0, r0, c1, c0, 0 //System Control Register-SCTLR | 读取系统控制寄存器内容 bic r0, #(1<<12) //禁用指令缓存功能 bic r0, #(1<<2 | 1<<0) //禁用数据和TLB的缓存功能(bit2) | mmu功能(bit0) mcr p15, 0, r0, c1, c0, 0 //写系统控制寄存器 /* enable fpu+neon 一些系统寄存器的操作 | 使能浮点运算(floating point unit)和 NEON就是一种基于SIMD思想的ARM技术,相比于ARMv6或之前的架构, NEON结合了64-bit和128-bit的SIMD指令集,提供128-bit宽的向量运算(vector operations)*/ #ifndef LOSCFG_TEE_ENABLE //Trusted Execution Environment 可信执行环境 MRC p15, 0, r0, c1, c1, 2 //非安全模式访问寄存器 (Non-Secure Access Control Register - NSACR) ORR r0, r0, #0xC00 //使能安全和非安全访问协处理器10和11(Coprocessor 10和11) BIC r0, r0, #0xC000 //设置bit15为0,不会影响修改CPACR.ASEDIS寄存器位(控制Advanced SIMD功能)| bit14 reserved MCR p15, 0, r0, c1, c1, 2 LDR r0, =(0xF << 20) //允许在EL0和EL1下,访问协处理器10和11(控制Floating-point和Advanced SIMD特性) MCR p15, 0, r0, c1, c0, 2 ISB #endif MOV r3, #0x40000000 //EN, bit[30] 设置FPEXC的EN位来使能FPU VMSR FPEXC, r3 //浮点异常控制寄存器 (Floating-Point Exception Control register | B4.1.57) /* r11: delta of physical address and virtual address | 计算虚拟地址和物理地址之间的差值,目的是为了建立映射关系表 */ adr r11, pa_va_offset //获取pa_va_offset变量物理地址,由于这时候mmu已经被关闭,所以这个值就表示pa_va_offset变量的物理地址。 /*adr 是一条小范围的地址读取伪指令,它将基于PC的相对偏移的地址值读到目标寄存器中。 *编译源程序时,汇编器首先计算当前PC值(当前指令位置)到exper的距离,然后用一条ADD或者SUB指令替换这条伪指令, *例如:ADD register,PC,#offset_to_exper 注意,标号exper与指令必须在同一代码段 */ ldr r0, [r11] //r0 = *r11 获取pa_va_offset变量虚拟地址 sub r11, r11, r0 //物理地址-虚拟地址 = 映射偏移量 放入r11 mrc p15, 0, r12, c0, c0, 5 /* Multiprocessor Affinity Register-MPIDR */ and r12, r12, #MPIDR_CPUID_MASK //掩码过滤 cmp r12, #0 //主控核0判断 bne secondary_cpu_init //初始化CPU次核 /* * adr是小范围的地址读取伪指令,它将基于PC寄存器相对偏移的地址值读取到寄存器中, * 例如: 0x00000004 : adr r4, __exception_handlers * 则此时PC寄存器的值为: 0x00000004 + 8(在三级流水线时,PC和执行地址相差8), * adr指令和标识__exception_handlers的地址相对固定,二者偏移量若为offset, * 最后r4 = (0x00000004 + 8) + offset */ /* if we need to relocate to proper location or not | 如果需要重新安装到合适的位置*/ adr r4, __exception_handlers /* r4: base of load address | 加载基址*/ ldr r5, =SYS_MEM_BASE /* r5: base of physical address | 物理基址*/ subs r12, r4, r5 /* r12: delta of load address and physical address | 二者偏移量*/ beq reloc_img_to_bottom_done /* if we load image at the bottom of physical address | 不相等就需要重定位 */ /* we need to relocate image at the bottom of physical address | 需要知道拷贝的大小*/ ldr r7, =__exception_handlers /* r7: base of linked address (or vm address) | 链接地址基地址*/ ldr r6, =__bss_start /* r6: end of linked address (or vm address),由于目前阶段有用的数据是中断向量表+代码段+只读数据段+数据段, 所以只需复制[__exception_handlers,__bss_start]这段数据到内存基址处 */ sub r6, r7 /* r6: delta of linked address (or vm address) | 内核镜像大小 */ add r6, r4 /* r6: end of load address | 说明需拷贝[ r4,r4+r6 ] 区间内容到 [ r5,r5+r6 ]*/ reloc_img_to_bottom_loop://重定位镜像到内核物理内存基地址,将内核从加载地址拷贝到内存基址处 ldr r7, [r4], #4 // 类似C语言 *r5 = *r4 , r4++ , r5++ str r7, [r5], #4 // #4 代表32位的指令长度,此时在拷贝内核代码区内容 cmp r4, r6 /* 拷贝完成条件. r4++ 直到等于r6 (加载结束地址) 完成拷贝动作 */ bne reloc_img_to_bottom_loop sub pc, r12 /* 重新校准pc寄存器, 无缝跳到了拷贝后的指令地址处执行 r12是重定位镜像前内核加载基地址和内核物理内存基地址的差值 */ nop // 注意执行完成sub pc, r12后,新的PC寄存器也指向了 nop ,nop是伪汇编指令,等同于 mov r0 r0 通常用于控制时序的目的,强制内存对齐,防止流水线灾难,占据分支指令延迟 sub r11, r11, r12 /* r11: eventual address offset | 最终地址映射偏移量, 用于构建MMU页表 */ //内核总大小 __bss_start - __exception_handlers reloc_img_to_bottom_done: #ifdef LOSCFG_KERNEL_MMU ldr r4, =g_firstPageTable /* r4: physical address of translation table and clear it 内核页表是用数组g_firstPageTable存储 见于los_arch_mmu.c */ add r4, r4, r11 //计算g_firstPageTable页表物理地址 mov r0, r4 //因为默认r0 将作为memset_optimized的第一个参数 mov r1, #0 //第二个参数,清0 mov r2, #MMU_DESCRIPTOR_L1_SMALL_ENTRY_NUMBERS //第三个参数是L1表的长度 bl memset_optimized /* optimized memset since r0 is 64-byte aligned | 将内核页表空间清零*/ ldr r5, =g_archMmuInitMapping //记录映射关系表 add r5, r5, r11 //获取g_archMmuInitMapping的物理地址 init_mmu_loop: //初始化内核页表 ldmia r5!, {r6-r10} /* r6 = phys, r7 = virt, r8 = size, r9 = mmu_flags, r10 = name | 传参: 物理地址、虚拟地址、映射大小、映射属性、名称*/ cmp r8, 0 /* if size = 0, the mmu init done | 完成条件 */ beq init_mmu_done //标志寄存器中Z标志位等于零时跳转到 init_mmu_done处执行 bl page_table_build //创建页表 b init_mmu_loop //循环继续 init_mmu_done: orr r8, r4, #MMU_TTBRx_FLAGS /* r8 = r4 and set cacheable attributes on translation walk | 设置缓存*/ ldr r4, =g_mmuJumpPageTable /* r4: jump pagetable vaddr | 页表虚拟地址*/ add r4, r4, r11 ldr r4, [r4] add r4, r4, r11 /* r4: jump pagetable paddr | 页表物理地址*/ /* build 1M section mapping, in order to jump va during turing on mmu:pa == pa, va == pa */ /* 从当前PC开始建立1MB空间的段映射,分别建立物理地址和虚拟地址方式的段映射页表项 * 内核临时页表在系统 使能mmu -> 切换到虚拟地址运行 这段时间使用 */ mov r6, pc mov r7, r6 /* r7: pa (MB aligned)*/ lsr r6, r6, #20 /* r6: pa l1 index */ ldr r10, =MMU_DESCRIPTOR_KERNEL_L1_PTE_FLAGS add r12, r10, r6, lsl #20 /* r12: pa |flags */ str r12, [r4, r7, lsr #(20 - 2)] /* jumpTable[paIndex] = pt entry */ rsb r7, r11, r6, lsl #20 /* r7: va */ str r12, [r4, r7, lsr #(20 - 2)] /* jumpTable[vaIndex] = pt entry */ bl mmu_setup /* set up the mmu | 内核映射表已经创建好了,此时可以启动MMU工作了*/ #endif /* clear out the interrupt and exception stack and set magic num to check the overflow |exc_stack|地址高位 |svc_stack|地址低位 清除中断和异常堆栈并设置magic num检查溢出 */ ldr r0, =__svc_stack //stack_init的第一个参数 __svc_stack表示栈顶 ldr r1, =__exc_stack_top //stack_init的第二个参数 __exc_stack_top表示栈底, 这里会有点绕, top表高地址位 bl stack_init //初始化各个cpu不同模式下的栈空间 //设置各个栈顶魔法数字 STACK_MAGIC_SET __svc_stack, #OS_EXC_SVC_STACK_SIZE, OS_STACK_MAGIC_WORD //中断栈底设成"烫烫烫烫烫烫" STACK_MAGIC_SET __exc_stack, #OS_EXC_STACK_SIZE, OS_STACK_MAGIC_WORD //异常栈底设成"烫烫烫烫烫烫" warm_reset: //热启动 Warm Reset, warm reboot, soft reboot, 在不关闭电源的情况,由软件控制重启计算机 /* initialize CPSR (machine state register) */ mov r0, #(CPSR_IRQ_DISABLE|CPSR_FIQ_DISABLE|CPSR_SVC_MODE) /* 禁止IRQ中断 | 禁止FIQ中断 | 管理模式-操作系统使用的保护模式 */ msr cpsr, r0 //设置CPSR寄存器 /* Note: some functions in LIBGCC1 will cause a "restore from SPSR"!! */ msr spsr, r0 //设置SPSR寄存器 /* get cpuid and keep it in r12 */ mrc p15, 0, r12, c0, c0, 5 //R12保存CPUID and r12, r12, #MPIDR_CPUID_MASK //掩码操作获取当前cpu id /* set svc stack, every cpu has OS_EXC_SVC_STACK_SIZE stack | 设置 SVC栈 */ ldr r0, =__svc_stack_top //注意这是栈底,高地址位 mov r2, #OS_EXC_SVC_STACK_SIZE //栈大小 mul r2, r2, r12 sub r0, r0, r2 /* 算出当前core的中断栈栈顶位置,写入所属core的sp */ mov sp, r0 LDR r0, =__exception_handlers MCR p15, 0, r0, c12, c0, 0 /* Vector Base Address Register - VBAR */ cmp r12, #0 //CPU是否为主核 bne cpu_start //不相等就跳到从核处理分支 clear_bss: //主核处理.bss段清零 ldr r0, =__bss_start ldr r2, =__bss_end mov r1, #0 sub r2, r2, r0 bl memset #if defined(LOSCFG_CC_STACKPROTECTOR_ALL) || \ defined(LOSCFG_CC_STACKPROTECTOR_STRONG) || \ defined(LOSCFG_CC_STACKPROTECTOR) bl __stack_chk_guard_setup #endif #ifdef LOSCFG_GDB_DEBUG /* GDB_START - generate a compiled_breadk,This function will get GDB stubs started, with a proper environment */ bl GDB_START .word 0xe7ffdeff #endif bl main //带LR的子程序跳转, LR = pc - 4, 执行C层main函数 解读 第一步: 操作 CP15 协处理器 TPIDRPRW 寄存器,它被 ARM 设计保存当前运行线程的 ID值,在ARMv7 架构中才新出现,需PL1权限以上才能访问,而硬件不会从内部去改变它的值,也就是说这是一个直接暴露给工程师操作维护的一个寄存器,在鸿蒙内核中被用于记录线程结构体的开始地址,可以搜索 OsCurrTaskSet 来跟踪哪些地方会切换当前任务以便更好的理解内核。 第二步: 系统控制寄存器(SCTLR),B4.1.130 SCTLR, System Control Register 它提供了系统的最高级别控制,高到了玉皇大帝级别,代码中将 0、2、12位写 0。对应关闭 MMU 、数据缓存 、指令缓存 功能。 第三步: 对浮点运算FPU的设置,在安全模式下使用FPU,须定义NSACR、CPACR、FPEXC 三个寄存器 第四步: 计算虚拟地址和物理地址的偏移量,为何要计算它呢 ? 主要目的是为了建立虚拟地址和物理地址的映射关系,因为在 MMU启动之后,运行地址(PC寄存器指向的地址)将变成虚拟地址,使用虚拟地址就离不开映射表,所以两个地址的映射关系需要在MMU启动前就创建好,而有了偏移量就可以创建映射表。但需先搞清楚 链接地址 和 运行地址 两个概念。 链接地址 由链接器确定,链接器会将所有输入的 .o 文件链接成一个格式的 .bin 文件,它们都是ELF格式, 链接器给每条指令/数据都赋与一个地址,这个地址叫链接地址,它可以是相对的也可以是绝对的。但它们之间的内部距离是固定的,链接具体过程可翻看 (重定位篇) 和 (链接脚本篇) 运行地址 由加载器确定,内核镜像首先通过烧录工具将内核烧录到 flash 指定的位置,开机后由boot loader工具,例如uboot,将内核镜像加载到指定地址后开始执行真正的内核代码,这个地址叫运行地址。 两个地址往往不一样,而内核设计者希望它们是一样的,那有没有办法检测二者是否一样呢? 答案是 : 当然有的 ,通过一个变量在链接时将其链接地址变成变量的内容 ,无论中间怎么加载变量的内容是不会变的,而获取运行地址是很容易获取的,其实就是PC寄存器的地址,二者一减,加载偏了多少不就出来了 pa_va_offset: .word . //定义一个4字节的pa_va_offset 变量, 链接器生成一个链接地址, . 表示 pa_va_offset = 链接地址 举例: 在地址 0x17321796 中保存了 0x17321796 值 adr r11, pa_va_offset //代码已执行至此,指令将获取 pa_va_offset 的运行地址(可能不是`0x17321796`) 给r11 ldr r0, [r11] // [r11]中存的是链接地址 `0x17321796`, 它不会随加载器变化的 sub r11, r11, r0 // 二者相减得到了偏移地址 第五步: 将内核代码从 __exception_handlers 处移到 SYS_MEM_BASE处,长度是 __bss_start - __exception_handlers , __exception_handlers是加载后的开始地址, 由加载器决定, 而SYS_MEM_BASE 是系统定义的内存地址, 可由系统集成商指定配置, 他们希望内核从这里运行。 下图为内核镜像布局 具体代码如下: /* if we need to relocate to proper location or not | 如果需要重新安装到合适的位置*/ adr r4, __exception_handlers /* r4: base of load address | 加载基址*/ ldr r5, =SYS_MEM_BASE /* r5: base of physical address | 物理基址*/ subs r12, r4, r5 /* r12: delta of load address and physical address | 二者偏移量*/ beq reloc_img_to_bottom_done /* if we load image at the bottom of physical address | 不相等就需要重定位 */ /* we need to relocate image at the bottom of physical address | 需要知道拷贝的大小*/ ldr r7, =__exception_handlers /* r7: base of linked address (or vm address) | 链接地址基地址*/ ldr r6, =__bss_start /* r6: end of linked address (or vm address),由于目前阶段有用的数据是中断向量表+代码段+只读数据段+数据段, 所以只需复制[__exception_handlers,__bss_start]这段数据到内存基址处 */ sub r6, r7 /* r6: delta of linked address (or vm address) | 内核镜像大小 */ add r6, r4 /* r6: end of load address | 说明需拷贝[ r4,r4+r6 ] 区间内容到 [ r5,r5+r6 ]*/ reloc_img_to_bottom_loop://重定位镜像到内核物理内存基地址,将内核从加载地址拷贝到内存基址处 ldr r7, [r4], #4 // 类似C语言 *r5 = *r4 , r4++ , r5++ str r7, [r5], #4 // #4 代表32位的指令长度,此时在拷贝内核代码区内容 cmp r4, r6 /* 拷贝完成条件. r4++ 直到等于r6 (加载结束地址) 完成拷贝动作 */ bne reloc_img_to_bottom_loop sub pc, r12 /* 重新校准pc寄存器, 无缝跳到了拷贝后的指令地址处执行 r12是重定位镜像前内核加载基地址和内核物理内存基地址的差值 */ nop // 注意执行完成sub pc, r12后,新的PC寄存器也指向了 nop ,nop是伪汇编指令,等同于 mov r0 r0 通常用于控制时序的目的,强制内存对齐,防止流水线灾难,占据分支指令延迟 sub r11, r11, r12 /* r11: eventual address offset | 最终地址偏移量 */ 第六步: 在打开MMU必须要做好虚拟地址和物理地址的映射关系 , 需构建页表 , 关于页表可翻看 虚实映射篇, 具体代码如下 #ifdef LOSCFG_KERNEL_MMU ldr r4, =g_firstPageTable /* r4: physical address of translation table and clear it 内核页表是用数组g_firstPageTable存储 见于los_arch_mmu.c */ add r4, r4, r11 //计算g_firstPageTable页表物理地址 mov r0, r4 //因为默认r0 将作为memset_optimized的第一个参数 mov r1, #0 //第二个参数,清0 mov r2, #MMU_DESCRIPTOR_L1_SMALL_ENTRY_NUMBERS //第三个参数是L1表的长度 bl memset_optimized /* optimized memset since r0 is 64-byte aligned | 将内核页表空间清零*/ ldr r5, =g_archMmuInitMapping //记录映射关系表 add r5, r5, r11 //获取g_archMmuInitMapping的物理地址 init_mmu_loop: //初始化内核页表 ldmia r5!, {r6-r10} /* r6 = phys, r7 = virt, r8 = size, r9 = mmu_flags, r10 = name | 物理地址、虚拟地址、映射大小、映射属性、名称*/ cmp r8, 0 /* if size = 0, the mmu init done */ beq init_mmu_done //标志寄存器中Z标志位等于零时跳转到 init_mmu_done处执行 bl page_table_build //创建页表 b init_mmu_loop //循环继续 init_mmu_done: orr r8, r4, #MMU_TTBRx_FLAGS /* r8 = r4 and set cacheable attributes on translation walk | 设置缓存*/ ldr r4, =g_mmuJumpPageTable /* r4: jump pagetable vaddr | 页表虚拟地址*/ add r4, r4, r11 ldr r4, [r4] add r4, r4, r11 /* r4: jump pagetable paddr | 页表物理地址*/ /* build 1M section mapping, in order to jump va during turing on mmu:pa == pa, va == pa */ /* 从当前PC开始建立1MB空间的段映射,分别建立物理地址和虚拟地址方式的段映射页表项 * 内核临时页表在系统 使能mmu -> 切换到虚拟地址运行 这段时间使用 */ mov r6, pc mov r7, r6 /* r7: pa (MB aligned)*/ lsr r6, r6, #20 /* r6: pa l1 index */ ldr r10, =MMU_DESCRIPTOR_KERNEL_L1_PTE_FLAGS add r12, r10, r6, lsl #20 /* r12: pa |flags */ str r12, [r4, r7, lsr #(20 - 2)] /* jumpTable[paIndex] = pt entry */ rsb r7, r11, r6, lsl #20 /* r7: va */ str r12, [r4, r7, lsr #(20 - 2)] /* jumpTable[vaIndex] = pt entry */ bl mmu_setup /* set up the mmu | 内核映射表已经创建好了,此时可以启动MMU工作了*/ #endif 第七步: 使能MMU, 有了页表就可以使用虚拟地址了 mmu_setup: //启动MMU工作 mov r12, #0 /* TLB Invalidate All entries - TLBIALL */ mcr p15, 0, r12, c8, c7, 0 /* Set c8 to control the TLB and set the mapping to invalid */ isb mcr p15, 0, r12, c2, c0, 2 /* Translation Table Base Control Register(TTBCR) = 0x0 [31] :0 - Use the 32-bit translation system(虚拟地址是32位) [5:4]:0 - use TTBR0和TTBR1 [2:0]:0 - TTBCR.N为0; 例如:TTBCR.N为0,TTBR0[31:14-0] | VA[31-0:20] | descriptor-type[1:0]组成32位页表描述符的地址, VA[31:20]可以覆盖4GB的地址空间,所以TTBR0页表是16KB,不使用TTBR1; 例如:TTBCR.N为1,TTBR0[31:14-1] | VA[31-1:20] | descriptor-type[1:0]组成32位页表描述符的地址, VA[30:20]可以覆盖2GB的地址空间,所以TTBR0页表是8KB,TTBR1页表是8KB(页表地址必须16KB对齐); */ isb orr r12, r4, #MMU_TTBRx_FLAGS //将临时页表属性[6:0]和基地址[31:14]放到r12 mcr p15, 0, r12, c2, c0, 0 /* Set attributes and set temp page table */ isb mov r12, #0x7 /* 0b0111 */ mcr p15, 0, r12, c3, c0, 0 /* Set DACR with 0b0111, client and manager domian */ isb mrc p15, 0, r12, c1, c0, 1 /* ACTLR, Auxlliary Control Register */ orr r12, r12, #(1 << 6) /* SMP, Enables coherent requests to the processor. */ orr r12, r12, #(1 << 2) /* Enable D-side prefetch */ orr r12, r12, #(1 << 11) /* Global BP Enable bit */ mcr p15, 0, r12, c1, c0, 1 /* ACTLR, Auxlliary Control Register */ dsb /* * 开始使能MMU,使用的是内核临时页表,这时cpu访问内存不管是取指令还是访问数据都是需要经过mmu来翻译, * 但是在mmu使能之前cpu使用的都是内核的物理地址,即使现在使能了mmu,cpu访问的地址值还是内核的物理地址值(这里仅仅从数值上来看), * 而又由于mmu使能了,所以cpu会把这个值当做虚拟地址的值到页表中去找其对应的物理地址来访问。 * 所以现在明白了为什么要在内核临时页表里建立一个内核物理地址和虚拟地址一一映射的页表项了吧,因为建立了一一映射, * cpu访问的地址经过mmu翻译得到的还是和原来一样的值,这样在cpu真正使用虚拟地址之前也能正常运行。 */ mrc p15, 0, r12, c1, c0, 0 bic r12, #(1 << 29 | 1 << 28) /* disable access flag[bit29],ap[0]是访问权限位,支持全部的访问权限类型 disable TEX remap[bit28],使用TEX[2:0]与C Bbit控制memory region属性 */ orr r12, #(1 << 0) /* mmu enable */ bic r12, #(1 << 1) orr r12, #(1 << 2) /* D cache enable */ orr r12, #(1 << 12) /* I cache enable */ mcr p15, 0, r12, c1, c0, 0 /* Set SCTLR with r12: Turn on the MMU, I/D cache Disable TRE/AFE */ isb ldr pc, =1f /* Convert to VA | 1表示标号,f表示forward(往下) - pc值取往下标识符“1”的虚拟地址(跳转到标识符“1”处) 因为之前已经在内核临时页表中建立了内核虚拟地址和物理地址的映射关系,所以接下来cpu切换到虚拟地址空间 */ 1: mcr p15, 0, r8, c2, c0, 0 /* Go to the base address saved in C2: Jump to the page table */ isb //r8中保存的是内核L1页表基地址和flags,r8写入到TTBR0实现临时页表和内核页表的切换 mov r12, #0 mcr p15, 0, r12, c8, c7, 0 /* TLB Invalidate All entries - TLBIALL(Invalidate all EL1&0 regime stage 1 and 2 TLB entries) */ isb sub lr, r11 /* adjust lr with delta of physical address and virtual address | lr中保存的是mmu使能之前返回地址的物理地址值,这时需要转换为虚拟地址,转换算法也很简单,虚拟地址 = 物理地址 - r11 */ bx lr //返回 第八步: 设置异常和中断栈 ,初始化栈内值和栈顶值 //初始化栈内值 ldr r0, =__svc_stack //stack_init的第一个参数 __svc_stack表示栈顶 ldr r1, =__exc_stack_top //stack_init的第二个参数 __exc_stack_top表示栈底, 这里会有点绕, top表高地址位 bl stack_init //初始化各个cpu不同模式下的栈空间 //设置各个栈顶魔法数字 STACK_MAGIC_SET __svc_stack, #OS_EXC_SVC_STACK_SIZE, OS_STACK_MAGIC_WORD //中断栈底设成"烫烫烫烫烫烫" STACK_MAGIC_SET __exc_stack, #OS_EXC_STACK_SIZE, OS_STACK_MAGIC_WORD //异常栈底设成"烫烫烫烫烫烫" stack_init: ldr r2, =OS_STACK_INIT //0xCACACACA ldr r3, =OS_STACK_INIT /* Main loop sets 32 bytes at a time. | 主循环一次设置 32 个字节*/ stack_init_loop: .irp offset, #0, #8, #16, #24 strd r2, r3, [r0, \offset] /* 等价于strd r2, r3, [r0, 0], strd r2, r3, [r0, 8], ... , strd r2, r3, [r0, 24] */ .endr add r0, #32 //加跳32个字节,说明在地址范围上 r1 > r0 ==> __exc_stack_top > __svc_stack cmp r0, r1 //是否到栈底 blt stack_init_loop bx lr //初始化栈顶值 excstack_magic: mov r3, #0 //r3 = 0 excstack_magic_loop: str r2, [r0] //栈顶设置魔法数字 add r0, r0, r1 //定位到栈底 add r3, r3, #1 //r3++ cmp r3, #CORE_NUM //栈空间等分成core_num个空间,所以每个core的栈顶需要magic num blt excstack_magic_loop bx lr /* param0 is stack top, param1 is stack size, param2 is magic num */ .macro STACK_MAGIC_SET param0, param1, param2 ldr r0, =\param0 mov r1, \param1 ldr r2, =\param2 bl excstack_magic .endm STACK_MAGIC_SET __svc_stack, #OS_EXC_SVC_STACK_SIZE, OS_STACK_MAGIC_WORD //中断栈底设成"烫烫烫烫烫烫" STACK_MAGIC_SET __exc_stack, #OS_EXC_STACK_SIZE, OS_STACK_MAGIC_WORD //异常栈底设成"烫烫烫烫烫烫" 第九步: 热启动 warm_reset: //热启动 Warm Reset, warm reboot, soft reboot, 在不关闭电源的情况,由软件控制重启计算机 /* initialize CPSR (machine state register) */ mov r0, #(CPSR_IRQ_DISABLE|CPSR_FIQ_DISABLE|CPSR_SVC_MODE) /* 禁止IRQ中断 | 禁止FIQ中断 | 管理模式-操作系统使用的保护模式 */ msr cpsr, r0 /* Note: some functions in LIBGCC1 will cause a "restore from SPSR"!! */ msr spsr, r0 /* get cpuid and keep it in r12 */ mrc p15, 0, r12, c0, c0, 5 //R12保存CPUID and r12, r12, #MPIDR_CPUID_MASK //掩码操作获取当前cpu id /* set svc stack, every cpu has OS_EXC_SVC_STACK_SIZE stack */ ldr r0, =__svc_stack_top mov r2, #OS_EXC_SVC_STACK_SIZE mul r2, r2, r12 sub r0, r0, r2 /* 算出当前core的中断栈栈顶位置,写入所属core的sp */ mov sp, r0 LDR r0, =__exception_handlers MCR p15, 0, r0, c12, c0, 0 /* Vector Base Address Register - VBAR */ cmp r12, #0 bne cpu_start //从核处理分支 第十步: 进入 C 语言的 main() bl main //带LR的子程序跳转, LR = pc - 4, 执行C层main函数 LITE_OS_SEC_TEXT_INIT INT32 main(VOID)//由主CPU执行,默认0号CPU 为主CPU { UINT32 ret = OsMain(); if (ret != LOS_OK) { return (INT32)LOS_NOK; } CPU_MAP_SET(0, OsHwIDGet());//设置CPU映射,参数0 代表0号CPU OsSchedStart();//调度开始 while (1) { __asm volatile("wfi");//WFI: wait for Interrupt 等待中断,即下一次中断发生前都在此hold住不干活 } } 百文说内核 | 抓住主脉络 百文相当于摸出内核的肌肉和器官系统,让人开始丰满有立体感,因是直接从注释源码起步,在加注释过程中,每每有心得处就整理,慢慢形成了以下文章。内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感,容易理解记忆。说别人能听得懂的话很重要! 百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思。更希望让内核变得栩栩如生,倍感亲切。 与代码需不断debug一样,文章内容会存在不少错漏之处,请多包涵,但会反复修正,持续更新,v**.xx 代表文章序号和修改的次数,精雕细琢,言简意赅,力求打造精品内容。 百文在 < 鸿蒙研究站 | 开源中国 | 博客园 | 51cto | csdn | 知乎 | 掘金 > 站点发布,鸿蒙研究站 | weharmonyos 中回复 百文 可方便阅读。 按功能模块: 基础知识 进程管理 任务管理 内存管理 双向链表 内核概念 源码结构 地址空间 计时单位 优雅的宏 钩子框架 位图管理 POSIX main函数 调度故事 进程控制块 进程空间 线性区 红黑树 进程管理 Fork进程 进程回收 Shell编辑 Shell解析 任务控制块 并发并行 就绪队列 调度机制 任务管理 用栈方式 软件定时器 控制台 远程登录 协议栈 内存规则 物理内存 内存概念 虚实映射 页表管理 静态分配 TLFS算法 内存池管理 原子操作 圆整对齐 通讯机制 文件系统 硬件架构 内核汇编 通讯总览 自旋锁 互斥锁 快锁使用 快锁实现 读写锁 信号量 事件机制 信号生产 信号消费 消息队列 消息封装 消息映射 共享内存 文件概念 文件故事 索引节点 VFS 文件句柄 根文件系统 挂载机制 管道文件 文件映射 写时拷贝 芯片模式 ARM架构 指令集 协处理器 工作模式 寄存器 多核管理 中断概念 中断管理 编码方式 汇编基础 汇编传参 链接脚本 内核启动 进程切换 任务切换 中断切换 异常接管 缺页中断 编译运行 调测工具 编译过程 编译构建 GN语法 忍者无敌 ELF格式 ELF解析 静态链接 重定位 动态链接 进程映像 应用启动 系统调用 VDSO 模块监控 日志跟踪 系统安全 测试用例 百万注源码 | 处处扣细节 百万汉字注解内核目的是要看清楚其毛细血管,细胞结构,等于在拿放大镜看内核。内核并不神秘,带着问题去源码中找答案是很容易上瘾的,你会发现很多文章对一些问题的解读是错误的,或者说不深刻难以自圆其说,你会慢慢形成自己新的解读,而新的解读又会碰到新的问题,如此层层递进,滚滚向前,拿着放大镜根本不愿意放手。 < gitee | github | coding | gitcode > 四大码仓推送 | 同步官方源码,鸿蒙研究站 | weharmonyos 中回复 百万 可方便阅读。 据说喜欢点赞分享的,后来都成了大神。:)

优秀的个人博客,低调大师

v76.01 鸿蒙内核源码分析(共享内存) | 进程间最快通讯方式 | 百篇博客分析OpenHarmony源码

百篇博客分析|本篇为:(共享内存篇) | 进程间最快通讯方式 进程通讯相关篇为: v26.08 鸿蒙内核源码分析(自旋锁) | 当立贞节牌坊的好同志 v27.05 鸿蒙内核源码分析(互斥锁) | 同样是锁它确更丰满 v28.04 鸿蒙内核源码分析(进程通讯) | 九种进程间通讯方式速揽 v29.05 鸿蒙内核源码分析(信号量) | 谁在解决任务间的同步 v30.07 鸿蒙内核源码分析(事件控制) | 多对多任务如何同步 v33.03 鸿蒙内核源码分析(消息队列) | 进程间如何异步传递大数据 v76.01 鸿蒙内核源码分析(共享内存) | 进程间最快通讯方式 概念 共享好端端的一词,近些年被玩坏了,共享单车,共享充电宝,共享办公室,共享雨伞... 甚至还有共享女朋友,真是人有多大胆,共享有多大产。但凡事太尽就容易恶心到人,自己也一度被 共享内存 恶心到了,一直不想碰它,拖到了现在才写。 共享内存的原理简单,目的是为了进程间通讯,方法是通过映射到同一块物理内存。它是一种稀缺资源由内核按资源池方式管理,数量有限,默认是 192个,用资源ID唯一标识,用户进程需要时通过系统调用向内核申请共享内存大小,管理器从资源池中分配一个可用资源ID,并向物理内存申请对应的物理页框。 如何使用共享内存就涉及到了内存模块最重要的概念 映射,不清楚的可以翻看系列相关篇。有共享需求的进程在各自的进程空间中划出一个线性区映射到共享内存段,那如何找到这个共享内存段呢 ? 由系统调用提供操作接口,简单说是先通过参数key创建共享资源ID(shmid),再由shmid来连接/删除/控制 共享内存。详见本篇末尾的4个系统调用 Shm***。 如何实现? 这是笔者看完内核共享内存模块画出来的图,尽量用一张图表达一个模块的内容,因为百文是在给源码注释的过程中产生的,所以会画出这种比较怪异的图,有代码,也有模型,姑且称之为 代码模型图: 图分 管理 和 映射使用 两部分解读。 为了精简,代码展示只留下骨干,删除了判断,检查的代码。 管理部分 初始化共享内存,共享内存是以资源池的方式管理的,上来就为全局变量g_shmSegs向内核堆空间申请了g_shmInfo.shmmni个struct shmIDSource #define SHM_MNI 192 //共享内存总数 默认192 // 共享内存模块设置信息 struct shminfo { unsigned long shmmax, shmmin, shmmni, shmseg, shmall, __unused[4]; }; STATIC struct shminfo g_shmInfo = { //描述共享内存范围的全局变量 .shmmax = SHM_MAX,//共享内存单个上限 4096页 即 16M .shmmin = SHM_MIN,//共享内存单个下限 1页 即:4K .shmmni = SHM_MNI,//共享内存总数 默认192 .shmseg = SHM_SEG,//每个用户进程可以使用的最多的共享内存段的数目 128 .shmall = SHM_ALL,//系统范围内共享内存的总页数,4096页 }; //共享内存初始化 UINT32 ShmInit(VOID) { // .. ret = LOS_MuxInit(&g_sysvShmMux, NULL);//初始化互斥 g_shmSegs = LOS_MemAlloc((VOID *)OS_SYS_MEM_ADDR, sizeof(struct shmIDSource) * g_shmInfo.shmmni);//分配shm段数组 (VOID)memset_s(g_shmSegs, (sizeof(struct shmIDSource) * g_shmInfo.shmmni), 0, (sizeof(struct shmIDSource) * g_shmInfo.shmmni));//数组清零 for (i = 0; i < g_shmInfo.shmmni; i++) { g_shmSegs[i].status = SHM_SEG_FREE;//节点初始状态为空闲 g_shmSegs[i].ds.shm_perm.seq = i + 1;//struct ipc_perm shm_perm;系统为每一个IPC对象保存一个ipc_perm结构体,结构说明了IPC对象的权限和所有者 LOS_ListInit(&g_shmSegs[i].node);//初始化节点 } g_shmUsedPageCount = 0; return LOS_OK; } 系列篇多次提过,每个功能模块都至少有一个核心结构体来支撑模块的运行,进程是PCB,任务是TCB,而共享内存就是shmIDSource struct shmIDSource {//共享内存描述符 struct shmid_ds ds; //是内核为每一个共享内存段维护的数据结构 UINT32 status; //状态 SHM_SEG_FREE ... LOS_DL_LIST node; //节点,挂VmPage #ifdef LOSCFG_SHELL CHAR ownerName[OS_PCB_NAME_LEN]; #endif }; 首先shmid_ds是真正描述共享内存信息的结构体,记录了本次共享内存由谁创建,大小,用户/组,访问时间等等。 //每个共享内存段在内核中维护着一个内部结构shmid_ds struct shmid_ds { struct ipc_perm shm_perm;///< 操作许可,里面包含共享内存的用户ID、组ID等信息 size_t shm_segsz; ///< 共享内存段的大小,单位为字节 time_t shm_atime; ///< 最后一个进程访问共享内存的时间 time_t shm_dtime; ///< 最后一个进程离开共享内存的时间 time_t shm_ctime; ///< 创建时间 pid_t shm_cpid; ///< 创建共享内存的进程ID pid_t shm_lpid; ///< 最后操作共享内存的进程ID unsigned long shm_nattch; ///< 当前使用该共享内存段的进程数量 unsigned long __pad1; //保留扩展用 unsigned long __pad2; }; //内核为每一个IPC对象保存一个ipc_perm结构体,该结构说明了IPC对象的权限和所有者 struct ipc_perm { key_t __ipc_perm_key; //调用shmget()时给出的关键字 uid_t uid; //共享内存所有者的有效用户ID gid_t gid; //共享内存所有者所属组的有效组ID uid_t cuid; //共享内存创建 者的有效用户ID gid_t cgid; //共享内存创建者所属组的有效组ID mode_t mode; //权限 + SHM_DEST / SHM_LOCKED /SHM_HUGETLB 标志位 int __ipc_perm_seq; //序列号 long __pad1; //保留扩展用 long __pad2; }; status 表示这段共享内存的状态,因为是资源池的方式,只有SHM_SEG_FREE的状态才可供分配,进程池和任务池也是这种管理方式。 #define SHM_SEG_FREE 0x2000 //空闲未使用 #define SHM_SEG_USED 0x4000 //已使用 #define SHM_SEG_REMOVE 0x8000 //删除 node双向链表上挂的是一个个的物理页框VmPage,这是核心属性,数据将被存在这一个个物理页框中。ShmAllocSeg为具体的分配函数 STATIC INT32 ShmAllocSeg(key_t key, size_t size, INT32 shmflg) { // ... count = LOS_PhysPagesAlloc(size >> PAGE_SHIFT, &seg->node);//分配共享页面,函数内部把node都挂好了. if (count != (size >> PAGE_SHIFT)) {//当未分配到足够的内存时,处理方式是:不稀罕给那么点,舍弃! (VOID)LOS_PhysPagesFree(&seg->node);//释放节点上的物理页框 seg->status = SHM_SEG_FREE;//共享段变回空闲状态 return -ENOMEM; } ShmSetSharedFlag(seg);//将node的每个页面设置为共享页 g_shmUsedPageCount += size >> PAGE_SHIFT; seg->status |= SHM_SEG_USED; //共享段贴上已在使用的标签 seg->ds.shm_perm.mode = (UINT32)shmflg & ACCESSPERMS; seg->ds.shm_perm.key = key;//保存参数key,如此 key 和 共享ID绑定在一块 seg->ds.shm_segsz = size; //共享段的大小 seg->ds.shm_perm.cuid = LOS_GetUserID(); //设置用户ID seg->ds.shm_perm.uid = LOS_GetUserID(); //设置用户ID seg->ds.shm_perm.cgid = LOS_GetGroupID(); //设置组ID seg->ds.shm_perm.gid = LOS_GetGroupID(); //设置组ID seg->ds.shm_lpid = 0; //最后一个操作的进程 seg->ds.shm_nattch = 0; //绑定进程的数量 seg->ds.shm_cpid = LOS_GetCurrProcessID(); //获取进程ID seg->ds.shm_atime = 0; //访问时间 seg->ds.shm_dtime = 0; //detach 分离时间 共享内存使用完之后,需要将它从进程地址空间中分离出来;将共享内存分离并不是删除它,只是使该共享内存对当前的进程不再可用 seg->ds.shm_ctime = time(NULL);//创建时间 #ifdef LOSCFG_SHELL (VOID)memcpy_s(seg->ownerName, OS_PCB_NAME_LEN, OsCurrProcessGet()->processName, OS_PCB_NAME_LEN); #endif return segNum; } 映射使用部分 第一步: 创建共享内存 要实现共享内存,首先得创建一个内存段用于共享,干这事的是ShmGet /*! * @brief ShmGet * 得到一个共享内存标识符或创建一个共享内存对象 * @param key 建立新共享内存对象 标识符是IPC对象的内部名。为使多个合作进程能够在同一IPC对象上汇聚,需要提供一个外部命名方案。 为此,每个IPC对象都与一个键(key)相关联,这个键作为该对象的外部名,无论何时创建IPC结构(通过msgget、semget、shmget创建), 都应给IPC指定一个键, key_t由ftok创建,ftok当然在本工程里找不到,所以要写这么多. * @param shmflg IPC_CREAT IPC_EXCL IPC_CREAT: 在创建新的IPC时,如果key参数是IPC_PRIVATE或者和当前某种类型的IPC结构无关,则需要指明flag参数的IPC_CREAT标志位, 则用来创建一个新的IPC结构。(如果IPC结构已存在,并且指定了IPC_CREAT,则IPC_CREAT什么都不做,函数也不出错) IPC_EXCL: 此参数一般与IPC_CREAT配合使用来创建一个新的IPC结构。如果创建的IPC结构已存在函数就出错返回, 返回EEXIST(这与open函数指定O_CREAT和O_EXCL标志原理相同) * @param size 新建的共享内存大小,以字节为单位 * @return * * @see */ INT32 ShmGet(key_t key, size_t size, INT32 shmflg) { SYSV_SHM_LOCK(); if (key == IPC_PRIVATE) { ret = ShmAllocSeg(key, size, shmflg); } else { ret = ShmFindSegByKey(key);//通过key查找资源ID ret = ShmAllocSeg(key, size, shmflg);//分配一个共享内存 } SYSV_SHM_UNLOCK(); return ret; } 第二步: 进程线性区绑定共享内存 shmat()函数的作用就是用来启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间。,ShmAt的第一个参数其实是ShmGet成功时的返回值 ,ShmatVmmAlloc负责分配一个可用的线性区并和共享内存映射好 /*! * @brief ShmAt * 用来启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间。 * @param shm_flg 是一组标志位,通常为0。 * @param shmaddr 指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址。 * @param shmid 是shmget()函数返回的共享内存标识符 * @return * 如果shmat成功执行,那么内核将使与该共享存储相关的shmid_ds结构中的shm_nattch计数器值加1 shmid 就是个索引,就跟进程和线程的ID一样 g_shmSegs[shmid] shmid > 192个 * @see */ VOID *ShmAt(INT32 shmid, const VOID *shmaddr, INT32 shmflg) { struct shmIDSource *seg = NULL; LosVmMapRegion *r = NULL; ret = ShmatParamCheck(shmaddr, shmflg);//参数检查 SYSV_SHM_LOCK(); seg = ShmFindSeg(shmid);//找到段 ret = ShmPermCheck(seg, acc_mode); seg->ds.shm_nattch++;//ds上记录有一个进程绑定上来 r = ShmatVmmAlloc(seg, shmaddr, shmflg, prot);//在当前进程空间分配一个线性区并映射到共享内存 r->shmid = shmid;//把ID给线性区的shmid r->regionFlags |= VM_MAP_REGION_FLAG_SHM;//这是一个共享线性区 seg->ds.shm_atime = time(NULL);//访问时间 seg->ds.shm_lpid = LOS_GetCurrProcessID();//进程ID SYSV_SHM_UNLOCK(); return (VOID *)(UINTPTR)r->range.base; } 第三步: 控制/使用 共享内存,这才是目的,前面的都是前戏 /*! * @brief ShmCtl * 此函数可以对shmid指定的共享存储进行多种操作(删除、取信息、加锁、解锁等) * @param buf 是一个结构指针,它指向共享内存模式和访问权限的结构。 * @param cmd command是要采取的操作,它可以取下面的三个值 : IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值。 IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值 IPC_RMID:删除共享内存段 * @param shmid 是shmget()函数返回的共享内存标识符 * @return * * @see */ INT32 ShmCtl(INT32 shmid, INT32 cmd, struct shmid_ds *buf) { SYSV_SHM_LOCK(); switch (cmd) { case IPC_STAT: case SHM_STAT://取段结构 ret = LOS_ArchCopyToUser(buf, &seg->ds, sizeof(struct shmid_ds));//把内核空间的共享页数据拷贝到用户空间 if (cmd == SHM_STAT) { ret = (unsigned int)((unsigned int)seg->ds.shm_perm.seq << 16) | (unsigned int)((unsigned int)shmid & 0xffff); /* 16: use the seq as the upper 16 bits */ } break; case IPC_SET://重置共享段 ret = ShmPermCheck(seg, SHM_M); //从用户空间拷贝数据到内核空间 ret = LOS_ArchCopyFromUser(&shm_perm, &buf->shm_perm, sizeof(struct ipc_perm)); seg->ds.shm_perm.uid = shm_perm.uid; seg->ds.shm_perm.gid = shm_perm.gid; seg->ds.shm_perm.mode = (seg->ds.shm_perm.mode & ~ACCESSPERMS) | (shm_perm.mode & ACCESSPERMS);//可访问 seg->ds.shm_ctime = time(NULL); #ifdef LOSCFG_SHELL (VOID)memcpy_s(seg->ownerName, OS_PCB_NAME_LEN, OS_PCB_FROM_PID(shm_perm.uid)->processName, OS_PCB_NAME_LEN); #endif break; case IPC_RMID://删除共享段 ret = ShmPermCheck(seg, SHM_M); seg->status |= SHM_SEG_REMOVE; if (seg->ds.shm_nattch <= 0) {//没有任何进程在使用了 ShmFreeSeg(seg);//释放 归还内存 } break; case IPC_INFO://把内核空间的共享页数据拷贝到用户空间 ret = LOS_ArchCopyToUser(buf, &g_shmInfo, sizeof(struct shminfo)); ret = g_shmInfo.shmmni; break; case SHM_INFO: shmInfo.shm_rss = 0; shmInfo.shm_swp = 0; shmInfo.shm_tot = 0; shmInfo.swap_attempts = 0; shmInfo.swap_successes = 0; shmInfo.used_ids = ShmSegUsedCount();//在使用的seg数 ret = LOS_ArchCopyToUser(buf, &shmInfo, sizeof(struct shm_info));//把内核空间的共享页数据拷贝到用户空间 ret = g_shmInfo.shmmni; break; default: VM_ERR("the cmd(%d) is not supported!", cmd); ret = EINVAL; goto ERROR; } SYSV_SHM_UNLOCK(); return ret; } 第四步: 完事了解绑/删除,好聚好散还有下次,在ShmDt中主要干了解除映射LOS_ArchMmuUnmap这件事,没有了映射就不再有关系了,并且会检测到最后一个解除映射的进程时,会彻底释放掉这段共享内存ShmFreeSeg /** * @brief 当对共享存储的操作已经结束时,则调用shmdt与该存储段分离 如果shmat成功执行,那么内核将使与该共享存储相关的shmid_ds结构中的shm_nattch计数器值减1 * @attention 注意:这并不从系统中删除共享存储的标识符以及其相关的数据结构。共享存储的仍然存在, 直至某个进程带IPC_RMID命令的调用shmctl特地删除共享存储为止 * @param shmaddr * @return INT32 */ INT32 ShmDt(const VOID *shmaddr) { LosVmSpace *space = OsCurrProcessGet()->vmSpace;//获取进程空间 (VOID)LOS_MuxAcquire(&space->regionMux); region = LOS_RegionFind(space, (VADDR_T)(UINTPTR)shmaddr);//找到线性区 shmid = region->shmid;//线性区共享ID LOS_RbDelNode(&space->regionRbTree, &region->rbNode);//从红黑树和链表中摘除节点 LOS_ArchMmuUnmap(&space->archMmu, region->range.base, region->range.size >> PAGE_SHIFT);//解除线性区的映射 (VOID)LOS_MuxRelease(&space->regionMux); /* free it */ free(region);//释放线性区所占内存池中的内存 SYSV_SHM_LOCK(); seg = ShmFindSeg(shmid);//找到seg,线性区和共享段的关系是 1:N 的关系,其他空间的线性区也会绑在共享段上 ShmPagesRefDec(seg);//页面引用数 -- seg->ds.shm_nattch--;//使用共享内存的进程数少了一个 if ((seg->ds.shm_nattch <= 0) && //无任何进程使用共享内存 (seg->status & SHM_SEG_REMOVE)) {//状态为删除时需要释放物理页内存了,否则其他进程还要继续使用共享内存 ShmFreeSeg(seg);//释放seg 页框链表中的页框内存,再重置seg状态 } else { seg->ds.shm_dtime = time(NULL);//记录分离的时间 seg->ds.shm_lpid = LOS_GetCurrProcessID();//记录操作进程ID } SYSV_SHM_UNLOCK(); 总结 看到这里你应该不会问共享内存的作用和为啥它是最快的进程间通讯方式了,如果还有这两个问题说明还要再看一遍 :P ,另外细心的话会发现共享内存会有个小缺点,就是同时访问的问题,所以需要使用互斥锁来保证同时只有一个进程在使用,SYSV_SHM_LOCK和 SYSV_SHM_UNLOCK在以上的四个步骤中都有出现。 STATIC LosMux g_sysvShmMux; //互斥锁,共享内存本身并不保证操作的同步性,所以需用互斥锁 /* private macro */ #define SYSV_SHM_LOCK() (VOID)LOS_MuxLock(&g_sysvShmMux, LOS_WAIT_FOREVER) //申请永久等待锁 #define SYSV_SHM_UNLOCK() (VOID)LOS_MuxUnlock(&g_sysvShmMux) //释放锁 百文说内核 | 抓住主脉络 百文相当于摸出内核的肌肉和器官系统,让人开始丰满有立体感,因是直接从注释源码起步,在加注释过程中,每每有心得处就整理,慢慢形成了以下文章。内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感,容易理解记忆。说别人能听得懂的话很重要! 百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思。更希望让内核变得栩栩如生,倍感亲切。 与代码需不断debug一样,文章内容会存在不少错漏之处,请多包涵,但会反复修正,持续更新,v**.xx 代表文章序号和修改的次数,精雕细琢,言简意赅,力求打造精品内容。 百文在 < 鸿蒙研究站 | 开源中国 | 博客园 | 51cto | csdn | 知乎 | 掘金 > 站点发布,公众号回复 百文 可方便阅读。 按功能模块: 前因后果 >> 总目录 | 调度故事 | 内存主奴 | 源码注释 | 源码结构 | 静态站点 | 参考文档 | 基础工具 >> 双向链表 | 位图管理 | 用栈方式 | 定时器 | 原子操作 | 时间管理 | 加载运行 >> ELF格式 | ELF解析 | 静态链接 | 重定位 | 进程映像 | 进程管理 >> 进程管理 | 进程概念 | Fork | 特殊进程 | 进程回收 | 信号生产 | 信号消费 | Shell编辑 | Shell解析 | 编译构建 >> 编译环境 | 编译过程 | 环境脚本 | 构建工具 | gn应用 | 忍者ninja | 进程通讯 >> 自旋锁 | 互斥锁 | 进程通讯 | 信号量 | 事件控制 | 消息队列 | 共享内存 | 内存管理 >> 内存分配 | 内存管理 | 内存汇编 | 内存映射 | 内存规则 | 物理内存 | 任务管理 >> 时钟任务 | 任务调度 | 任务管理 | 调度队列 | 调度机制 | 线程概念 | 并发并行 | CPU | 系统调用 | 任务切换 | 文件系统 >> 文件概念 | 文件系统 | 索引节点 | 挂载目录 | 根文件系统 | VFS | 文件句柄 | 管道文件 | 硬件架构 >> 汇编基础 | 汇编传参 | 工作模式 | 寄存器 | 异常接管 | 汇编汇总 | 中断切换 | 中断概念 | 中断管理 | 设备驱动 >> 字符设备 | 控制台 | 远程登录 | 百万注源码 | 处处扣细节 百万汉字注解内核目的是要看清楚其毛细血管,细胞结构,等于在拿放大镜看内核。内核并不神秘,带着问题去源码中找答案是很容易上瘾的,你会发现很多文章对一些问题的解读是错误的,或者说不深刻难以自圆其说,你会慢慢形成自己新的解读,而新的解读又会碰到新的问题,如此层层递进,滚滚向前,拿着放大镜根本不愿意放手。 < gitee | github | coding | codechina > 四大码仓推送 | 同步官方源码,公众号中回复 百万 可方便阅读。 关注不迷路 | 代码即人生

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

用户登录
用户注册