|
348 | 348 | Thread 2: releasing semaphore
|
349 | 349 |
|
350 | 350 |
|
351 |
| -上述的示例都是在用户态实现的应用程序,其中的Thread、Mutex和Condvar是需要应用程序所在的操作系统(这里就是Linux)提供相应的支持的。在本章中,我们会在自己写的操作系统中实现Thread、Mutex、Condvar和Semaphore 机制,从而对同步互斥的原理有更加深入的了解,对应操作系统如何支持这些同步互斥底层机制有全面的掌握。 |
| 351 | +上述的示例都是在用户态实现的应用程序,其中的Thread、Mutex和Condvar需要应用程序所在的操作系统(这里就是Linux)提供相应的支持。在本章中,我们会在自己写的操作系统中实现Thread、Mutex、Condvar和Semaphore 机制,从而对同步互斥的原理有更加深入的了解,对应操作系统如何支持这些同步互斥底层机制有全面的掌握。 |
352 | 352 |
|
353 | 353 | 实践体验
|
354 | 354 | -----------------------------------------
|
|
502 | 502 | │ └── ...
|
503 | 503 |
|
504 | 504 | 本章代码导读
|
505 |
| ------------------------------------------------------ |
| 505 | +----------------------------------------------------- |
| 506 | + |
| 507 | +在本章实现支持多线程的达科塔盗龙操作系统 -- Thread&Coroutine OS过程中,需要考虑如下一些关键点:线程的总体结构、管理线程执行的线程控制块数据结构、以及对线程管理相关的重要函数:线程创建和线程切换。这些关键点既可以在用户态实现,也可在内核态实现。 |
| 508 | + |
| 509 | + |
| 510 | +线程设计与实现 |
| 511 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 512 | + |
| 513 | +在 :doc:`./1thread` 一节中讲述了设计实现用户态线程管理运行时库的过程,这其实是第三章中 :ref:`任务切换的设计与实现 <term-task-switch-impl>` 和 :ref:`协作式调度 <term-coop-impl>` 的一种更简单的用户态实现。首先是要构建多线程的基本执行环境,即定义线程控制块数据结构,包括线程执行状态、线程执行上下文(使用的通用寄存器集合)等。然后是要实现线程创建和线程切换这两个关键函数。这两个函数的关键就是构建线程的上下文和切换线程的上下文。当线程启动后,不会被抢占,所以需要线程通过 `yield_task` 函数主动放弃处理器,从而把处理器控制权交还给用户态线程管理运行时库,让其选择其他处于就绪态的线程执行。 |
| 514 | + |
| 515 | +在 :doc:`./1thread-kernel` 一节中讲述了在操作系统内部设计实现内核态线程管理的实现过程,这其实基于第三章中 :ref:`任务切换的设计与实现 <term-task-switch-impl>` 和 :ref:`抢占式调度 <term-preempt-sched>` 的进一步改进实现。这涉及到对进程的重构,把以前的线程管理相关数据结构转移到线程控制块中,并把线程作为一种资源,放在进程控制块中。这样与线程相关的关键部分包括: |
| 516 | + |
| 517 | +- 任务控制块 TaskControlBlock :表示线程的核心数据结构 |
| 518 | +- 任务管理器 TaskManager :管理线程集合的核心数据结构 |
| 519 | +- 处理器管理结构 Processor :用于线程调度,维护线程的处理器状态 |
| 520 | +- 线程切换:涉及特权级切换和线程上下文切换 |
| 521 | + |
| 522 | +进程控制块和线程控制块的主要部分如下所示: |
| 523 | + |
| 524 | +.. code-block:: Rust |
| 525 | + :linenos: |
| 526 | +
|
| 527 | + // os/src/task/tasks.rs |
| 528 | + // 线程控制块 |
| 529 | + pub struct TaskControlBlock { |
| 530 | + pub process: Weak<ProcessControlBlock>, //线程所属的进程控制块 |
| 531 | + pub kstack: KernelStack,//任务(线程)的内核栈 |
| 532 | + inner: UPSafeCell<TaskControlBlockInner>, |
| 533 | + } |
| 534 | + pub struct TaskControlBlockInner { |
| 535 | + pub res: Option<TaskUserRes>, //任务(线程)用户态资源 |
| 536 | + pub trap_cx_ppn: PhysPageNum,//trap上下文地址 |
| 537 | + pub task_cx: TaskContext,//任务(线程)上下文 |
| 538 | + pub task_status: TaskStatus,//任务(线程)状态 |
| 539 | + pub exit_code: Option<i32>,//任务(线程)退出码 |
| 540 | + } |
| 541 | + // os/src/task/process.rs |
| 542 | + // 进程控制块 |
| 543 | + pub struct ProcessControlBlock { |
| 544 | + pub pid: PidHandle, //进程ID |
| 545 | + inner: UPSafeCell<ProcessControlBlockInner>, |
| 546 | + } |
| 547 | + pub struct ProcessControlBlockInner { |
| 548 | + pub tasks: Vec<Option<Arc<TaskControlBlock>>>, //线程控制块列表 |
| 549 | + ... |
| 550 | + } |
| 551 | +
|
| 552 | +接下来就是相关的线程管理功能的设计与实现了。首先是线程创建,即当一个进程执行中发出系统调用 `sys_thread_create`` 后,操作系统就需要在当前进程控制块中创建一个线程控制块,并在线程控制块中初始化各个成员变量,建立好进程和线程的关系等,关键要素包括: |
| 553 | + |
| 554 | +- 线程的用户态栈:确保在用户态的线程能正常执行函数调用 |
| 555 | +- 线程的内核态栈:确保线程陷入内核后能正常执行函数调用 |
| 556 | +- 线程的跳板页:确保线程能正确的进行用户态<–>内核态切换 |
| 557 | +- 线程上下文:即线程用到的寄存器信息,用于线程切换 |
| 558 | + |
| 559 | +创建线程的主要代码如下所示: |
| 560 | + |
| 561 | +.. code-block:: Rust |
| 562 | + :linenos: |
| 563 | +
|
| 564 | + pub fn sys_thread_create(entry: usize, arg: usize) -> isize { |
| 565 | + // 创建新线程 |
| 566 | + let new_task = Arc::new(TaskControlBlock::new(... |
| 567 | + // 把线程加到就绪调度队列中 |
| 568 | + add_task(Arc::clone(&new_task)); |
| 569 | + // 把线程控制块加入到进程控制块中 |
| 570 | + let tasks = &mut process_inner.tasks; |
| 571 | + tasks[new_task_tid] = Some(Arc::clone(&new_task)); |
| 572 | + //建立trap/task上下文 |
| 573 | + *new_task_trap_cx = TrapContext::app_init_context( |
| 574 | + entry, |
| 575 | + new_task_res.ustack_top(), |
| 576 | + kernel_token(), |
| 577 | + ... |
| 578 | +
|
| 579 | +而关于线程切换和线程调度这两部分在之前已经介绍过。线程切换与第三章中介绍的特权级上下文切换和任务上下文切换的设计与实现是一致的,线程执行中的调度切换过程与第六章中介绍的进程调度机制是一致的。这里就不再进一步赘述了。 |
| 580 | + |
| 581 | +同步互斥机制的设计实现 |
| 582 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 583 | + |
| 584 | +在实现支持同步互斥机制的慈母龙操作系统 -- SyncMutexOS中,包括三种同步互斥机制,在 :doc:`./2lock-legacy` 一节中讲述了互斥锁的设计与实现,在 :doc:`./3semaphore` 一节中讲述了信号量的设计与实现,在 :doc:`./4condition-variable` 一节中讲述了条件变量的设计与实现。无论哪种同步互斥机制,都需要确保操作系统任意抢占线程,调度和切换线程的执行,都可以保证线程执行的互斥需求和同步需求,从而能够得到可预测和可重现的共享资源访问结果。这三种用于多线程的同步互斥机制所对应的内核数据结构都在进程控制块中,以进程资源的形式存在。 |
| 585 | + |
| 586 | +.. code-block:: Rust |
| 587 | + :linenos: |
| 588 | +
|
| 589 | + // 进程控制块内部结构 |
| 590 | + pub struct ProcessControlBlockInner { |
| 591 | + ... |
| 592 | + pub mutex_list: Vec<Option<Arc<dyn Mutex>>>, // 互斥锁列表 |
| 593 | + pub semaphore_list: Vec<Option<Arc<Semaphore>>>, // 信号量列表 |
| 594 | + pub condvar_list: Vec<Option<Arc<Condvar>>>, // 条件变量列表 |
| 595 | + } |
| 596 | +
|
| 597 | +在互斥锁的设计实现中,设计了一个更底层的 `UPSafeCellSafeCell<T>` 类型,用于支持在单核处理器上安全地在线程间共享可变全局变量。这个类型大致结构如下所示: |
| 598 | + |
| 599 | +.. code-block:: Rust |
| 600 | + :linenos: |
| 601 | +
|
| 602 | + pub struct UPSafeCell<T> { //允许在单核上安全使用可变全局变量 |
| 603 | + inner: RefCell<T>, //提供内部可变性和运行时借用检查 |
| 604 | + } |
| 605 | + unsafe impl<T> Sync for UPSafeCell<T> {} //声明支持全局变量安全地在线程间共享 |
| 606 | + impl<T> UPSafeCell<T> { |
| 607 | + pub unsafe fn new(value: T) -> Self { |
| 608 | + Self { inner: RefCell::new(value) } |
| 609 | + } |
| 610 | + pub fn exclusive_access(&self) -> RefMut<'_, T> { |
| 611 | + self.inner.borrow_mut() //得到它包裹的数据的独占访问权 |
| 612 | + } |
| 613 | + } |
| 614 | +
|
| 615 | +并基于此设计了 `Mutex` 互斥锁类型,可进一步细化为忙等型互斥锁和睡眠型互斥锁,二者的大致结构如下所示: |
| 616 | + |
| 617 | +.. code-block:: Rust |
| 618 | + :linenos: |
| 619 | +
|
| 620 | + pub struct MutexSpin { |
| 621 | + locked: UPSafeCell<bool>, //locked是被UPSafeCell包裹的布尔全局变量 |
| 622 | + } |
| 623 | + pub struct MutexBlocking { |
| 624 | + inner: UPSafeCell<MutexBlockingInner>, |
| 625 | + } |
| 626 | + pub struct MutexBlockingInner { |
| 627 | + locked: bool, |
| 628 | + wait_queue: VecDeque<Arc<TaskControlBlock>>, //等待获取锁的线程等待队列 |
| 629 | + } |
| 630 | +
|
| 631 | +在上述代码片段的第9行,可以看到挂在睡眠型互斥锁上的线程,会被放入到互斥锁的等待队列 `wait_queue` 中。 `Mutex` 互斥锁类型实现了 `lock` 和 `unlock` 两个方法完成获取锁和释放锁操作。而系统调用 `sys_mutex_create` 、 `sys_mutex_lock` 、 `sys_mutex_unlock` 这几个系统调用,是提供给多线程应用程序实现互斥锁的创建、获取锁和释放锁的同步互斥操作。 |
| 632 | + |
| 633 | +信号量 `Semaphore` 类型的大致结构如下所示: |
| 634 | + |
| 635 | +.. code-block:: Rust |
| 636 | + :linenos: |
| 637 | +
|
| 638 | + pub struct Semaphore { |
| 639 | + pub inner: UPSafeCell<SemaphoreInner>, //UPSafeCell包裹的内部可变结构 |
| 640 | + } |
| 641 | +
|
| 642 | + pub struct SemaphoreInner { |
| 643 | + pub count: isize, //信号量的计数值 |
| 644 | + pub wait_queue: VecDeque<Arc<TaskControlBlock>>, //信号量的等待队列 |
| 645 | + } |
| 646 | +
|
| 647 | +在上述代码片段的第7行,可以看到挂在信号量上的线程,会被放入到信号量的等待队列 `wait_queue` 中。信号量 `Semaphore` 类型实现了 `up` 和 `down` 两个方法完成获取获取信号量和释放信号量的操作。而系统调用 `sys_semaphore_create` 、 `sys_semaphore_up` 、 `sys_semaphore_down` 这几个系统调用,是提供给多线程应用程序实现信号量的创建、获取和释放的同步互斥操作。 |
| 648 | + |
| 649 | + |
| 650 | +条件变量 `Condvar` 类型的大致结构如下所示: |
| 651 | + |
| 652 | + |
| 653 | +.. code-block:: Rust |
| 654 | + :linenos: |
| 655 | +
|
| 656 | + pub struct Condvar { |
| 657 | + pub inner: UPSafeCell<CondvarInner>, //UPSafeCell包裹的内部可变结构 |
| 658 | + } |
| 659 | +
|
| 660 | + pub struct CondvarInner { |
| 661 | + pub wait_queue: VecDeque<Arc<TaskControlBlock>>,//等待队列 |
| 662 | + } |
| 663 | +
|
| 664 | +在上述代码片段的第7行,可以看到挂在条件变量上的线程,会被放入到条件变量的等待队列 `wait_queue` 中。条件变量 `Condvar` 类型实现了 `wait` 和 `signal` 两个方法完成获取等待条件变量和通知信号量的操作。而系统调用 `sys_condvar_create` 、 `sys_condvar_wait` 、 `sys_condvar_signal` 这几个系统调用,是提供给多线程应用程序实现条件变量的创建、等待和通知的同步互斥操作。 |
| 665 | + |
| 666 | +同学可能会注意到,上述的睡眠型互斥锁、信号量和条件变量的数据结构几乎相同,都会把挂起的线程放到等待队列中。但是它们的具体实现还是有区别的,这需要同学了解这三种同步互斥机制的操作原理,再看看它们的方法对的设计与实现:互斥锁的lock和unlock、信号量的up和down、条件变量的wait和signal,就可以看到它们的具体区别了。 |
0 commit comments