百篇博客系列篇.本篇为:

任务管理相关篇为:

本篇说清楚任务的问题

在鸿蒙内核线程(thread)就是任务(task),也可以叫作业.线程是对外的说法,对内就叫任务.跟王二毛一样, 在公司叫你王董,回到家里还有领导,就叫二毛啊.这多亲切.在鸿蒙内核是大量的task,很少看到thread,只出现在posix层.当一个东西理解就行.

读本篇之前建议先阅读

typedef struct {
VOID *stackPointer; /**< Task stack pointer */ //内核态栈指针,SP位置,切换任务时先保存上下文并指向TaskContext位置
UINT16 taskStatus; /**< Task status */ //各种状态标签,可以拥有多种标签,按位标识
UINT16 priority; /**< Task priority */ //任务优先级[0:31],默认是31级
UINT16 policy; //任务的调度方式(三种 .. LOS_SCHED_RR )
UINT16 timeSlice; /**< Remaining time slice *///剩余时间片
UINT32 stackSize; /**< Task stack size */ //非用户模式下栈大小
UINTPTR topOfStack; /**< Task stack top */ //非用户模式下的栈顶 bottom = top + size
UINT32 taskID; /**< Task ID */ //任务ID,任务池本质是一个大数组,ID就是数组的索引,默认 < 128
TSK_ENTRY_FUNC taskEntry; /**< Task entrance function */ //任务执行入口函数
VOID *joinRetval; /**< pthread adaption */ //用来存储join线程的返回值
VOID *taskSem; /**< Task-held semaphore */ //task在等哪个信号量
VOID *taskMux; /**< Task-held mutex */ //task在等哪把锁
VOID *taskEvent; /**< Task-held event */ //task在等哪个事件
UINTPTR args[4]; /**< Parameter, of which the maximum number is 4 */ //入口函数的参数 例如 main (int argc,char *argv[])
CHAR taskName[OS_TCB_NAME_LEN]; /**< Task name */ //任务的名称
LOS_DL_LIST pendList; /**< Task pend node */ //如果任务阻塞时就通过它挂到各种阻塞情况的链表上,比如OsTaskWait时
LOS_DL_LIST threadList; /**< thread list */ //挂到所属进程的线程链表上
SortLinkList sortList; /**< Task sortlink node */ //挂到cpu core 的任务执行链表上
UINT32 eventMask; /**< Event mask */ //事件屏蔽
UINT32 eventMode; /**< Event mode */ //事件模式
UINT32 priBitMap; /**< BitMap for recording the change of task priority, //任务在执行过程中优先级会经常变化,这个变量用来记录所有曾经变化
the priority can not be greater than 31 */ //过的优先级,例如 ..01001011 曾经有过 0,1,3,6 优先级
INT32 errorNo; /**< Error Num */
UINT32 signal; /**< Task signal */ //任务信号类型,(SIGNAL_NONE,SIGNAL_KILL,SIGNAL_SUSPEND,SIGNAL_AFFI)
sig_cb sig; //信号控制块,这里用于进程间通讯的信号,类似于 linux singal模块
#if (LOSCFG_KERNEL_SMP == YES)
UINT16 currCpu; /**< CPU core number of this task is running on */ //正在运行此任务的CPU内核号
UINT16 lastCpu; /**< CPU core number of this task is running on last time */ //上次运行此任务的CPU内核号
UINT16 cpuAffiMask; /**< CPU affinity mask, support up to 16 cores */ //CPU亲和力掩码,最多支持16核,亲和力很重要,多核情况下尽量一个任务在一个CPU核上运行,提高效率
UINT32 timerCpu; /**< CPU core number of this task is delayed or pended */ //此任务的CPU内核号被延迟或挂起
#if (LOSCFG_KERNEL_SMP_TASK_SYNC == YES)
UINT32 syncSignal; /**< Synchronization for signal handling */ //用于CPU之间 同步信号
#endif
#if (LOSCFG_KERNEL_SMP_LOCKDEP == YES) //死锁检测开关
LockDep lockDep;
#endif
#if (LOSCFG_KERNEL_SCHED_STATISTICS == YES) //调度统计开关,显然打开这个开关性能会受到影响,鸿蒙默认是关闭的
SchedStat schedStat; /**< Schedule statistics */ //调度统计
#endif
#endif
UINTPTR userArea; //使用区域,由运行时划定,根据运行态不同而不同
UINTPTR userMapBase; //用户模式下的栈底位置
UINT32 userMapSize; /**< user thread stack size ,real size : userMapSize + USER_STACK_MIN_SIZE */
UINT32 processID; /**< Which belong process *///所属进程ID
FutexNode futex; //实现快锁功能
LOS_DL_LIST joinList; /**< join list */ //联结链表,允许任务之间相互释放彼此
LOS_DL_LIST lockList; /**< Hold the lock list */ //拿到了哪些锁链表
UINT32 waitID; /**< Wait for the PID or GID of the child process */ //等待孩子的PID或GID进程
UINT16 waitFlag; /**< The type of child process that is waiting, belonging to a group or parent,
a specific child process, or any child process */
#if (LOSCFG_KERNEL_LITEIPC == YES)
UINT32 ipcStatus; //IPC状态
LOS_DL_LIST msgListHead; //消息队列头结点,上面挂的都是任务要读的消息
BOOL accessMap[LOSCFG_BASE_CORE_TSK_LIMIT];//访问图,指的是task之间是否能访问的标识,LOSCFG_BASE_CORE_TSK_LIMIT 为任务池总数
#endif
} LosTaskCB;

结构体还是比较复杂,虽一一都做了注解,但还是不够清晰,没有模块化.这里把它分解成以下六大块逐一分析:

第一大块:多核CPU相关块

#if (LOSCFG_KERNEL_SMP == YES) //多CPU核支持
UINT16 currCpu; /**< CPU core number of this task is running on */ //正在运行此任务的CPU内核号
UINT16 lastCpu; /**< CPU core number of this task is running on last time */ //上次运行此任务的CPU内核号
UINT16 cpuAffiMask; /**< CPU affinity mask, support up to 16 cores */ //CPU亲和力掩码,最多支持16核,亲和力很重要,多核情况下尽量一个任务在一个CPU核上运行,提高效率
UINT32 timerCpu; /**< CPU core number of this task is delayed or pended */ //此任务的CPU内核号被延迟或挂起
#if (LOSCFG_KERNEL_SMP_TASK_SYNC == YES)
UINT32 syncSignal; /**< Synchronization for signal handling */ //用于CPU之间 同步信号
#endif
#if (LOSCFG_KERNEL_SMP_LOCKDEP == YES) //死锁检测开关
LockDep lockDep;
#endif
#if (LOSCFG_KERNEL_SCHED_STATISTICS == YES) //调度统计开关,显然打开这个开关性能会受到影响,鸿蒙默认是关闭的
SchedStat schedStat; /**< Schedule statistics */ //调度统计
#endif
#endif

鸿蒙内核支持多CPU,谁都知道多CPU当然好,效率高,快嘛,但凡事有两面性,在享受一个东西带来好处的同时,也得承担伴随它一起带来的麻烦和风险.多核有哪些的好处和麻烦,这里不展开说,后续有专门的文章和视频说明.任务可叫线程,或叫作业.CPU就是做作业的,多个CPU就是有多个能做作业的,一个作业能一鼓作气做完吗?

答案是:往往不行,因为现实不允许,作业可以有N多,而CPU数量非常有限,所以经常做着A作业被老板打断让去做B作业.这老板就是调度算法.A作业被打断回来接着做的还会是原来那个CPU吗?

答案是:不一定. 变量cpuAffiMask叫CPU亲和力,它的作用是可以指定A的作业始终是同一个CPU来完成, 也可以随便,交给调度算法,分到谁就谁来,这方面可以不挑.

第二大块:栈空间

    VOID            *stackPointer;      /**< Task stack pointer */  //内核态栈指针,SP位置,切换任务时先保存上下文并指向TaskContext位置.
UINT32 stackSize; /**< Task stack size */ //内核态栈大小
UINTPTR topOfStack; /**< Task stack top */ //内核态栈顶 bottom = top + size UINTPTR userArea; //使用区域,由运行时划定,根据运行态不同而不同
UINTPTR userMapBase; //用户态下的栈底位置
UINT32 userMapSize; /**< user thread stack size ,real size : userMapSize + USER_STACK_MIN_SIZE */

进程分内核态进程和用户态进程,这个区别表现在线程(任务)层面上就是

  • 内核态进程下创建的任务只有内核态的栈空间,OsTaskStackAlloc负责内核态栈空间的分配.OsTaskStackInit负责对内核态栈的初始化.

    //任务栈初始化,非常重要的函数,返回任务上下文
    LITE_OS_SEC_TEXT_INIT VOID *OsTaskStackInit(UINT32 taskID, UINT32 stackSize, VOID *topStack, BOOL initFlag)
    {
    UINT32 index = 1;
    TaskContext *taskContext = NULL; if (initFlag == TRUE) {
    OsStackInit(topStack, stackSize);
    }
    taskContext = (TaskContext *)(((UINTPTR)topStack + stackSize) - sizeof(TaskContext));//上下文存放在栈的底部 /* initialize the task context */ //初始化任务上下文
    #ifdef LOSCFG_GDB
    taskContext->PC = (UINTPTR)OsTaskEntrySetupLoopFrame;
    #else
    taskContext->PC = (UINTPTR)OsTaskEntry;//程序计数器,CPU首次执行task时跑的第一条指令位置
    #endif
    taskContext->LR = (UINTPTR)OsTaskExit; /* LR should be kept, to distinguish it's THUMB or ARM instruction */
    taskContext->resved = 0x0;
    taskContext->R[0] = taskID; /* R0 */
    taskContext->R[index++] = 0x01010101; /* R1, 0x01010101 : reg initialed magic word */ //0x55
    for (; index < GEN_REGS_NUM; index++) {//R2 - R12的初始化很有意思,为什么要这么做?
    taskContext->R[index] = taskContext->R[index - 1] + taskContext->R[1]; /* R2 - R12 */
    }//R[2]=R[2]<<1=0xAA #ifdef LOSCFG_INTERWORK_THUMB // 16位模式
    taskContext->regPSR = PSR_MODE_SVC_THUMB; /* CPSR (Enable IRQ and FIQ interrupts, THUMNB-mode) */
    #else //用于设置CPSR寄存器
    taskContext->regPSR = PSR_MODE_SVC_ARM; /* CPSR (Enable IRQ and FIQ interrupts, ARM-mode) */
    #endif #if !defined(LOSCFG_ARCH_FPU_DISABLE)
    /* 0xAAA0000000000000LL : float reg initialed magic word */
    for (index = 0; index < FP_REGS_NUM; index++) {
    taskContext->D[index] = 0xAAA0000000000000LL + index; /* D0 - D31 */
    }
    taskContext->regFPSCR = 0;
    taskContext->regFPEXC = FP_EN;
    #endif return (VOID *)taskContext;
    }

    可以看到,初始化了任务上下文(TaskContext),并将任务上下文放在了栈底,初始化任务上下文目的是为了在运行阶段先初始化R0~R15,CPSR寄存器的值.

    保存上下文和恢复上下文都是针对寄存器值而言的.这个工作是在内核态的栈中完成的,也就是说一个任务的上下文就是保存在任务的内核态栈中.

    OsTaskStackInit的返回值将赋给stackPointer,即寄存器SP

  • 用户态进程下创建的任务除了有内核态的栈空间外,还有用户态栈空间.

      //用户任务使用栈初始化
    LITE_OS_SEC_TEXT_INIT VOID OsUserTaskStackInit(TaskContext *context, TSK_ENTRY_FUNC taskEntry, UINTPTR stack)
    {
    LOS_ASSERT(context != NULL); #ifdef LOSCFG_INTERWORK_THUMB
    context->regPSR = PSR_MODE_USR_THUMB;
    #else
    context->regPSR = PSR_MODE_USR_ARM;//工作模式:用户模式 + 工作状态:arm
    #endif
    context->R[0] = stack;//栈指针给r0寄存器
    context->SP = TRUNCATE(stack, LOSCFG_STACK_POINT_ALIGN_SIZE);//异常模式所专用的堆栈 segment fault 输出回溯信息
    context->LR = 0;//保存子程序返回地址 例如 a call b ,在b中保存 a地址
    context->PC = (UINTPTR)taskEntry;//入口函数
    }

    注意看里面的内容用户栈的初始化时修改了任务的上下文内容,任务的上下文内容是始终保存在内核栈中,注意这个不要搞混了.OsUserTaskStackInit只是修改上下文地址中的内容.

    context->SP的值被修改了,这个修改意味着任务被调度后首先是恢复上下文,即要重置SP寄存器的值,SP的值将被变成context->SP,由此就指向了用户栈空间运行

    context->PC也被改变了,这意味着入口地址(代码段位置)也改变了.

    context->LR默认是为0,不跳转到任务地方.

    在后续每次调度上下文切换过程中,context的内容将不断的变化.

第三大块:资源竞争/同步

    VOID            *taskSem;           /**< Task-held semaphore */ //task在等哪个信号量
VOID *taskMux; /**< Task-held mutex */ //task在等哪把锁
VOID *taskEvent; /**< Task-held event */ //task在等哪个事件
UINT32 eventMask; /**< Event mask */ //事件屏蔽
UINT32 eventMode; /**< Event mode */ //事件模式
FutexNode futex; //实现快锁功能
LOS_DL_LIST joinList; /**< join list */ //联结链表,允许任务之间相互释放彼此
LOS_DL_LIST lockList; /**< Hold the lock list */ //拿到了哪些锁链表
UINT32 signal; /**< Task signal */ //任务信号类型,(SIGNAL_NONE,SIGNAL_KILL,SIGNAL_SUSPEND,SIGNAL_AFFI)
sig_cb sig;

公司的资源是有限的,CPU自己也是公司的资源,除了它还有其他的设备,比如做作业用的黑板,用户A,B,C都可能用到,狼多肉少,咋搞?

互斥量(taskMux,futex)能解决这个问题,办事前先拿锁,拿到了锁的爽了,没有拿到的就需要排队,在lockList上排队,注意lockList是个双向链表,它是内核最重要的结构体,开篇就提过,没印象的看这篇 鸿蒙内核源码分析(双向链表篇) | 谁是内核最重要结构体?,上面挂都是等锁进房间的西门大官人.这是互斥量的原理,解决任务间资源紧张的竞争性问题.

另外一个是用于任务的同步的信号量(sig_cb),任务和任务之间是会有关联的,现实生活中公司的A,B用户之间本身有业务往来的正常,CPU在帮B做作业的时候发现前置条件是需要A完成某项作业才能进行,这时B就需要主动让出CPU先办完A的事.这就是信号量的原理,解决的是任务间的同步问题.

第四大块:任务调度

前面说过了作业N多,做作业的只有几个人,单核CPU等于只有一个人干活.那要怎么分配CPU,就需要调度算法.

    UINT16          taskStatus;         /**< Task status */   //各种状态标签,可以拥有多种标签,按位标识
UINT16 priority; /**< Task priority */ //任务优先级[0:31],默认是31级
UINT16 policy; //任务的调度方式(三种 .. LOS_SCHED_RR )
UINT16 timeSlice; /**< Remaining time slice *///剩余时间片
CHAR taskName[OS_TCB_NAME_LEN]; /**< Task name */ //任务的名称
LOS_DL_LIST pendList; /**< Task pend node */ //如果任务阻塞时就通过它挂到各种阻塞情况的链表上,比如OsTaskWait时
LOS_DL_LIST threadList; /**< thread list */ //挂到所属进程的线程链表上
SortLinkList sortList; /**< Task sortlink node */ //挂到cpu core 的任务执行链表上

是简单的先来后到(FIFO)吗? 当然也支持这个方式.鸿蒙内核用的是抢占式调度(policy),就是可以插队,比优先级(priority)大小,[0,31]级,数字越大的优先级越低,跟考试一样,排第一才是最牛的.

鸿蒙排0的最牛! 想也想得到内核的任务优先级都是很高的,比如资源回收任务排第5,定时器任务排第0.够牛了吧.普通老百姓排多少呢?默认28级,惨!!!

另外任务有时间限制timeSlice,叫时间片,默认20ms,用完了会给你重置,发起重新调度,找出优先级高的执行,阻塞的任务(比如没拿到锁的,等信号量同步的,等读写消息队列的)都挂到pendList上,方便管理.

第五大块:任务间通讯

#if (LOSCFG_KERNEL_LITEIPC == YES)
UINT32 ipcStatus; //IPC状态
LOS_DL_LIST msgListHead; //消息队列头结点,上面挂的都是任务要读的消息
BOOL accessMap[LOSCFG_BASE_CORE_TSK_LIMIT];//访问图,指的是task之间是否能访问的标识,LOSCFG_BASE_CORE_TSK_LIMIT 为任务池总数
#endif

这个很重要,解决任务间通讯问题,要知道进程负责的是资源的管理功能,什么意思?就是它不并负责内容的生产和消费,它只负责管理确保你的内容到达率和完整性.生产者和消费者始终是任务.进程管了哪些东西系列篇有专门的文章,请自行翻看.

liteipc是鸿蒙专有的通讯消息队列实现.简单说它是基于文件的,而传统的ipc消息队列是基于内存的.有什么区别也不在这里讨论,已有专门的文章分析.

第六大块:辅助工具

要知道任务对内核来说太重要了,是任务让CPU忙里忙外的,那中间出差错了怎么办,怎么诊断你问题出哪里了,就需要一些工具,比如死锁检测,比如占用CPU,内存监控 如下:

#if (LOSCFG_KERNEL_SMP_LOCKDEP == YES) //死锁检测开关
LockDep lockDep;
#endif
#if (LOSCFG_KERNEL_SCHED_STATISTICS == YES) //调度统计开关,显然打开这个开关性能会受到影响,鸿蒙默认是关闭的
SchedStat schedStat; /**< Schedule statistics */ //调度统计
#endif

以上就是任务的五脏六腑,看清楚它鸿蒙内核的影像会清晰很多!

鸿蒙内核源码分析.总目录

v08.xx 鸿蒙内核源码分析(总目录) | 百万汉字注解 百篇博客分析 | 51.c.h .o

百万汉字注解.百篇博客分析

百万汉字注解 >> 精读鸿蒙源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< gitee| github| csdn| coding >

百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点定期更新中< 51cto| csdn| harmony| osc >

关注不迷路.代码即人生

QQ群:790015635 | 入群密码: 666

原创不易,欢迎转载,但请注明出处.

鸿蒙内核源码分析(线程概念篇) | 是谁在不停的折腾CPU? | 百篇博客分析OpenHarmony源码 | v21.06的更多相关文章

  1. 鸿蒙内核源码分析(中断概念篇) | 海公公的日常工作 | 百篇博客分析OpenHarmony源码 | v43.02

    百篇博客系列篇.本篇为: v43.xx 鸿蒙内核源码分析(中断概念篇) | 海公公的日常工作 | 51.c.h .o 硬件架构相关篇为: v22.xx 鸿蒙内核源码分析(汇编基础篇) | CPU在哪里 ...

  2. 鸿蒙内核源码分析(并发并行篇) | 听过无数遍的两个概念 | 百篇博客分析OpenHarmony源码 | v25.01

    百篇博客系列篇.本篇为: v25.xx 鸿蒙内核源码分析(并发并行篇) | 听过无数遍的两个概念 | 51.c.h .o 任务管理相关篇为: v03.xx 鸿蒙内核源码分析(时钟任务篇) | 触发调度 ...

  3. 鸿蒙内核源码分析(进程概念篇) | 进程在管理哪些资源 | 百篇博客分析OpenHarmony源码 | v24.01

    百篇博客系列篇.本篇为: v24.xx 鸿蒙内核源码分析(进程概念篇) | 进程在管理哪些资源 | 51.c.h .o 进程管理相关篇为: v02.xx 鸿蒙内核源码分析(进程管理篇) | 谁在管理内 ...

  4. 鸿蒙内核源码分析(索引节点篇) | 谁是文件系统最重要的概念 | 百篇博客分析OpenHarmony源码 | v64.01

    百篇博客系列篇.本篇为: v64.xx 鸿蒙内核源码分析(索引节点篇) | 谁是文件系统最重要的概念 | 51.c.h.o 文件系统相关篇为: v62.xx 鸿蒙内核源码分析(文件概念篇) | 为什么 ...

  5. 鸿蒙内核源码分析(文件概念篇) | 为什么说一切皆是文件 | 百篇博客分析OpenHarmony源码 | v62.01

    百篇博客系列篇.本篇为: v62.xx 鸿蒙内核源码分析(文件概念篇) | 为什么说一切皆是文件 | 51.c.h.o 本篇开始说文件系统,它是内核五大模块之一,甚至有Linux的设计哲学是" ...

  6. 鸿蒙内核源码分析(Shell编辑篇) | 两个任务,三个阶段 | 百篇博客分析OpenHarmony源码 | v71.01

    子曰:"我非生而知之者,好古,敏以求之者也." <论语>:述而篇 百篇博客系列篇.本篇为: v71.xx 鸿蒙内核源码分析(Shell编辑篇) | 两个任务,三个阶段 ...

  7. 鸿蒙内核源码分析(文件句柄篇) | 深挖应用操作文件的细节 | 百篇博客分析OpenHarmony源码 | v69.01

    百篇博客系列篇.本篇为: v69.xx 鸿蒙内核源码分析(文件句柄篇) | 深挖应用操作文件的细节 | 51.c.h.o 文件系统相关篇为: v62.xx 鸿蒙内核源码分析(文件概念篇) | 为什么说 ...

  8. 鸿蒙内核源码分析(进程镜像篇)|ELF是如何被加载运行的? | 百篇博客分析OpenHarmony源码 | v56.01

    百篇博客系列篇.本篇为: v56.xx 鸿蒙内核源码分析(进程映像篇) | ELF是如何被加载运行的? | 51.c.h.o 加载运行相关篇为: v51.xx 鸿蒙内核源码分析(ELF格式篇) | 应 ...

  9. 鸿蒙内核源码分析(信号生产篇) | 信号安装和发送过程是怎样的? | 百篇博客分析OpenHarmony源码 | v48.03

    百篇博客系列篇.本篇为: v48.xx 鸿蒙内核源码分析(信号生产篇) | 年过半百,依然活力十足 | 51.c.h .o 进程管理相关篇为: v02.xx 鸿蒙内核源码分析(进程管理篇) | 谁在管 ...

随机推荐

  1. ESP32CAM 人脸识别追踪

    引言 总体实现的流程:ESP32cam作为客户端,pc作为服务端通过mqtt协议建立通信,将采集的图像在电脑端显示人脸识别的方法使用的是opencv,并通过mqtt传输指令给esp32cam控制舵机云 ...

  2. javascript html 鼠标放大镜效果

    1.鼠标放大镜效果 鼠标放大镜效果,将鼠标移入到左图片,则可以在其右边看到放大的图片,且鼠标移动滑块的大小即为右图显示图片.实际效果如下图所示:   <!DOCTYPE html> < ...

  3. 使用JS获取SessionStorage的值

    参考:https://www.jb51.net/article/132729.htm 获取sessionStorage的意义 首先获取它是为了将获得的信息输出或者alert():让人容易看到, 其次, ...

  4. C# 查询所有设备的插拔事件

    private void test() { //Win32_DeviceChangeEvent  Win32_VolumeChangeEvent ManagementEventWatcher watc ...

  5. 深入浅出Mybatis系列(九)---缓存

    MyBatis系统中默认定义了两级缓存:一级缓存和二级缓存. 1.默认情况下,只有一级缓存(SqlSession级别的缓存,也称为本地缓存)开启. 2.二级缓存需要手动开启和配置,他是基于namesp ...

  6. hive简单数据分析

    简单查询分析 select brand_id from user_log limit 10; -- 查看日志前10数据 好像也没啥,和SQL相同,,limit取前多少条  , as取别名 查询条数统计 ...

  7. 区间k大数训练

    问题描述 给定一个序列,每次询问序列中第l个数到第r个数中第K大的数是哪个. 输入格式 第一行包含一个数n,表示序列长度. 第二行包含n个正整数,表示给定的序列. 第三个包含一个正整数m,表示询问个数 ...

  8. servlet中servletContext的五大作用(五)

    1.    获取web的上下文路径 2.    获取全局的参数 3.    作为域对象使用 4.    请求转发 5.    读取web项目的资源文件 package day10.about_serv ...

  9. Java事件模型

    1 import javax.swing.*; 2 import java.awt.event.*; 3 public class TestSourceListener { 4 5 public st ...

  10. T-SQL - 习题01_查询每门课都大于80分的学生姓名

    时间:2017-09-11 整理:byzqy 题目:用一条SQL语句查询出每门课都大于80分的学生姓名. 最近面试C#开发工程师,碰到上面这个考数据库的题目,自己感觉有点难度,没有思路,现将找到的解决 ...