v78.01 Hongmeng kernel source code analysis (message mapping) | analyze LiteIpc process communication mechanism | 100 blogs analyze OpenHarmony source code

Posted by ShiloVir on Fri, 11 Feb 2022 06:21:57 +0100

One hundred blog Analysis | this article is: (message mapping) | analyze the process communication mechanism of liteipc (Part 2)

Related articles of process communication are:

Basic concepts

LiteIPC is a new IPC (inter process communication) mechanism provided by OpenHarmony LiteOS-A kernel. It is a lightweight inter process communication component and provides inter process communication capability for service-oriented system service framework. It is divided into two parts: Kernel Implementation and user mode implementation. The kernel implementation completes inter process messaging, IPC memory management Timeout notification, death notification and other functions; User mode provides serialization and deserialization capabilities, and completes the distribution of IPC callback message and death message.

We mainly explain the kernel state implementation part. I wanted to finish one article, but I found that it is far more complex and important than expected. Therefore, we divided it into communication content and communication mechanism. In the first part, you can look at the source code analysis of Hongmeng kernel (message encapsulation) | analyze the communication content of liteipc (Part I). This part is the communication mechanism and introduces the implementation process of liteipc in the kernel layer.

Spatial mapping

The word "mapping" has appeared many times in the series. The basis of virtual address is mapping. The implementation of shared memory also depends on mapping. The bottom implementation of LiteIPC communication is also inseparable from mapping. Interestingly, the linear area of user state and the linear area of kernel state are mapped. In other words, when a user accesses a virtual address in user space, it actually points to the same physical memory address as a virtual address in kernel space. Someone might ask, is that ok? After understanding the relationship between physical address, kernel virtual address and user virtual address, you will understand that of course.

  • Virtual address includes kernel space address and user process space address. Their range is exposed to the outside world and set by the system integrator. The kernel also provides judgment function. That is, when you see a virtual address, you can know whether the kernel is in use or the user (application) process is in use.

    ///Is the virtual address in kernel space
      STATIC INLINE BOOL LOS_IsKernelAddress(VADDR_T vaddr)
      {
          return ((vaddr >= (VADDR_T)KERNEL_ASPACE_BASE) &&
                  (vaddr <= ((VADDR_T)KERNEL_ASPACE_BASE + ((VADDR_T)KERNEL_ASPACE_SIZE - 1))));
      }
      ///Is the virtual address in user space
      STATIC INLINE BOOL LOS_IsUserAddress(VADDR_T vaddr)
      {
          return ((vaddr >= USER_ASPACE_BASE) &&
                  (vaddr <= (USER_ASPACE_BASE + (USER_ASPACE_SIZE - 1))));
      }
    
  • The physical address is provided by the physical memory, and the system integrator sets the address range according to the actual physical memory size. As for whether a specific section of physical memory is used for kernel space or user space, there is no requirement. The so-called mapping refers to the mapping of virtual address < -- > physical address, which is an N:1 relationship. A physical address can be mapped by multiple virtual addresses, while a virtual address can only be mapped to one physical address.

  • The above is the conceptual basis for the implementation of LiteIPC. After understanding it, it is not difficult to understand the significance of the structure IpcPool

    /**
    * @struct IpcPool | ipc pool
    * @brief  LiteIPC The core idea of is to maintain an IPC message queue for each Service task in the kernel state, which is sent to the upper layer through LiteIPC device files
    * The user mode program provides read operations on behalf of receiving IPC messages and write operations on behalf of sending IPC messages.
    */
    typedef struct {
        VOID   *uvaddr;	///< user state space address is the address mapped from kvaddr. The relationship between these two addresses must be clear, otherwise the core idea of IPC cannot be understood
        VOID   *kvaddr;	///< kernel space address. IPC applies for kernel space, but it will map this address to user space through DoIpcMmap
        UINT32 poolSize; ///< IPC pool size
    } IpcPool;
    

File access

The operating mechanism of LiteIPC is to first register the task that needs to receive IPC messages as a Service through ServiceManager, and then configure access permissions for the Service task through ServiceManager, that is, specify which tasks can send IPC messages to the Service task. The core idea of LiteIPC is to maintain an IPC message queue for each Service task in the kernel state. The message queue provides the upper user state program with read operations on behalf of receiving IPC messages and write operations on behalf of sending IPC messages through LiteIPC device files. The interface layer (VFS) of the equipment file is implemented as g_liteIpcFops. By tracing these functions, you can understand the whole implementation process of LiteIPC

#define LITEIPC_DRIVER "/dev/lite_ipc" 	///<  Virtual device, file access, read
STATIC const struct file_operations_vfs g_liteIpcFops = {
    .open = LiteIpcOpen,   /* open | Create Ipc memory pool*/
    .close = LiteIpcClose,  /* close */
    .ioctl = LiteIpcIoctl,  /* ioctl | Include read and write operations */
    .mmap = LiteIpcMmap,   /* mmap | Realize linear area mapping*/
};

LiteIpcOpen | create message memory pool

LITE_OS_SEC_TEXT STATIC int LiteIpcOpen(struct file *filep)
{
    LosProcessCB *pcb = OsCurrProcessGet();
    if (pcb->ipcInfo != NULL) {
        return 0;
    }
    pcb->ipcInfo = LiteIpcPoolCreate();
    if (pcb->ipcInfo == NULL) {
        return -ENOMEM;
    }
    return 0;
}
///Create IPC message memory pool
LITE_OS_SEC_TEXT_INIT STATIC ProcIpcInfo *LiteIpcPoolCreate(VOID)
{
    ProcIpcInfo *ipcInfo = LOS_MemAlloc(m_aucSysMem1, sizeof(ProcIpcInfo));//Apply for IPC control body from kernel heap memory
    if (ipcInfo == NULL) {
        return NULL;
    }
    (VOID)memset_s(ipcInfo, sizeof(ProcIpcInfo), 0, sizeof(ProcIpcInfo));
    (VOID)LiteIpcPoolInit(ipcInfo);
    return ipcInfo;
}

unscramble

  • First get the current process OsCurrProcessGet(), that is, create a unique IPC message control body for each process. ProcIpcInfo is responsible for managing IPC messages in the process control block
  • Initialize the message memory pool. Here, only the memory occupied by the structure itself is applied. The real memory pool is completed in LiteIpcMmap

LiteIpcMmap | mapping

///Set the parameter linear area as IPC dedicated area
LITE_OS_SEC_TEXT STATIC int LiteIpcMmap(struct file *filep, LosVmMapRegion *region)
{
    int ret = 0;
    LosVmMapRegion *regionTemp = NULL;
    LosProcessCB *pcb = OsCurrProcessGet();
    ProcIpcInfo *ipcInfo = pcb->ipcInfo;
	//The mapped linear area cannot be in constant and private data areas
    if ((ipcInfo == NULL) || (region == NULL) || (region->range.size > LITE_IPC_POOL_MAX_SIZE) ||
        (!LOS_IsRegionPermUserReadOnly(region)) || (!LOS_IsRegionFlagPrivateOnly(region))) {
        ret = -EINVAL;
        goto ERROR_REGION_OUT;
    }
    if (IsPoolMapped(ipcInfo)) {//There is already a mapping relationship between user space and kernel space
        return -EEXIST;
    }
    if (ipcInfo->pool.uvaddr != NULL) {//ipc pool already has an address in process space
        regionTemp = LOS_RegionFind(pcb->vmSpace, (VADDR_T)(UINTPTR)ipcInfo->pool.uvaddr);//Find the linear region in the specified process space
        if (regionTemp != NULL) {
            (VOID)LOS_RegionFree(pcb->vmSpace, regionTemp);//Release the linear region first
        }
		// It is recommended to add ipcinfo - > pool uvaddr = NULL;  Same below
    }
    ipcInfo->pool.uvaddr = (VOID *)(UINTPTR)region->range.base;//Bind the specified linear area to the ipc pool virtual address
    if (ipcInfo->pool.kvaddr != NULL) {//If there is a kernel space address
        LOS_VFree(ipcInfo->pool.kvaddr);//To remap, you must first free up physical memory
        ipcInfo->pool.kvaddr = NULL; //In terms of effect, this sentence can be omitted, but it looks more comfortable. uvaddr and kvaddr are a new couple to welcome a better future
    }
    /* use vmalloc to alloc phy mem */
    ipcInfo->pool.kvaddr = LOS_VMalloc(region->range.size);//Apply for a linear area from the kernel dynamic space, allocate the same amount of physical memory, and map the kernel < -- > physical memory
    if (ipcInfo->pool.kvaddr == NULL) {//Failed to apply for physical memory. I'm sure I can't play anymore
        ret = -ENOMEM; //Return no memory
        goto ERROR_REGION_OUT;
    }
    /* do mmap */
    ret = DoIpcMmap(pcb, region);//Make a mapping relationship between uvaddr and kvaddr, so that the purpose of operating kvaddr can be achieved by operating uvaddr in user mode
    if (ret) {
        goto ERROR_MAP_OUT;
    }
    /* ipc pool init */
    if (LOS_MemInit(ipcInfo->pool.kvaddr, region->range.size) != LOS_OK) {//Initialize ipc pool
        ret = -EINVAL;
        goto ERROR_MAP_OUT;
    }
    ipcInfo->pool.poolSize = region->range.size;//The ipc pool size is linear
    return 0;
ERROR_MAP_OUT:
    LOS_VFree(ipcInfo->pool.kvaddr);
ERROR_REGION_OUT:
    if (ipcInfo != NULL) {
        ipcInfo->pool.uvaddr = NULL;
        ipcInfo->pool.kvaddr = NULL;
    }
    return ret;
}

unscramble

  • This function must be understood. The important part has been annotated. It mainly does two things.
  • Via LOS_VMalloc applies for a section of physical memory from the inner core heap space. The parameter is the size of the linear area and is mapped. Because it is the kernel heap space, the allocated virtual address is a kernel address, which is assigned to the pool kvaddr
    ipcInfo->pool.kvaddr = LOS_VMalloc(region->range.size);//Apply for a linear area from the kernel dynamic space, allocate the same amount of physical memory, and map the kernel < -- > physical memory
    
  • Set the parameter PCB (user process) to the pool of IPC message pool through DoIpcMmap Uvaddr is also mapped to Los_ On the physical memory allocated by vmalloc,
      ret = DoIpcMmap(pcb, region);//Make a mapping relationship between uvaddr and kvaddr, so that the operation can be achieved by operating uvaddr in user mode
    
    See the implementation of DoIpcMmap for details. Because it is too important, the code here will not be deleted or modified.
    LITE_OS_SEC_TEXT STATIC INT32 DoIpcMmap(LosProcessCB *pcb, LosVmMapRegion *region)
      {
          UINT32 i;
          INT32 ret = 0;
          PADDR_T pa;
          UINT32 uflags = VM_MAP_REGION_FLAG_PERM_READ | VM_MAP_REGION_FLAG_PERM_USER;
          LosVmPage *vmPage = NULL;
          VADDR_T uva = (VADDR_T)(UINTPTR)pcb->ipcInfo->pool.uvaddr;//User space address
          VADDR_T kva = (VADDR_T)(UINTPTR)pcb->ipcInfo->pool.kvaddr;//Kernel space address
          (VOID)LOS_MuxAcquire(&pcb->vmSpace->regionMux);
          for (i = 0; i < (region->range.size >> PAGE_SHIFT); i++) {//Get the number of pages in the linear area, and map page by page
              pa = LOS_PaddrQuery((VOID *)(UINTPTR)(kva + (i << PAGE_SHIFT)));//Find physical address through kernel space
              if (pa == 0) {
                  PRINT_ERR("%s, %d\n", __FUNCTION__, __LINE__);
                  ret = -EINVAL;
                  break;
              }
              vmPage = LOS_VmPageGet(pa);//Get physical page box
              if (vmPage == NULL) {//The purpose is to check whether the physical page exists
                  PRINT_ERR("%s, %d\n", __FUNCTION__, __LINE__);
                  ret = -EINVAL;
                  break;
              }
              STATUS_T err = LOS_ArchMmuMap(&pcb->vmSpace->archMmu, uva + (i << PAGE_SHIFT), pa, 1, uflags);//Map physical pages to user space
              if (err < 0) {
                  ret = err;
                  PRINT_ERR("%s, %d\n", __FUNCTION__, __LINE__);
                  break;
              }
          }
          /* if any failure happened, rollback | If a mapping failure occurs in the middle, roll back*/
          if (i != (region->range.size >> PAGE_SHIFT)) {
              while (i--) {
                  pa = LOS_PaddrQuery((VOID *)(UINTPTR)(kva + (i << PAGE_SHIFT)));//Query physical address
                  vmPage = LOS_VmPageGet(pa);//Get physical page box
                  (VOID)LOS_ArchMmuUnmap(&pcb->vmSpace->archMmu, uva + (i << PAGE_SHIFT), 1);//Unmap from user space
                  LOS_PhysPageFree(vmPage);//Release physical page
              }
          }
          (VOID)LOS_MuxRelease(&pcb->vmSpace->regionMux);
          return ret;
      }
    
  • The preparation for LiteIPC has been completed, and the next step is the operation / control phase

LiteIpcIoctl 𞓜 control

LITE_OS_SEC_TEXT int LiteIpcIoctl(struct file *filep, int cmd, unsigned long arg)
{
    UINT32 ret = LOS_OK;
    LosProcessCB *pcb = OsCurrProcessGet();
    ProcIpcInfo *ipcInfo = pcb->ipcInfo;
	// The whole system can only have one ServiceManager, while the Service can have multiple. ServiceManager has two main functions: one is responsible for the registration and logoff of services,
	// The second is to manage the access rights of the Service (only authorized tasks can send IPC messages to the corresponding Service).
    switch (cmd) {
        case IPC_SET_CMS:
            return SetCms(arg); //Set ServiceManager. The whole system can only have one ServiceManager
        case IPC_CMS_CMD: // Control commands, create / delete / add permissions 
            return HandleCmsCmd((CmsCmdContent *)(UINTPTR)arg);
        case IPC_SET_IPC_THREAD:
            return SetIpcTask();//Set the current task as the IPC task ID of the current process
        case IPC_SEND_RECV_MSG://Send and receive messages, representing the message content
            ret = LiteIpcMsgHandle((IpcContent *)(UINTPTR)arg);//Processing IPC messages
            break;
    }
    return ret;
}

unscramble

  • There are two main concepts in LiteIPC, one is ServiceManager and the other is Service. The whole system can only have one ServiceManager, while the Service can have multiple. ServiceManager has two main functions: one is responsible for the registration and logoff of services, and the other is responsible for managing the access rights of services (only authorized tasks can send IPC messages to the corresponding services). IPC_SET_CMS sets the ServiceManager command to IPC_CMS_CMD is the management command for Service.
  • IPC_SEND_RECV_MSG is the process of message processing. The encapsulation of messages is understood in combination with the previous article. The two functions corresponding to receiving and sending messages are LiteIpcRead and LiteIpcWrite.
  • LiteIpcWrite message writing refers to writing data from user space to kernel space. The task to which this message is written has been indicated in the message content body, so as to achieve the purpose of inter process (actually inter task) communication.
      ///Write IPC message queue from user space to kernel space
      LITE_OS_SEC_TEXT STATIC UINT32 LiteIpcWrite(IpcContent *content)
      {
          UINT32 ret, intSave;
          UINT32 dstTid;
          IpcMsg *msg = content->outMsg;
          LosTaskCB *tcb = OS_TCB_FROM_TID(dstTid);//Target task entity
          LosProcessCB *pcb = OS_PCB_FROM_PID(tcb->processID);//Target process entity
          if (pcb->ipcInfo == NULL) {
              PRINT_ERR("pid %u Liteipc not create\n", tcb->processID);
              return -EINVAL;
          }
          //Why apply MSG - > dataSz here? Because the real data body data in IpcMsg is a pointer, and its size is dataSz Apply for storage offset space at the same time
          UINT32 bufSz = sizeof(IpcListNode) + msg->dataSz + msg->spObjNum * sizeof(UINT32);//This sentence is the key to understand the data storage of upper layer messages in kernel space@ note_good
          IpcListNode *buf = (IpcListNode *)LiteIpcNodeAlloc(tcb->processID, bufSz);//Request bufSz size memory from kernel space
          if (buf == NULL) {
              PRINT_ERR("%s, %d\n", __FUNCTION__, __LINE__);
              return -ENOMEM;
          }//The first member variable of IpcListNode is IpcMsg
          ret = CopyDataFromUser(buf, bufSz, (const IpcMsg *)msg);//Copy the message content to the kernel space, including message control body + content body + offset
          if (ret != LOS_OK) {
              PRINT_ERR("%s, %d\n", __FUNCTION__, __LINE__);
              goto ERROR_COPY;
          }
          if (tcb->ipcTaskInfo == NULL) {//If the task has no IPC information
              tcb->ipcTaskInfo = LiteIpcTaskInit();//Initialize the IPC information module of this task because the message comes and needs to be processed
          }
          ret = HandleSpecialObjects(dstTid, buf, FALSE);//Processing messages
          if (ret != LOS_OK) {
              PRINT_ERR("%s, %d\n", __FUNCTION__, __LINE__);
              goto ERROR_COPY;
          }
          /* add data to list and wake up dest task *///Add data to the list and wake up the target task
          SCHEDULER_LOCK(intSave);
          LOS_ListTailInsert(&(tcb->ipcTaskInfo->msgListHead), &(buf->listNode));//Hang the message control body on the IPC linked list header of the target task
          OsHookCall(LOS_HOOK_TYPE_IPC_WRITE, &buf->msg, dstTid, tcb->processID, tcb->waitFlag);
          if (tcb->waitFlag == OS_TASK_WAIT_LITEIPC) {//If this task is waiting for this message, note that this tcb is not the current task
              OsTaskWakeClearPendMask(tcb);//Tear off the corresponding label
              OsSchedTaskWake(tcb);//Wake up task execution because the task is waiting to read IPC messages
              SCHEDULER_UNLOCK(intSave);
              LOS_MpSchedule(OS_MP_CPU_ALL);//Set the scheduling mode. All CPU cores are scheduled once. Do you want all CPUs to be scheduled here? 
              //Can you query which CPU the task is attached to and only schedule the corresponding CPU? The announcer throws a thinking @ note here_ thinking
              LOS_Schedule();//Initiate scheduling
          } else {
              SCHEDULER_UNLOCK(intSave);
          }
          return LOS_OK;
      ERROR_COPY:
          LiteIpcNodeFree(OS_TCB_FROM_TID(dstTid)->processID, buf);//If a copy error occurs, the kernel heap memory will be released. That's a big chunk of heap memory
          return ret;
      }  
    
    • The general process is to allocate kernel space and user space data from LiteIpc memory pool. Note that it must be allocated from LiteIpcNodeAlloc, and the reason code has also been indicated.
    • When there is data, hang the data to the IPC two-way linked list of the target task. If the task is waiting for a read message (OS_TASK_WAIT_LITEIPC), wake up the target task execution and initiate scheduling LOS_Schedule.
  • LiteIpcRead and LiteIpcWrite echo each other. Reading message refers to reading kernel space data to user space for processing.
    ///Read IPC message
      LITE_OS_SEC_TEXT STATIC UINT32 LiteIpcRead(IpcContent *content)
      {
          UINT32 intSave, ret;
          UINT32 selfTid = LOS_CurTaskIDGet();//Current task ID
          LOS_DL_LIST *listHead = NULL;
          LOS_DL_LIST *listNode = NULL;
          IpcListNode *node = NULL;
          UINT32 syncFlag = (content->flag & SEND) && (content->flag & RECV);//Sync tag
          UINT32 timeout = syncFlag ? LOS_MS2Tick(LITEIPC_TIMEOUT_MS) : LOS_WAIT_FOREVER;
          LosTaskCB *tcb = OS_TCB_FROM_TID(selfTid);//Get current task entity
          if (tcb->ipcTaskInfo == NULL) {//If the task has not been given IPC function
              tcb->ipcTaskInfo = LiteIpcTaskInit();//Initialize IPC for task
          }
          listHead = &(tcb->ipcTaskInfo->msgListHead);//Get IPC header node
          do {//Notice that this is a dead circle
              SCHEDULER_LOCK(intSave);
              if (LOS_ListEmpty(listHead)) {//Is the linked list empty?
                  OsTaskWaitSetPendMask(OS_TASK_WAIT_LITEIPC, OS_INVALID_VALUE, timeout);//Set the information for the current task to wait for
                  OsHookCall(LOS_HOOK_TYPE_IPC_TRY_READ, syncFlag ? MT_REPLY : MT_REQUEST, tcb->waitFlag);//Input the waiting log information to the hook module
                  ret = OsSchedTaskWait(&g_ipcPendlist, timeout, TRUE);//Hang the task to the global linked list, enter the IPC information such as the task, and wait for the time (timeout). The scheduling is generated here, and the task will be switched to other tasks for execution
                  //If the time-out message is not reached, it will be returned before the execution of a task_ ERRNO_ TSK_ TIMEOUT
                  if (ret == LOS_ERRNO_TSK_TIMEOUT) {//If the specified time has not arrived yet
                      OsHookCall(LOS_HOOK_TYPE_IPC_READ_TIMEOUT, syncFlag ? MT_REPLY : MT_REQUEST, tcb->waitFlag);//Reply / request timeout occurred while the print job was waiting for IPC
                      SCHEDULER_UNLOCK(intSave);
                      return -ETIME;
                  }
                  if (OsTaskIsKilled(tcb)) {//If an exception occurs that the task is killed
                      OsHookCall(LOS_HOOK_TYPE_IPC_KILL, syncFlag ? MT_REPLY : MT_REQUEST, tcb->waitFlag);//The print job was killed while waiting for IPC
                      SCHEDULER_UNLOCK(intSave);
                      return -ERFKILL;
                  }
                  SCHEDULER_UNLOCK(intSave);
              } else {//When there is IPC node data
                  listNode = LOS_DL_LIST_FIRST(listHead);//Get the first node
                  LOS_ListDelete(listNode);//Remove the node from the linked list and burn it after reading it
                  node = LOS_DL_LIST_ENTRY(listNode, IpcListNode, listNode);//Get node entity
                  SCHEDULER_UNLOCK(intSave);
                  ret = CheckRecievedMsg(node, content, tcb);//Check the information received
                  if (ret == LOS_OK) {//The information is OK
                      break;
                  }
                  if (ret == -ENOENT) { /* It means that we've recieved a failed reply | Abnormal reply received*/
                      return ret;
                  }
              }
          } while (1);
          node->msg.data = (VOID *)GetIpcUserAddr(LOS_GetCurrProcessID(), (INTPTR)(node->msg.data));//Convert to user space address
          node->msg.offsets = (VOID *)GetIpcUserAddr(LOS_GetCurrProcessID(), (INTPTR)(node->msg.offsets));//Offset converted to user space
          content->inMsg = (VOID *)GetIpcUserAddr(LOS_GetCurrProcessID(), (INTPTR)(&(node->msg)));//Convert to user spatial data structure
          EnableIpcNodeFreeByUser(LOS_GetCurrProcessID(), (VOID *)node);//Create an idle node and hang it to the process IPC used node list
          return LOS_OK;
      }
    
    • After scheduling to the target task, it will switch to LiteIpcRead for execution. At this time, the read function is going through a do While (1) the loop waits for the message to arrive. The last part of LiteIpcRead is the conversion of kernel address and user address, which is also the best part of LiteIpc. They point to the same piece of data.
    • When LiteIpcRead cannot read the message, that is, when the message chain list of the current task is empty, the task will set a waiting tag OS_TASK_WAIT_LITEIPC and suspend yourself. OsSchedTaskWait gives the CPU to other tasks to continue. Please understand the read-write function again and again.

Bai Wen said that the core | grasp the main context

  • Baiwen is equivalent to touching out the muscle and organ system of the kernel, which makes people start to feel plump and three-dimensional. Because it starts directly from the annotation source code, it is often sorted out every experience in the annotation process, and the following articles are slowly formed. The content is based on the source code and often uses life scenes as an analogy. Put the kernel knowledge points into a certain scene as much as possible, which has a sense of picture and is easy to understand and remember. It's important to say what others can understand! A hundred blogs are by no means Baidu's dogmatic talking about a bunch of awkward concepts, which is meaningless. More hope to make the kernel lifelike and feel more cordial.
  • Just as the code needs to be debug ged constantly, there will be many errors and omissions in the content of the article. Please forgive me, but it will be corrected repeatedly and updated continuously, V * XX represents the article serial number and the number of modifications. It is carefully crafted, concise and comprehensive, and strives to create high-quality content.
  • Bai Wen is posted at the "Hong Meng research station", "51CTO China CSDN", "the official account".

By function module:

Million note source code | buckle details everywhere

  • The purpose of annotating the core of millions of Chinese characters is to see its capillaries and cell structure clearly, which is equivalent to looking at the core with a magnifying glass. The kernel is not mysterious. It is easy to be addicted to find answers in the source code with questions. You will find that many articles interpret some problems incorrectly, or it is difficult to justify them if they are not profound. You will slowly form your own new interpretation, and the new interpretation will encounter new problems. You will advance layer by layer and roll forward. You are unwilling to let go with a magnifying glass.

  • < gitee | github | coding | codechina > The four big yards push the official source code, and the official account can be easily read back to millions.

Focus on not getting lost | code is life

Topics: gitee harmonyos liteos