v47.02 鸿蒙内核源码分析(进程回收篇) | 临终前如何向老祖宗托孤 | 百篇博客分析OpenHarmony源码

原创
04/13 16:53
阅读数 7.2K

子曰:“先进于礼乐,野人也;后进于礼乐,君子也。如用之,则吾从先进。” 《论语》:先进篇

在这里插入图片描述

百篇博客系列篇.本篇为: v47.xx 鸿蒙内核源码分析(进程回收篇) | 临终前如何向老祖宗托孤

进程管理相关篇为:

进程关系链

进程是家族式管理的,父子关系,兄弟关系,朋友关系,子女关系,甚至陌生人关系(等待你消亡)在一个进程的生命周期中都会记录下来.用什么来记录呢?当然是内核最重要的胶水结构体LOS_DL_LIST,进程控制块(以下简称PCB)用了8个双向链表来记录进程家族的基因关系和运行时关系.如下:

typedef struct ProcessCB {
    //...此处省略其他变量
    LOS_DL_LIST          pendList;                     /**< Block list to which the process belongs */ //进程所属的阻塞列表,如果因拿锁失败,就由此节点挂到等锁链表上
    LOS_DL_LIST          childrenList;                 /**< my children process list */	//孩子进程都挂到这里,形成双循环链表
    LOS_DL_LIST          exitChildList;                /**< my exit children process list */	//那些要退出孩子进程挂到这里,白发人送黑发人。
    LOS_DL_LIST          siblingList;                  /**< linkage in my parent's children list */ //兄弟进程链表, 56个民族是一家,来自同一个父进程.
    LOS_DL_LIST          subordinateGroupList;         /**< linkage in my group list */ //进程是组长时,有哪些组员进程
    LOS_DL_LIST          threadSiblingList;            /**< List of threads under this process *///进程的线程(任务)列表
    LOS_DL_LIST          threadPriQueueList[OS_PRIORITY_QUEUE_NUM]; /**< The process's thread group schedules thepriority hash table */	//进程的线程组调度优先级哈希表
    LOS_DL_LIST          waitList;     /**< The process holds the waitLits to support wait/waitpid *///进程持有等待链表以支持wait/waitpid
} LosProcessCB;

解读

  • pendList 个人认为它是鸿蒙内核功能最多的一个链表,它远不止字面意思阻塞链表这么简单,只有深入解读源码后才能体会它真的是太会来事了,一般把它理解为阻塞链表就行.上面挂的是处于阻塞状态的进程.
  • childrenList孩子链表,所有由它fork出来的进程都挂到这个链表上.上面的孩子进程在死亡前会将自己从上面摘出去,转而挂到exitChildList链表上.
  • exitChildList退出孩子链表,进入死亡程序的进程要挂到这个链表上,一个进程的死亡是件挺麻烦的事,进程池的数量有限,需要及时回收进程资源,但家族管理关系复杂,要去很多地方消除痕迹.尤其还有其他进程在看你笑话,等你死亡(wait/waitpid)了通知它们一声.
  • siblingList兄弟链表,和你同一个父亲的进程都挂到了这个链表上.
  • subordinateGroupList 朋友圈链表,里面是因为兴趣爱好(进程组)而挂在一起的进程,它们可以不是一个父亲,不是一个祖父,但一定是同一个老祖宗(用户态和内核态根进程).
  • threadSiblingList线程链表,上面挂的是进程ID都是这个进程的线程(任务),进程和线程的关系是1:N的关系,一个线程只能属于一个进程.这里要注意任务在其生命周期中是不能改所属进程的.
  • threadPriQueueList线程的调度队列数组,一共32个,任务和进程一样有32个优先级,调度算法的过程是先找到优先级最高的进程,在从该进程的任务队列里去最高的优先级任务运行.
  • waitList 是等待子进程消亡的任务链表,注意上面挂的是任务.任务是通过系统调用
      pid_t wait(int *status);
      pid_t waitpid(pid_t pid, int *status, int options);
    
    将任务挂到waitList上.鸿蒙waitpid系统调用为SysWait,稍后会讲.

进程正常死亡过程

一个进程的自然消亡过程如下

//一个进程的自然消亡过程,参数是当前运行的任务
STATIC VOID OsProcessNaturalExit(LosTaskCB *runTask, UINT32 status)
{
    LosProcessCB *processCB = OS_PCB_FROM_PID(runTask->processID);//通过task找到所属PCB
    LosProcessCB *parentCB = NULL;

    LOS_ASSERT(!(processCB->threadScheduleMap != 0));//断言没有任务需要调度了,当前task是最后一个了
    LOS_ASSERT(processCB->processStatus & OS_PROCESS_STATUS_RUNNING);//断言必须为正在运行的进程

    OsChildProcessResourcesFree(processCB);//释放孩子进程的资源

#ifdef LOSCFG_KERNEL_CPUP 
    OsCpupClean(processCB->processID);
#endif

    /* is a child process */
    if (processCB->parentProcessID != OS_INVALID_VALUE) {//判断是否有父进程
        parentCB = OS_PCB_FROM_PID(processCB->parentProcessID);//获取父进程实体
        LOS_ListDelete(&processCB->siblingList);//将自己从兄弟链表中摘除,家人们,永别了!
        if (!OsProcessExitCodeSignalIsSet(processCB)) {//是否设置了退出码?
            OsProcessExitCodeSet(processCB, status);//将进程状态设为退出码
        }
        LOS_ListTailInsert(&parentCB->exitChildList, &processCB->siblingList);//挂到父进程的孩子消亡链表,家人中,永别的可不止我一个.
        LOS_ListDelete(&processCB->subordinateGroupList);//和志同道合的朋友们永别了,注意家里可不一定是朋友的,所有各有链表.
        LOS_ListTailInsert(&processCB->group->exitProcessList, &processCB->subordinateGroupList);//挂到进程组消亡链表,朋友中,永别的可不止我一个.

        OsWaitCheckAndWakeParentProcess(parentCB, processCB);//检查父进程的等待任务并唤醒任务,此处将会切换到其他任务运行.

        OsDealAliveChildProcess(processCB);//老父亲临终向各自的祖宗托孤

        processCB->processStatus |= OS_PROCESS_STATUS_ZOMBIES;//贴上僵死进程的标签

        (VOID)OsKill(processCB->parentProcessID, SIGCHLD, OS_KERNEL_KILL_PERMISSION);//以内核权限发送SIGCHLD(子进程退出)信号.
        LOS_ListHeadInsert(&g_processRecyleList, &processCB->pendList);//将进程通过其阻塞节点挂入全局进程回收链表
        OsRunTaskToDelete(runTask);//删除正在运行的任务
        return;
    }

    LOS_Panic("pid : %u is the root process exit!\n", processCB->processID);
    return;
}

解读

  • 退群,向兄弟姐妹siblingList告别,向朋友圈(进程组)告别subordinateGroupList.
  • 留下你的死亡记录,老父亲记录到exitChildList,朋友圈记录到exitProcessList中.
  • 告诉后人死亡原因OsProcessExitCodeSet,因为waitList上挂的任务在等待你的死亡信息.
  • 向老祖宗托孤,用户态和内核态进程都有自己的祖宗进程(1和2号进程),老祖宗身子硬朗,最后死.所有的短命鬼进程都可以把自己的孩子委托给老祖宗照顾,老祖宗会一视同仁.
  • 将自己变成了OS_PROCESS_STATUS_ZOMBIES僵尸进程.
  • 老父亲跑到村口广播这个孩子已经死亡的信号OsKill.
  • 将自己挂入进程回收链表,等待回收任务ResourcesTask回收资源.
  • 最后删除这个正在运行的任务,很明显其中一定会发生一次调度OsSchedResched.
      //删除一个正在运行的任务
      LITE_OS_SEC_TEXT VOID OsRunTaskToDelete(LosTaskCB *taskCB)
      {
          LosProcessCB *processCB = OS_PCB_FROM_PID(taskCB->processID);//拿到task所属进程
          OsTaskReleaseHoldLock(processCB, taskCB);//task还锁
          OsTaskStatusUnusedSet(taskCB);//task重置为未使用状态,等待回收
    
          LOS_ListDelete(&taskCB->threadList);//从进程的线程链表中将自己摘除
          processCB->threadNumber--;//进程的活动task --,注意进程还有一个记录总task的变量 processCB->threadCount
          LOS_ListTailInsert(&g_taskRecyleList, &taskCB->pendList);//将task插入回收链表,等待回收资源再利用
          OsEventWriteUnsafe(&g_resourceEvent, OS_RESOURCE_EVENT_FREE, FALSE, NULL);//发送释放资源的事件,事件由 OsResourceRecoveryTask 消费
    
          OsSchedResched();//申请调度
          return;
      }
    
  • 但这是一个自然死亡的进程,还有很多非正常死亡在其他篇幅中已有说明.请自行翻看.非正常死亡的会产生僵尸进程.这种进程需要别的进程通过 waitpid来回收.

孤儿进程

一般情况下往往是白发人送黑发人,子进程的生命周期是要短于父进程.但因为fork之后,进程之间相互独立,调度算法一视同仁,父子之间是弱的关系力,就什么情况都可能发生了.内核是允许老父亲先走的,如果父进程退出而它的一个或多个子进程还在运行,那么这些子进程就被称为孤儿进程,孤儿进程最终将被两位老祖宗(用户态和内核态)所收养,并由老祖宗完成对它们的状态收集工作。

//当一个进程自然退出的时候,它的孩子进程由两位老祖宗收养
STATIC VOID OsDealAliveChildProcess(LosProcessCB *processCB)
{
    UINT32 parentID;
    LosProcessCB *childCB = NULL;
    LosProcessCB *parentCB = NULL;
    LOS_DL_LIST *nextList = NULL;
    LOS_DL_LIST *childHead = NULL;

    if (!LOS_ListEmpty(&processCB->childrenList)) {//如果存在孩子进程
        childHead = processCB->childrenList.pstNext;//获取孩子链表
        LOS_ListDelete(&(processCB->childrenList));//清空自己的孩子链表
        if (OsProcessIsUserMode(processCB)) {//是用户态进程
            parentID = g_userInitProcess;//用户态进程老祖宗
        } else {
            parentID = g_kernelInitProcess;//内核态进程老祖宗
        }

        for (nextList = childHead; ;) {//遍历孩子链表
            childCB = OS_PCB_FROM_SIBLIST(nextList);//找到孩子的真身
            childCB->parentProcessID = parentID;//孩子磕头认老祖宗为爸爸
            nextList = nextList->pstNext;//找下一个孩子进程
            if (nextList == childHead) {//一圈下来,孩子们都磕完头了
                break;
            }
        }

        parentCB = OS_PCB_FROM_PID(parentID);//找个老祖宗的真身
        LOS_ListTailInsertList(&parentCB->childrenList, childHead);//挂到老祖宗的孩子链表上
    }

    return;
}

解读

  • 函数很简单,都一一注释了,老父亲临终托付后事,请各自的老祖宗照顾孩子.
  • 从这里也可以看出进程的家族管理模式,两个家族从进程的出生到死亡负责到底.

僵尸进程

一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用wait或waitpid获取这些信息,然后彻底清除掉这个进程。

如果一个进程已经终止,但是它的父进程尚未调用wait或waitpid对它进行清理,这时的进程状态称为僵尸(Zombie)进程,即 Z 进程.任何进程在刚终止时都是僵尸进程,正常情况下,僵尸进程都立刻被父进程清理了. 不正常情况下就需要手动waitpid清理了.

waitpid

在鸿蒙系统中,一个进程结束了,但是它的父进程没有等待(调用wait waitpid)它,那么它将变成一个僵尸进程。通过系统调用 waitpid可以彻底的清理掉子进程.归还pcb.最终调用到SysWait

#include <sys/wait.h>
#include "syscall.h"

pid_t waitpid(pid_t pid, int *status, int options)
{
	return syscall_cp(SYS_wait4, pid, status, options, 0);
}
//等待子进程结束
int SysWait(int pid, USER int *status, int options, void *rusage)
{
    (void)rusage;

    return LOS_Wait(pid, status, (unsigned int)options, NULL);
}
//返回已经终止的子进程的进程ID号,并清除僵死进程。
LITE_OS_SEC_TEXT INT32 LOS_Wait(INT32 pid, USER INT32 *status, UINT32 options, VOID *rusage)
{
    (VOID)rusage;
    UINT32 ret;
    UINT32 intSave;
    LosProcessCB *childCB = NULL;
    LosProcessCB *processCB = NULL;
    LosTaskCB *runTask = NULL;

    ret = OsWaitOptionsCheck(options);//参数检查,只支持LOS_WAIT_WNOHANG
    if (ret != LOS_OK) {
        return -ret;
    }

    SCHEDULER_LOCK(intSave);
    processCB = OsCurrProcessGet();	//获取当前进程
    runTask = OsCurrTaskGet();		//获取当前任务

    ret = OsWaitChildProcessCheck(processCB, pid, &childCB);//先检查下看能不能找到参数要求的退出子进程
    if (ret != LOS_OK) {
        pid = -ret;
        goto ERROR;
    }

    if (childCB != NULL) {//找到了进程
        return OsWaitRecycleChildPorcess(childCB, intSave, status);//回收进程
    }
	//没有找到,看是否要返回还是去做个登记
    if ((options & LOS_WAIT_WNOHANG) != 0) {//有LOS_WAIT_WNOHANG标签
        runTask->waitFlag = 0;//等待标识置0
        pid = 0;//这里置0,是为了 return 0
        goto ERROR;
    }
	//等待孩子进程退出
    OsWaitInsertWaitListInOrder(runTask, processCB);//将当前任务挂入进程waitList链表
	//发起调度的目的是为了让出CPU,让其他进程/任务运行
    OsSchedResched();//发起调度
	
    runTask->waitFlag = 0;
    if (runTask->waitID == OS_INVALID_VALUE) {
        pid = -LOS_ECHILD;//没有此子进程
        goto ERROR;
    }

    childCB = OS_PCB_FROM_PID(runTask->waitID);//获取当前任务的等待子进程ID
    if (!(childCB->processStatus & OS_PROCESS_STATUS_ZOMBIES)) {//子进程非僵死进程
        pid = -LOS_ESRCH;//没有此进程
        goto ERROR;
    }
	//回收僵死进程
    return OsWaitRecycleChildPorcess(childCB, intSave, status);

ERROR:
    SCHEDULER_UNLOCK(intSave);
    return pid;
}

解读

  • pid是数据参数,根据不同的参数代表不同的含义,含义如下:

    参数值 说明
    pid<-1 等待进程组号为pid绝对值的任何子进程。
    pid=-1 等待任何子进程,此时的waitpid()函数就退化成了普通的wait()函数。
    pid=0 等待进程组号与目前进程相同的任何子进程,也就是说任何和调用waitpid()函数的进程在同一个进程组的进程。
    pid>0 等待进程号为pid的子进程。

    pid不同值代表的真正含义可以看这个函数OsWaitSetFlag.

    //设置等待子进程退出方式方法
        STATIC UINT32 OsWaitSetFlag(const LosProcessCB *processCB, INT32 pid, LosProcessCB **child)
        {
            LosProcessCB *childCB = NULL;
            ProcessGroup *group = NULL;
            LosTaskCB *runTask = OsCurrTaskGet();
            UINT32 ret;
    
            if (pid > 0) {//等待进程号为pid的子进程结束
                /* Wait for the child process whose process number is pid. */
                childCB = OsFindExitChildProcess(processCB, pid);//看能否从退出的孩子链表中找到PID
                if (childCB != NULL) {//找到了,确实有一个已经退出的PID,注意一个进程退出时会挂到父进程的exitChildList上
                    goto WAIT_BACK;//直接成功返回
                }
    
                ret = OsFindChildProcess(processCB, pid);//看能否从现有的孩子链表中找到PID
                if (ret != LOS_OK) {
                    return LOS_ECHILD;//参数进程并没有这个PID孩子,返回孩子进程失败.
                }
                runTask->waitFlag = OS_PROCESS_WAIT_PRO;//设置当前任务的等待类型
                runTask->waitID = pid;	//当前任务要等待进程ID结束
            } else if (pid == 0) {//等待同一进程组中的任何子进程
                /* Wait for any child process in the same process group */
                childCB = OsFindGroupExitProcess(processCB->group, OS_INVALID_VALUE);//看能否从退出的孩子链表中找到PID
                if (childCB != NULL) {//找到了,确实有一个已经退出的PID
                    goto WAIT_BACK;//直接成功返回
                }
                runTask->waitID = processCB->group->groupID;//等待进程组的任意一个子进程结束
                runTask->waitFlag = OS_PROCESS_WAIT_GID;//设置当前任务的等待类型
            } else if (pid == -1) {//等待任意子进程
                /* Wait for any child process */
                childCB = OsFindExitChildProcess(processCB, OS_INVALID_VALUE);//看能否从退出的孩子链表中找到PID
                if (childCB != NULL) {//找到了,确实有一个已经退出的PID
                    goto WAIT_BACK;
                }
                runTask->waitID = pid;//等待PID,这个PID可以和当前进程没有任何关系
                runTask->waitFlag = OS_PROCESS_WAIT_ANY;//设置当前任务的等待类型
            } else { /* pid < -1 */ //等待指定进程组内为|pid|的所有子进程
                /* Wait for any child process whose group number is the pid absolute value. */
                group = OsFindProcessGroup(-pid);//先通过PID找到进程组
                if (group == NULL) {
                    return LOS_ECHILD;
                }
    
                childCB = OsFindGroupExitProcess(group, OS_INVALID_VALUE);//在进程组里任意一个已经退出的子进程
                if (childCB != NULL) {
                    goto WAIT_BACK;
                }
    
                runTask->waitID = -pid;//此处用负数是为了和(pid == 0)以示区别,因为二者的waitFlag都一样.
                runTask->waitFlag = OS_PROCESS_WAIT_GID;//设置当前任务的等待类型
            }
    
        WAIT_BACK:
            *child = childCB;
            return LOS_OK;
        }
    
  • status带走进程退出码,exitCode分成了三个部分格式如下

      /*
      * Process exit code
      * 31    15           8           7        0
      * |     | exit code  | core dump | signal |
      */
      #define OS_PRO_EXIT_OK 0 //进程正常退出
      //置进程退出码第七位为1
      STATIC INLINE VOID OsProcessExitCodeCoreDumpSet(LosProcessCB *processCB)
      {
          processCB->exitCode |= 0x80U;// 0b10000000 
      }
      //设置进程退出信号(0 ~ 7)
      STATIC INLINE VOID OsProcessExitCodeSignalSet(LosProcessCB *processCB, UINT32 signal)
      {
          processCB->exitCode |= signal & 0x7FU;//0b01111111
      }
      //清除进程退出信号(0 ~ 7)
      STATIC INLINE VOID OsProcessExitCodeSignalClear(LosProcessCB *processCB)
      {
          processCB->exitCode &= (~0x7FU);//低7位全部清0
      }
      //进程退出码是否被设置过,默认是 0 ,如果 & 0x7FU 还是 0 ,说明没有被设置过.
      STATIC INLINE BOOL OsProcessExitCodeSignalIsSet(LosProcessCB *processCB)
      {
          return (processCB->exitCode) & 0x7FU;
      }
      //设置进程退出号(8 ~ 15)
      STATIC INLINE VOID OsProcessExitCodeSet(LosProcessCB *processCB, UINT32 code)
      {
          processCB->exitCode |= ((code & 0x000000FFU) << 8U) & 0x0000FF00U; /* 8: Move 8 bits to the left, exitCode */
      }
    

    0 - 7为信号位,信号处理有专门的篇幅,此处不做详细介绍,请自行翻看,这里仅列出部分信号含义.

      #define SIGHUP    1	//终端挂起或者控制进程终止
      #define SIGINT    2	//键盘中断(如break键被按下)
      #define SIGQUIT   3	//键盘的退出键被按下
      #define SIGILL    4	//非法指令
      #define SIGTRAP   5	//跟踪陷阱(trace trap),启动进程,跟踪代码的执行
      #define SIGABRT   6	//由abort(3)发出的退出指令
      #define SIGIOT    SIGABRT //abort发出的信号
      #define SIGBUS    7	//总线错误 
      #define SIGFPE    8	//浮点异常
      #define SIGKILL   9		//常用的命令 kill 9 123 | 不能被忽略、处理和阻塞
      #define SIGUSR1   10	//用户自定义信号1 
      #define SIGSEGV   11	//无效的内存引用, 段违例(segmentation     violation),进程试图去访问其虚地址空间以外的位置 
      #define SIGUSR2   12	//用户自定义信号2
      #define SIGPIPE   13	//向某个非读管道中写入数据 
      #define SIGALRM   14	//由alarm(2)发出的信号,默认行为为进程终止
      #define SIGTERM   15	//终止信号
      #define SIGSTKFLT 16	//栈溢出
      #define SIGCHLD   17	//子进程结束信号
      #define SIGCONT   18	//进程继续(曾被停止的进程)
      #define SIGSTOP   19	//终止进程  	 | 不能被忽略、处理和阻塞
      #define SIGTSTP   20	//控制终端(tty)上 按下停止键
      #define SIGTTIN   21	//进程停止,后台进程企图从控制终端读
      #define SIGTTOU   22	//进程停止,后台进程企图从控制终端写
      #define SIGURG    23	//I/O有紧急数据到达当前进程
      #define SIGXCPU   24	//进程的CPU时间片到期
      #define SIGXFSZ   25	//文件大小的超出上限
      #define SIGVTALRM 26	//虚拟时钟超时
      #define SIGPROF   27	//profile时钟超时
      #define SIGWINCH  28	//窗口大小改变
      #define SIGIO     29	//I/O相关
      #define SIGPOLL   29	//
      #define SIGPWR    30	//电源故障,关机
      #define SIGSYS    31	//系统调用中参数错,如系统调用号非法 
      #define SIGUNUSED SIGSYS		//系统调用异常
    
  • options是行为参数,提供了一些另外的选项来控制waitpid()函数的行为。

    参数值 鸿蒙支持 说明
    LOS_WAIT_WNOHANG 支持 如果没有孩子进程退出,则立即返回,而不是阻塞在这个函数上等待;如果结束了,则返回该子进程的进程号。
    LOS_WAIT_WUNTRACED 不支持 报告终止或停止的子进程的状态
    LOS_WAIT_WCONTINUED 不支持

    鸿蒙目前只支持了LOS_WAIT_WNOHANG模式,内核源码中虽有LOS_WAIT_WUNTRACEDLOS_WAIT_WCONTINUED的实现痕迹,但是整体阅读下来比较乱,应该是没有写好.

百篇博客分析.深挖内核地基

  • 给鸿蒙内核源码加注释过程中,整理出以下文章。内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感,容易理解记忆。说别人能听得懂的话很重要! 百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思。更希望让内核变得栩栩如生,倍感亲切.确实有难度,自不量力,但已经出发,回头已是不可能的了。 😛
  • 与代码有bug需不断debug一样,文章和注解内容会存在不少错漏之处,请多包涵,但会反复修正,持续更新,v**.xx 代表文章序号和修改的次数,精雕细琢,言简意赅,力求打造精品内容。

按功能模块:

基础工具 加载运行 进程管理 编译构建
双向链表 位图管理 用栈方式 定时器 原子操作 时间管理 ELF格式 ELF解析 静态链接 重定位 进程映像 进程管理 进程概念 Fork 特殊进程 进程回收 信号生产 信号消费 Shell编辑 Shell解析 编译环境 编译过程 环境脚本 构建工具 gn应用 忍者ninja
进程通讯 内存管理 前因后果 任务管理
自旋锁 互斥锁 进程通讯 信号量 事件控制 消息队列 内存分配 内存管理 内存汇编 内存映射 内存规则 物理内存 总目录 调度故事 内存主奴 源码注释 源码结构 静态站点 时钟任务 任务调度 任务管理 调度队列 调度机制 线程概念 并发并行 CPU 系统调用 任务切换
文件系统 硬件架构
文件概念 文件系统 索引节点 挂载目录 根文件系统 字符设备 VFS 文件句柄 管道文件 汇编基础 汇编传参 工作模式 寄存器 异常接管 汇编汇总 中断切换 中断概念 中断管理

百万汉字注解.精读内核源码

WeHarmony/kernel_liteos_a_note

鸿蒙研究站 | 每天死磕一点点,原创不易,欢迎转载,但请注明出处。

展开阅读全文
打赏
1
2 收藏
分享
加载中
更多评论
打赏
0 评论
2 收藏
1
分享
返回顶部
顶部