操作系统MIT6.S081:P2->OS organization and system calls

本文详细介绍了操作系统的核心概念,包括隔离性和防御性,阐述了用户/内核模式和页表在实现这些特性中的作用。讨论了宏内核和微内核设计的优缺点。此外,深入解析了XV6操作系统的启动过程,从内核编译到QEMU模拟执行,再到用户服务的启动,展示了系统调用在其中的角色。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本系列文章为MIT6.S081的学习笔记,包含了参考手册、课程、实验三部分的内容,前面的系列文章链接如下
操作系统MIT6.S081:[xv6参考手册第1章]->操作系统接口
操作系统MIT6.S081:P1->Introduction and examples
操作系统MIT6.S081:Lab1->Unix utilities
操作系统MIT6.S081:[xv6参考手册第2章]->操作系统组织结构


前言

这节课讨论操作系统的组织结构,主要围绕4个话题:

①Isolation。隔离性是设计操作系统组织结构的驱动力。
②Kernel mode和User mode。这两种模式用来隔离操作系统内核和用户应用程序。
③System calls。系统调用是你的应用程序能够转换到内核执行的基本方法,这样你的用户应用程序才能使用内核服务。
④最后我们会看以上三种设计是如何以一种简单的方式在XV6中实现。


复习

学习完第一课之后,你应该对操作系统的结构有了大致的认知。首先,会有类似于Shell、echo、find或者任何你实现的工具之类的程序,这些程序运行在操作系统之上。而操作系统又抽象了一些硬件资源,例如磁盘、CPU。通常来说操作系统和应用程序之间的接口被称为系统调用接口(System call interface),我们这门课程看到的接口都是Unix风格的接口。基于这些Unix接口,你们在lab1中完成了不同的应用程序。
在这里插入图片描述
lab1主要集中在理解上图中的应用程序到操作系统内核之间的接口。而这节课以及后面的许多节课都会实际分析在操作系统内核中,这些接口是如何实现的。实际上,在这个学期我们都会花时间来理解如何实现操作系统接口(也就是系统调用接口),所以这是第一节有关这些内容的课。你们通过邮件和网站提出了很多非常棒的问题,我们现在还不能立即开始很多细节的讨论,因为讨论这些细节需要仔细研究操作系统的内部实现,而毕竟这是第一节讨论实现的课程。我们这节课会讨论很多东西,但是很多东西会在后面的课程中变得更加清晰,因为我们会在后面的课程进行更加深入的讨论。


学习感悟

Frans教授:在我们进一步讨论之前,我们讨论一下在之前的utils lab(也就是lab1)中学到的最有意思的东西是什么。
Frans教授:我会先来回答这个问题。在实现lab1的代码之后,让我感到吃惊的一件事情是:我对于xargs的使用频率比之前更多了。我之前主要用另一种与xargs相同功能的方法,但是做完了xargs实验之后,我发现使用xargs更加的方便。所以,从那以后,我使用xargs的频率越来越高。


一、操作系统隔离性

为什么需要隔离性

①假设我们在用户空间有多个应用程序,如Shell,echo,find。但是,如果你通过Shell运行Prime代码(lab1中的一个部分)时,你的代码出现了问题,此时Shell不应该影响到其他的应用程序。举个反例,如果Shell出现问题时kill了其他的进程,这将会非常糟糕。所以你需要在不同的应用程序之间有强隔离性。
②类似的,操作系统某种程度上为所有的应用程序服务。当你的应用程序出现问题时(比如说你向操作系统传递了一些奇怪的参数),你会希望操作系统仍然能够很好的处理异常情况),不会因此而崩溃。所以,你也需要在应用程序和操作系统之间有强隔离性。


没有操作系统的影响

如果没有操作系统(或者操作系统只是一些库文件,比如说你在使用Python,通过import os你就可以将整个操作系统加载到你的应用程序中)。假设现在我们有一个Shell,并且引用了代表操作系统的库。同时,我们有一些其他的应用程序,例如echo。
在这里插入图片描述
通常来说,如果没有操作系统,应用程序会直接与硬件交互。比如,应用程序可以直接看到CPU的多个核、看到磁盘、内存(直接访问内存)。所以现在应用程序和硬件资源之间没有一个额外的抽象层,如下图所示。
在这里插入图片描述
从隔离性的角度来看,这并不是一个很好的设计,这里你可以看到这种设计是如何破坏隔离性的。使用操作系统的一个目的是为了同时运行多个应用程序,所以CPU时不时会从一个应用程序切换到另一个应用程序。我们假设硬件资源里只有一个CPU核,并且我们现在在这个CPU核上运行Shell。但是时不时的,也需要让其他的应用程序也可以运行。现在我们没有操作系统来帮我们完成切换,所以Shell就需要时不时的释放CPU资源。
在这里插入图片描述
复用
为了不变成一个恶意程序,Shell在发现自己运行了一段时间之后,需要让别的程序也有机会能运行,这种机制有时候称为协同调度(Cooperative Scheduling)。但是这里的场景并没有很好的隔离性,比如说Shell中的某个函数有一个死循环,那么Shell永远也不会释放CPU,进而其他的应用程序也不能够运行,甚至都不能运行一个第三方的程序来停止或者杀死Shell程序。所以这种场景下,我们基本上得不到真正的复用,而复用这个特性是非常有用的,不论应用程序在执行什么操作,复用都会迫使应用程序时不时的释放CPU,这样其他的应用程序才能运行。
内存隔离
从内存的角度来说,如果应用程序直接运行在硬件资源之上,那么每个应用程序的文本、代码和数据都直接保存在物理内存中。物理内存中的一部分被Shell使用,另一部分被echo使用。
在这里插入图片描述
在这个例子中,由于两个应用程序的内存之间没有边界,如果echo程序将数据存储在属于Shell的一个内存地址中(下图中的1000),那么echo就会覆盖Shell程序内存中的内容。这是我们非常不想看到的场景,因为echo渗透到了Shell中来,并且这类的问题是非常难定位的。所以这里也没有为我们提供好的隔离性,我们希望不同应用程序之间的内存是隔离的,这样一个应用程序就不会覆盖另一个应用程序的内存。
在这里插入图片描述
总结: 使用操作系统的一个原因甚至可以说是主要原因就是为了实现复用和内存隔离。如果你不使用操作系统并让应用程序直接与硬件交互就很难实现这两点。所以,将操作系统设计成一个库并不是一种常见的设计。你或许可以在一些实时操作系统中看到这样的设计,因为在这些实时操作系统中,应用程序之间彼此相互信任。但是在大部分的其他操作系统中,都会强制实现硬件资源的隔离。
Unix接口: 如果我们从隔离的角度来看Unix接口,那么我们可以发现,接口被精心设计以实现资源的强隔离(即多路复用和物理内存的隔离)。接口通过抽象硬件资源,从而使得提供强隔离性成为可能。
在这里插入图片描述


例子

之前通过fork创建了进程。进程本身不是CPU,但是它们对应了CPU,它们使得你可以在CPU上运行计算任务。所以操作系统不是直接将CPU提供给应用程序,而是向应用程序提供进程,进程抽象了CPU,这样操作系统才能在多个应用程序之间复用一个或者多个CPU。

问答

学生提问:这里说进程抽象了CPU,是不是说一个进程使用了部分的CPU,另一个进程使用了CPU的另一部分?这里CPU和进程的关系是什么?
Frans教授:我们在实验中使用的RISC-V处理器有4个核,所以你可以同时运行4个进程,一个进程占用一个核。但是假设你有8个应用程序,操作系统会分时复用这些CPU核。比如说对于一个进程运行100毫秒,之后内核会停止运行并将那个进程从CPU中卸载,再加载另一个应用程序进来运行100毫秒。通过这种方式使得每一个应用程序都不会连续运行超过100毫秒。这里只是一些基本概念,我们在接下来的几节课中会具体的看这里是如何实现的。
学生提问:好的,但是多个进程不能在同一时间使用同一个CPU核,对吧?
Frans教授:是的,分时复用就是指CPU运行一个进程一段时间,再运行另一个进程。
在这里插入图片描述


例子

①我们可以认为exec抽象了内存。当我们在执行exec系统调用的时候,我们会传入一个文件名,而这个文件名对应了一个应用程序的内存镜像。内存镜像里面包括了程序对应的指令、全局的数据。应用程序可以逐渐扩展自己的内存,比如通过sbrk扩展其数据段,但是应用程序并没有直接访问物理内存的权限,例如应用程序不能直接访问物理内存的1000-2000这段地址。不能直接访问的原因是,操作系统会提供内存隔离并控制内存,操作系统会在应用程序和硬件资源之间提供一个中间层。exec是这样一种系统调用,它表明了应用程序不能直接访问物理内存。
在这里插入图片描述
②files基本上来说抽象了磁盘。应用程序不会直接读写挂在计算机上的磁盘本身,并且在Unix中这也是不被允许的。在Unix中,与存储系统交互的唯一方式就是通过files。files提供了非常方便的磁盘抽象,你可以对文件命名、读写文件等等。之后,操作系统会决定如何将文件与磁盘中的块一一映射,确保一个磁盘块只出现在一个文件中,并且确保用户A不能操作用户B的文件。通过files的抽象,可以实现不同用户之间和同一个用户的不同进程之间的文件强隔离。
在这里插入图片描述


问答

学生提问:更复杂的内核会不会尝试将进程调度到同一个CPU核上来减少缓存缺失(Cache Miss)?
Frans教授:是的,有一种东西叫做缓存亲和性(Cache affinity)。现在的操作系统的确非常复杂,并且会尽量避免缓存缺失和类似的事情来提升性能。我们在这门课程后面介绍高性能网络的时候会介绍更多相关的内容。
学生提问:XV6的代码中,哪一部分可以看到操作系统为多个进程复用了CPU?
Frans教授:有挺多文件与这个相关,但是proc.c应该是最相关的一个。两三周之后的课程中会有一个话题介绍这个内容,我们会看大量的细节并展示操作系统的多路复用是如何发生的。所以可以这么看待这节课,这节课的内容是对许多不同内容的初始介绍,因为我们总得从某个地方开始吧。


二、操作系统防御性

为什么需要操作系统防御性

当你在做内核开发时,操作系统需要确保所有的组件都能工作,所以它需要做好准备抵御来自应用程序的攻击。如果说应用程序无意或者恶意地向系统调用传入一些错误的参数就会导致操作系统崩溃,那就太糟糕了。在这种场景下,操作系统因为崩溃了会拒绝为其它所有的应用程序提供服务。所以操作系统需要能够应对恶意的应用程序。
在这里插入图片描述
另一个需要考虑的是,应用程序不能够打破对它的隔离。应用程序非常有可能是恶意的,它或许是由攻击者写出来的,攻击者或许想要打破对应用程序的隔离,进而控制内核。一旦有了对于内核的控制能力,就可以做任何事情,因为内核控制了所有的硬件资源。
在这里插入图片描述
所以操作系统或者说内核需要具备防御性来避免类似的事情发生。实际中,要满足这些要求还有点棘手。在Linux中,时不时的有一些内核的bug使得应用程序可以打破它的隔离域并控制内核。这里需要持续的关注,并尽可能的提供最好的防御性。当你在开发内核时,防御性是你必须掌握的一个思想。实际中的应用程序或许就是恶意的,这意味着我们需要在应用程序和操作系统之间提供强隔离性。如果操作系统需要具备防御性,那么在应用程序和操作系统之间需要有一堵厚墙,并且操作系统可以在这堵墙上执行任何它想执行的策略。


实现强隔离性的方法

通常来说,需要通过硬件来实现强隔离性。我们这节课会简单介绍一些硬件隔离的内容,但是在后续的课程我们会介绍的更加详细。这里的硬件支持包括了两部分。
①第一部分是用户/内核模式(user/kernel mode),kernel mode在RISC-V中被称为管理者模式(Supervisor mode),但是其实是同一个东西。
②第二部分是页表(page table)或者虚拟内存(Virtual Memory)。
对于所有的处理器,如果需要运行能够支持多个应用程序的操作系统,需要同时支持user/kernle mode和虚拟内存。具体的实现或许会有细微的差别,但是基本上来说所有的处理器需要能支持这些。我们在这门课中使用的RISC-V处理器就支持了这些功能。
在这里插入图片描述


2.1 用户/内核模式

user/kernel mode

这里会以尽可能全局的视角来介绍,有很多重要的细节在这节课中都不会涉及。处理器有两种操作模式,user mode和kernel mode。当运行在kernel mode时,CPU可以运行特定权限的指令(privileged instructions)。当运行在user mode时,CPU只能运行普通权限的指令(unprivileged instructions)。
在这里插入图片描述
普通权限的指令都是一些你们熟悉的指令,例如将两个寄存器相加的指令ADD、将两个寄存器相减的指令SUB、跳转指令JRC、BRANCH分支指令等等。这些都是普通权限指令,所有的应用程序都允许执行这些指令。
在这里插入图片描述
特殊权限指令主要是一些直接操纵硬件的指令和设置保护的指令,例如设置页表寄存器、关闭时钟中断。在处理器上有各种各样的状态,操作系统会使用这些状态,但是只能通过特殊权限指令来变更这些状态。举个例子,当一个应用程序尝试执行一条特殊权限指令,因为不允许在user mode执行特殊权限指令,处理器会拒绝执行这条指令。通常来说,这时会将控制权限从user mode切换到kernel mode,当操作系统拿到控制权之后,或许会杀掉进程,因为应用程序执行了不该执行的指令。
在这里插入图片描述
下图是RISC-V privilege架构的文档,这个文档包括了所有的特殊权限指令。在接下来的一个月,你们都会与这些特殊权限指令打交道。我们下节课就会详细介绍其中一些指令。这里我们先对这些指令有一些初步的认识:应用程序不应该执行这些指令,这些指令只能被内核执行。
在这里插入图片描述


问答

学生提问:如果kernel mode允许一些指令的执行,user mode不允许一些指令的执行,那么是谁在检查当前的mode并实际运行这些指令,并且怎么知道当前是不是kernel mode?是有什么标志位吗?
Frans教授:是的,在处理器里面有一个flag,存储在处理器的一个bit中。当它为1的时候是user mode,当它为0时是kernel mode。当处理器在解析指令时,如果指令是特殊权限指令,并且该bit被设置为1,处理器会拒绝执行这条指令,就像在运算时不能除以0一样。
学生提问:所以,唯一的控制方式就是通过某种方式更新那个bit?
Frans教授:你认为是什么指令更新了那个bit位?是特殊权限指令还是普通权限指令?很明显,设置那个bit位的指令必须是特殊权限指令,因为应用程序不应该能够设置那个bit到kernel mode,否则的话应用程序就可以运行各种特殊权限指令了,所以那个bit是被保护的。许多同学都已经知道了,实际上RISC-V还有第三种模式称为machine mode。在大多数场景下,我们会忽略这种模式,所以我也不太会介绍这种模式。 所以实际上我们有三级权限(user/kernel/machine),而不是两级(user/kernel)。

问答

学生提问:考虑到安全性,所有的用户代码都会通过内核访问硬件,但是有没有可能一个计算机的用户可以随意的操纵内核?
Frans教授:并不会,至少小心的设计就不会发生这种事。或许一些程序会有额外的权限,操作系统也会认可这一点。但是这些额外的权限并不会给每一个用户,比如只有root用户有特定的权限来完成安全相关的操作。
学生提问:那BIOS呢?BIOS会在操作系统之前运行还是之后?
Frans教授:BIOS是一段计算机自带的代码,它会先启动,之后它会启动操作系统,所以BIOS需要是一段可被信任的代码,它最好是正确的且不是恶意的。
学生提问:之前提到,设置处理器中kernel mode的bit位的指令是一条特殊权限指令,那么一个用户程序怎么才能让内核执行任何内核指令?因为现在切换到kernel mode的指令都是一条特殊权限指令了,对于用户程序来说也没法修改那个bit位。
Frans教授:你说的对,这也是我们想要看到的结果。可以这么来看这个问题,首先这里不是完全按照你说的方式工作,在RISC-V中,如果你在用户空间尝试执行一条特殊权限指令,用户程序会通过系统调用来切换到kernel mode。当用户程序执行系统调用,会通过ECALL触发一个软中断(software interrupt),软中断会查询操作系统预先设定的中断向量表,并执行中断向量表中包含的中断处理程序。中断处理程序在内核中,这样就完成了user mode到kernel mode的切换,并执行用户程序想要执行的特殊权限指令。


2.2 页表

页表/虚拟内存

基本上所有的CPU都支持虚拟内存,页表将虚拟内存地址与物理内存地址做了映射。
在这里插入图片描述
操作系统会设置页表,使得每一个进程都有不重合的物理内存,这样一个进程就不能访问其他进程的物理内存,因为其他进程的物理内存都不在它的页表中。一个进程甚至都不能随意编造一个内存地址,然后通过这个内存地址来访问其他进程的物理内存,这样就给了我们内存的强隔离性。
在这里插入图片描述
基本上来说,页表定义了内存的布局,而每一个用户进程都有自己对应内存的独立布局,这给了我们非常强的内存隔离性。基于硬件的支持,我们可以重新画一下之前的一张图,我们先画一个矩形,ls程序位于这个矩形中。再画一个矩形,echo程序位于这个矩形中。每个矩形都有一个虚拟内存地址,从 0 0 0 开始到 2 n 2^n 2n 。这样,ls程序有了一个内存地址0,echo程序也有了一个内存地址0。但是操作系统会将两个程序的内存地址0映射到不同的物理内存地址,所以ls程序不能访问echo程序的内存,同样echo程序也不能访问ls程序的内存。
在这里插入图片描述
类似的,内核位于应用程序下方,假设是XV6,那么它也有自己的内存地址空间,并且与应用程序完全独立。我们可以认为user/kernel mode是分隔用户空间和内核空间的边界,用户空间运行的程序运行在user mode,内核空间的程序运行在kernel mode。操作系统位于内核空间。
在这里插入图片描述


ecall

基于我们已经介绍的内容,上面这张图有点太过严格了。因为我们用矩形包括了一个程序的所有部分,但是这里没有描述如何从一个矩形将控制权转移到另一个矩形的,而很明显这种转换是需要的。例如当ls程序运行的时候,会调用read/write系统调用,Shell程序会调用fork或者exec系统调用,所以必须要有一种方式可以使得用户的应用程序能够将控制权以一种协同工作的方式转移到内核,这样内核才能提供相应的服务。
在这里插入图片描述
所以,需要有一种方式能够让应用程序可以将控制权转移给内核。在RISC-V中,有一个专门的指令用来实现这个功能,叫做ecall。ecall接收一个数字参数,当一个用户程序想要将程序执行的控制权转移到内核,它只需要执行ecall指令,并传入一个数字,这里的数字参数代表了应用程序想要调用的System Call。
在这里插入图片描述
ecall会跳转到内核中一个特定的由内核控制的位置。我们在这节课的最后可以看到在XV6中存在一个唯一的系统调用接入点,每一次应用程序执行ecall指令,应用程序都会通过这个接入点进入到内核中。举个例子,不论是Shell还是其他的应用程序,当它在用户空间执行fork时,它并不是直接调用操作系统中对应的函数,而是调用ecall指令,并将fork对应的数字作为参数传给ecall,之后再通过ecall跳转到内核。

ecall案例分析

下图中通过一根竖线来区分用户空间和内核空间,左边是用户空间,右边是内核空间。在内核侧,有一个位于syscall.c的函数syscall,每一个从应用程序发起的系统调用都会调用到这个syscall函数,syscall函数会检查ecall的参数,通过这个参数内核可以知道需要调用的是fork(3.9会有相应的代码跟踪介绍)。
在这里插入图片描述
这里需要澄清的是,用户空间和内核空间的界限是一个硬性的界限,用户不能直接调用fork,用户的应用程序执行系统调用的唯一方法就是通过这里的ecall指令。假设我现在要执行另一个系统调用write,相应的流程是类似的,write系统调用不能直接调用内核中的write代码,而是由封装好的系统调用函数执行ecall指令。所以write函数实际上调用的是ecall指令,指令的参数是代表了write系统调用的数字。之后控制权到了syscall函数,syscall会实际调用write系统调用。
在这里插入图片描述


问答

学生提问:操作系统在什么时候检查是否允许执行fork或者write?现在看起来应用程序只需要执行ECALL再加上系统调用对应的数字(编号)就能完成调用,但是内核在什么时候决定这个应用程序是否有权限执行特定的系统调用?
Frans教授:原则上来说,在内核侧实现fork的位置可以实现任何的检查,例如检查系统调用的参数,并决定应用程序是否被允许执行fork系统调用。在Unix中,任何应用程序都能调用fork,我们以write为例吧,write的实现需要检查传递给write的地址(需要写入数据的指针)属于用户应用程序,这样内核才不会被欺骗从别的不属于应用程序的位置写入数据。
学生提问:当应用程序表现的恶意或者就是在一个死循环中,内核是如何夺回控制权限的?
Frans教授:内核会通过硬件设置一个定时器,定时器到期之后会将控制权限从用户空间转移到内核空间,之后内核就有了控制能力并可以重新调度CPU到另一个进程中。
学生提问:这其实是一个顶层设计的问题,是什么驱动了操作系统的设计人员使用编程语言C?
Frans教授:C提供了很多对于硬件的控制能力,比如说当你需要去编程一个定时器芯片时,这更容易通过C来完成,因为你可以得到更多对于硬件资源的底层控制能力。所以,如果你要做大量的底层开发,C会是一个非常方便的编程语言,尤其是需要与硬件交互的时候。当然,不是说你不能用其他的编程语言,但是这是C成功的一个历史原因。
学生提问:为什么C比C++流行的多?仅仅是因为历史原因吗?有没有其他的原因导致大部分的操作系统并没有采用C++?
Frans教授:我认为有一些操作系统是用C++写的,这完全是可能的。但是大部分你知道的操作系统并不是用C++写的,这里的主要原因是Linus不喜欢C++,所以Linux主要是C语言实现。


三、宏内核与微内核

操作系统组织结构

现在,我们有了一种方法,可以通过系统调用或者说ecall指令,将控制权从应用程序转到操作系统中。之后内核负责实现具体的功能并检查参数以确保不会被一些坏的参数所欺骗。所以内核有时候也被称为可被信任的计算空间(Trusted Computing Base),在一些安全的术语中也被称为TCB。基本上来说,要被称为TCB,内核首先要是正确且没有Bug的。假设内核中有Bug,攻击者可能会利用那个Bug并将其转变成漏洞,这个漏洞使得攻击者可以打破操作系统的隔离性并接管内核。所以内核真的是需要越少的Bug越好。
在这里插入图片描述
另一方面,内核必须要将用户应用程序或者进程当做是恶意的。如我之前所说的,内核的设计人员在编写和实现内核代码时,必须要有安全的思想。这个目标很难实现,因为当你的操作系统变得足够大的时候,很多事情就不是那么直观了。几乎每一个你用过的或者被广泛使用的操作系统,时不时的都有一个安全漏洞。就算被修复了,但是过了一段时间又会出现一个新的漏洞。我们之后会介绍为什么很难让所有部分都正确工作,但是你要知道是内核需要做一些棘手的工作,需要操纵硬件、非常小心做检查,所以很容易就出现一些小的疏漏,进而触发一个Bug,这也是可以理解的。
在这里插入图片描述


宏内核

一个有趣的问题是,什么程序应该运行在kernel mode?敏感的代码肯定是运行在kernel mode,因为这是Trusted Computing Base。对于这个问题的一个答案是,首先我们会有user/kernel边界,在上面是应用程序,在下面是运行在kernel mode的程序。
在这里插入图片描述
其中一个选项是让整个操作系统代码都运行在kernel mode。大多数的Unix操作系统实现都运行在kernel mode。比如XV6中,所有的操作系统服务都在kernel mode中,这种形式被称为Monolithic Kernel Design(宏内核设计)。
在这里插入图片描述
这里有几件事情需要注意:
----首先,如果考虑Bug的话,这种方式不太好。在一个宏内核中,任何一个操作系统的Bug都有可能成为漏洞。因为我们现在在内核中运行了一个巨大的操作系统,出现Bug的可能性更大了。你们可以去查一些统计信息,平均每3000行代码都会有几个Bug,所以如果有许多行代码运行在内核中,那么出现严重Bug的可能性也变得更大。所以从安全的角度来说,在内核中有大量的代码是宏内核的缺点。
----另一方面,如果你去看一个操作系统,它包含了各种各样的组成部分,比如说文件系统,虚拟内存,进程管理,这些都是操作系统内实现了特定功能的子模块。宏内核的优势在于,因为这些子模块现在都位于同一个程序中,它们可以紧密的集成在一起,这样的集成提供很好的性能。例如Linux,它就有很不错的性能。
在这里插入图片描述


微内核

另一种设计主要关注点是减少内核中的代码,它被称为Micro Kernel Design(微内核设计)。在这种模式下,希望在kernel mode中运行尽可能少的代码。所以这种设计下还是有内核,但是内核只有非常少的几个模块,例如内核通常会有一些IPC的实现或者是Message passing(消息传递);非常少的虚拟内存的支持,可能只支持了页表;以及分时复用CPU的一些支持。微内核的目的在于将大部分的操作系统运行在内核之外。所以,我们还是会有user mode以及user/kernel mode的边界。但是我们现在会将原来在内核中的其他部分,作为普通的用户程序来运行。比如文件系统可能就是个常规的用户空间程序。这个文件系统我不小心画成了红色,其实我想画成黑色的。
在这里插入图片描述
现在,文件系统运行的就像一个普通的用户程序,就像echo、Shell一样,这些程序都运行在用户空间。可能还会有一些其他的用户应用程序,例如虚拟内存系统的一部分也会以一个普通的应用程序的形式运行在user mode。
在这里插入图片描述
某种程度上来说,这是一种好的设计。因为在内核中的代码的数量较小,更少的代码意味着更少的Bug。但是这种设计也有相应的问题。假设我们需要让Shell能与文件系统交互,比如Shell调用了exec,必须有种方式可以接入到文件系统中。通常来说,这里工作的方式是,Shell会通过内核中的IPC系统发送一条消息,内核会查看这条消息并发现这是给文件系统的消息,之后内核会把消息发送给文件系统。
在这里插入图片描述
文件系统会完成它的工作之后会向IPC系统发送回一条消息说,这是你的exec系统调用的结果,之后IPC系统再将这条消息发送给Shell。
在这里插入图片描述
所以,这里是典型的通过消息来实现传统的系统调用。现在,对于任何文件系统的交互,都需要分别完成2次用户空间<->内核空间的跳转。与宏内核对比,在宏内核中如果一个应用程序需要与文件系统交互,只需要完成1次用户空间<->内核空间的跳转,所以微内核的的跳转是宏内核的两倍。通常微内核的挑战在于性能更差,这里有两个方面需要考虑:
①在user/kernel mode反复跳转带来的性能损耗。
②在一个类似宏内核的紧耦合系统,各个组成部分,例如文件系统和虚拟内存系统,可以很容易的共享page cache页缓存。而在微内核中,每个部分之间都很好的隔离开了,这种共享更难实现。进而导致更难在微内核中得到更高的性能。


总结

我们这里介绍的有关宏内核和微内核的区别都特别笼统。在实际中,两种内核设计都会出现,出于历史原因大部分的桌面操作系统是宏内核,如果你运行需要大量内核计算的应用程序,例如在数据中心服务器上的操作系统,通常也是使用的宏内核,主要的原因是Linux提供了很好的性能。但是很多嵌入式系统,例如Minix、Cell4这些都是微内核设计。这两种设计都很流行,如果你从头开始写一个操作系统,你可能会从一个微内核设计开始。但是一旦你有了类似于Linux这样的宏内核设计,将它重写到一个微内核设计将会是巨大的工作。并且这样重构的动机也不足,因为人们总是想把时间花在实现新功能上,而不是重构他们的内核。如你们所知的,XV6是一种宏内核设计,如大多数经典的Unix系统一样。但是在这个学期的后半部分,我们会讨论更多有关微内核设计的内容。


四、XV6的启动过程

接下来我会切换到代码介绍,来看一下XV6是如何工作的。

4.1 内核编译过程

代码结构

首先,我们来看一下代码结构,主要由三个部分组成:
在这里插入图片描述
①第一个是kernel。我们可以ls kernel的内容,里面包含了基本上所有的内核文件。因为XV6是一个宏内核结构,这里所有的文件会被编译成一个叫做kernel的二进制文件,然后这个二进制文件会被运行在kernel mode中。
在这里插入图片描述
②第二个部分是user。这基本上是运行在user mode的程序,这也是为什么一个目录称为kernel,另一个目录称为user的原因。
③第三部分叫做mkfs。它会创建一个空的文件镜像,我们会将这个镜像存在磁盘上,这样我们就可以直接使用一个空的文件系统。


内核编译过程

接下来我们简单介绍一下内核是如何编译的。你们可能已经编译过内核,但是还没有真正的理解编译过程,这个过程还是比较重要的。
①首先,Makefile(XV6目录下的文件)会读取一个C文件,例如proc.c。之后调用gcc编译器,生成一个proc.s文件,这是RISC-V 汇编语言文件。之后再走到汇编解释器,生成proc.o,这是汇编语言的二进制格式。
在这里插入图片描述
Makefile会为所有内核文件做相同的操作,比如pipe.c,会按照同样的套路先经过gcc编译成pipe.s,再通过汇编解释器生成pipe.o。
在这里插入图片描述
之后,系统加载器(Loader)会收集所有的.o文件,将它们链接在一起,并生成内核文件。
在这里插入图片描述
这里生成的内核文件就是我们将会在QEMU中运行的文件。同时,为了你们的方便,Makefile还会创建kernel.asm,它包含了内核的完整汇编代码,你们可以通过查看它来定位究竟是哪个指令导致了Bug。比如,我接下来查看kernel.asm文件,我们可以看到用汇编指令描述的内核。
在这里插入图片描述

问答

Frans教授:这里你们可能已经注意到了,第一个指令位于地址0x80000000,对应的是一个RISC-V指令:auipc指令。有人知道第二列,例如0x0000a117、0x83010113、0x6505是什么意思吗?有人想来回答这个问题吗?
学生提问:学生回答:这是汇编指令的16进制表现形式对吗?
Frans教授:是的,完全正确。所以这里0x0000a117就是auipc,这里是二进制编码后的指令。因为每个指令都有一个二进制编码,kernel的asm文件会显示这些二进制编码。当你在运行gdb时,如果你想知道具体在运行什么,你可以看具体的二进制编码是什么,有的时候这还挺方便的。


4.2 QEMU

QEMU

接下来我们不带gdb运行XV6(make会读取Makefile文件中的指令)。这里会编译文件,然后调用QEMU(qemu-system-riscv64指令)。这里本质上是通过C语言来模拟仿真RISC-V处理器。
在这里插入图片描述
我们来看传给QEMU的几个参数:
-kernel:这里传递的是内核文件(kernel目录下的kernel文件),这是将在QEMU中运行的程序文件。
-m:这里传递的是RISC-V虚拟机将会使用的内存大小
-smp:这里传递的是虚拟机可以使用的CPU核数
-drive:传递的是虚拟机使用的磁盘驱动,这里传入的是fs.img文件
这样,XV6系统就在QEMU中启动了。QEMU表现的就像一个真正的计算机一样。当你想到QEMU时,你不应该认为它是一个C程序,你应该把它想成是下图,一个真正的主板。这是我办公室中的RISC-V主板,它可以启动一个XV6。当你通过QEMU来运行你的内核时,你应该认为你的内核是运行在这样一个主板之上。主板有一个开关,一个RISC-V处理器,有支持外设的空间(比如说一个接口是连接网线的,一个是PCI-E插槽,主板上还有一些内存芯片)。这是一个你可以在上面编程的物理硬件,而XV6操作系统管理这样一块主板,你在你的脑海中应该有这么一张图。
在这里插入图片描述
对于RISC-V有完整的文档介绍,比如说下图是一个RISC-V的结构图:
在这里插入图片描述
这个图里面有:
4个核:U54 Core 1-4
L2 cache:Banked L2
连接DRAM的连接器:DDR Controller
各种连接外部设备的方式,比如说UART0,一端连接了键盘,另一端连接了terminal(显示器)。
连接了时钟的接口:Clock Generation
我们后面会讨论更多的细节,但是这里基本上就是RISC-V处理器的所有组件,你通过它与实际的硬件交互。实际上抛开一些细节,通过QEMU模拟的计算机主板与这里真实的计算机主板非常相似,当你们在运行QEMU时,基本上跟在运行真实硬件是一样的,只是说同样的东西QEMU在软件中实现了而已。
QEMU仿真了RISC-V处理器背后的含义:
直观来看,QEMU是一个大型的开源C程序,你可以下载或者git clone它。但是在QEMU的主循环代码中,只在做一件事情:
----读取4字节或者8字节的RISC-V指令
----然后解析RISC-V指令并找出对应的操作码(op code)。我们之前在看kernel.asm的时候看过一些操作码的二进制版本,通过解析可以知道这是一个ADD指令或者是一个SUB指令。
----之后在软件中执行相应的指令。
这基本上就是QEMU的全部工作了,对于每个CPU核,QEMU都会运行这么一个循环。
在这里插入图片描述
为了完成这里的工作,QEMU的主循环需要维护寄存器的状态。所以QEMU会有以C语言声明的类似于X0,X1寄存器等。
在这里插入图片描述
当QEMU在执行一条指令,比如(ADD a0, 7, 1),这里会将常量7和1相加,并将结果存储在a0寄存器中,所以在这个例子中,寄存器X0会是7。
在这里插入图片描述
之后QEMU会执行下一条指令,并持续不断地执行指令。除了模拟所有的普通权限指令之外,QEMU还会模拟所有的特殊权限指令,这就是QEMU的工作原理。对于你们来说,你们只需要认为你们跑在QEMU上的代码跟跑在一个真正的RISC-V处理器上是一样的,就像你们在6.004这门课程中使用过的RISC-V处理器一样。


问答

学生提问:我想知道QEMU有没有什么欺骗硬件(采用什么硬件技巧)的实现,比如说overlapping instruction(指令重叠)?
Frans教授:并没有,真正的CPU运行在QEMU的下层。当你运行QEMU时,很有可能你是运行在一个x86处理器上,这个x86处理器本身会做各种处理,比如顺序解析指令。所以QEMU对你来说就是个C语言程序。
学生提问:那多线程呢?程序能真正跑在4个核上吗?还是只能跑在一个核上?如果能跑在多个核上,那么QEMU是不是有多线程?
Frans教授:我们在Athena上使用的QEMU还有你们下载的QEMU它们会使用多线程。QEMU在内部通过多线程实现并行处理,所以当QEMU在仿真4个CPU核的时候,它是并行的模拟这4个核并在不同的CPU核上并行运算。我们在后面有个实验会演示这里是如何工作的。


4.3 xv6系统服务启动过程

xv6系统的启动过程

接下来我会系统地介绍XV6,让你们对XV6的结构有个大概的了解。在后面的课程,我们会涉及到更多的细节。首先,我们打开一个终端在xv6目录下启动QEMU,并打开gdb。本质上来说QEMU内部有一个gdb server,当我们启动之后QEMU会等待gdb客户端连接。
在这里插入图片描述
我们在xv6目录下再启动一个gdb客户端,这里是一个RISC-V 64位Linux的gdb,有些同学的电脑可能是multi-arch或者其他版本的的gdb,但是基本上来说,这里的gdb是为RISC-V 64位处理器编译的。
在这里插入图片描述
在连接上之后,我们在程序的入口处设置一个断点,因为我们知道这是QEMU会跳转到的第一个指令。
在这里插入图片描述
设置完断点之后运行程序,可以发现代码并没有停在0x80000000(见3.7 kernel.asm中,0x80000000是程序的起始位置),而是停在了0x8000000a。
在这里插入图片描述
如果我们查看kernel的汇编文件,可以看到在地址0x8000000a读取了控制系统寄存器(Control System Register)mhartid,并将结果加载到了a1寄存器。所以QEMU会模拟执行这条指令,之后执行下一条指令。
在这里插入图片描述
地址0x80000000是一个被QEMU认可的地址,也就是说如果你想使用QEMU,那么第一个指令地址必须是它。所以,我们会让内核加载器从那个位置开始加载内核。我们查看kernel.ld文件(这个文件定义了内核是如何被加载的),可以看到内核使用的起始地址就是QEMU指定的0x80000000这个地址。这就是我们操作系统最初运行的步骤。
在这里插入图片描述
回到gdb,我们可以看到gdb也显示了指令的二进制编码。可以看出,csrr是一个4字节的指令,而addi是一个2字节的指令。
在这里插入图片描述
我们这里可以看到,XV6从entry.s开始启动,这个时候没有内存分页,没有隔离性,并且运行在machine mode(机器模式)。XV6会尽可能快地跳转到kernel mode或者说是supervisor mode。我们在main函数设置一个断点,可以看到main函数已经运行在supervisor mode了。接下来我运行程序,代码会在断点,也就是main函数的第一条指令停住。
在这里插入图片描述
上图中,右边是gdb的断点显示,做边是main.c的源码。接下来,我想运行在gdb的layout split模式,于是执行layout split命令。
在这里插入图片描述
效果如下,从这个视图可以看出gdb要执行的下一条指令是什么,断点具体在什么位置。这里我们只在一个CPU上运行QEMU(见最初的make参数),这样会使得gdb调试更加简单。因为现在只指定了一个CPU核,QEMU只会仿真一个核,我可以单步执行程序(因为在单核或者单线程场景下,单个断点就可以停止整个程序的运行)。
在这里插入图片描述
通过在gdb中输入n,可以跳到下一条指令。这里调用了一个名为consoleinit的函数,它的工作与你想象的完全一样,也就是设置好console。
在这里插入图片描述
一旦console设置好了,接下来可以向console打印输出(代码16、17行)。执行完16、17行之后,我们可以在QEMU看到相应的输出。
在这里插入图片描述
除了console之外,还有许多代码来做初始化。
在这里插入图片描述
kinit:设置好页表分配器(page allocator)
kvminit:设置好虚拟内存,这是下节课的内容
kvminithart:打开页表,也是下节课的内容
processinit:设置好初始进程或者说设置好进程表单
trapinit/trapinithart:设置好user/kernel mode转换代码
plicinit/plicinithart:设置好中断控制器PLIC(Platform Level Interrupt Controller),我们后面在介绍中断的时候会详细的介绍这部分,这是我们用来与磁盘和console交互方式
binit:分配buffer cache
iinit:初始化inode缓存
fileinit:初始化文件系统
virtio_disk_init:初始化磁盘
userinit:最后当所有的设置都完成了,操作系统也运行起来了,会运行第一个用户进程userinit,接下来我们会看一下userinit


问答

学生提问:这里的初始化函数的调用顺序重要吗?
Frans教授:重要,一些函数必须在另一些函数之后运行,某几个函数的顺序可能不重要,但是它们可能又需要在其它的一些函数之后运行。


4.4 xv6用户服务启动过程

我们再按几次n,跳到usnerinit函数那一行,然后可以通过gdb的s指令,跳到userinit内部。图中右边是源码,左边是gdb视图。
在这里插入图片描述
userinit有点像是胶水代码(Glue code,胶水代码不实现具体的功能,只是为了适配不同的部分而存在),它利用了XV6的特性,并启动了第一个进程。我们总是需要有一个用户进程在运行,这样才能实现与操作系统的交互,所以这里需要一个小程序来初始化第一个用户进程。这个小程序定义在initcode中。
在这里插入图片描述
这里直接是程序的二进制形式,它会链接或者在内核中直接静态定义。实际上,这段代码对应了usr/initcode.S的汇编程序。这个汇编程序中,它首先将init中的地址加载到a0(la a0, init),argv中的地址加载到a1(la a1, argv),exec系统调用对应的数字加载到a7(li a7, SYS_exec),最后调用ecall。所以这里执行了3条指令,之后在第4条指令将控制权交给了操作系统。
在这里插入图片描述
如果我在syscall中设置一个断点,并让程序运行起来。userinit会创建初始进程,返回到用户空间,执行刚刚介绍的3条指令,再回到内核空间。这里是任何XV6用户会使用到的第一个系统调用。
在这里插入图片描述
让我们来看一下会发生什么。通过在gdb中执行c,让程序运行起来,我们现在进入到了syscall函数。
在这里插入图片描述
我们可以查看syscall的代码,num = p->trapframe->a7会读取使用的系统调用对应的整数。
在这里插入图片描述
当代码执行完这一行之后,我们可以在gdb中打印num,可以看到是7。
在这里插入图片描述
如果我们查看syscall.h,可以看到7对应的是exec系统调用。
在这里插入图片描述
所以,这里本质上是告诉内核,某个用户应用程序执行了ecall指令,并且想要调用exec系统调用。p->trapframe->a0 = syscall[num]()这一行是实际执行系统调用。这里可以看出,num用来索引一个数组,这个数组是一个函数指针数组,可以看到syscall[7]对应了exec的入口函数。我们跳到这个函数中去,可以看到,我们现在在sys_exec函数中。
在这里插入图片描述
sys_exec中的第一件事情是从用户空间读取参数,它会读取path,也就是要执行程序的文件名。这里首先会为参数分配空间,然后从用户空间将参数拷贝到内核空间。之后我们打印path,可以看到传入的就是init程序。
在这里插入图片描述
所以,综合来看,initcode完成了通过exec调用init程序。让我们来看看init程序,
在这里插入图片描述
init会为用户空间设置好一些东西,比如配置好console,调用fork,并在fork出的子进程中执行shell。
在这里插入图片描述
最终的效果就是Shell运行起来了。如果我再次运行代码,我还会陷入到syscall中的断点,并且同样也是调用exec系统调用,只是这次是通过exec运行Shell。当Shell运行起来之后,我们可以从QEMU看到Shell。
在这里插入图片描述
这里简单的介绍了一下XV6是如何从0开始直到第一个Shell程序运行起来。并且我们也看了一下第一个系统调用是在什么时候发生的。我们并没有看系统调用背后的具体机制,这个在后面会介绍。但是目前来说,这些对于你们完成这周的syscall lab是足够了。


问答

学生提问:我们会处理网络吗,比如说网络相关的实验?
Frans教授:是的,最后一个lab中你们会实现一个网络驱动。你们会写代码与硬件交互,操纵连接在RISC-V主板上网卡的驱动,以及寄存器,再向以太网发送一些网络报文。
好的,最后让我总结一下。因为没有涉及到太多的细节,我认为syscall lab可能会比上一个utils lab简单些,但是下一个实验会更加的复杂。

<think>我们正在解决一个具体的错误:在R语言中使用`mrgsolve`包构建PBPK模型时,遇到错误提示:`'make' not found`。这个错误通常与系统环境配置有关,特别是在Windows操作系统上,因为R包`mrgsolve`在编译模型时需要Rtools的支持,而Rtools可能没有正确安装或配置。 ### 错误原因分析 - **Rtools缺失**:`mrgsolve`包在编译模型时依赖Rtools(一个用于Windows上构建R包的软件套件)。如果Rtools未安装或未正确配置,就会出现`'make' not found`的错误。 - **环境变量未设置**:即使安装了Rtools,如果系统环境变量`PATH`中没有包含Rtools的路径,R也无法找到`make`命令。 - **版本不匹配**:安装的Rtools版本与当前R版本不兼容。 ### 解决方案 #### 步骤1:安装Rtools 1. 访问[Rtools官网](https://cran.r-project.org/bin/windows/Rtools/)下载与您R版本匹配的Rtools安装程序。 - 例如,R 4.2.x 对应 Rtools422. 运行安装程序,注意在安装过程中勾选**“Add Rtools to system PATH”**选项(这是关键步骤,确保环境变量被设置)。 #### 步骤2:验证Rtools安装 安装完成后,在R控制台中运行以下代码检查Rtools路径: ```r Sys.which("make") ``` 如果返回一个有效的路径(如`C:/rtools42/usr/bin/make.exe`),则表明安装成功。如果返回空字符串,则需手动配置环境变量。 #### 步骤3:手动配置环境变量(可选) 如果自动添加PATH失败,手动配置: 1. 右键点击“此电脑” → 属性 → 高级系统设置 → 环境变量。 2. 在“系统变量”中找到`PATH`,点击编辑。 3. 添加Rtools的`bin`目录(例如`C:\rtools42\usr\bin`)和`mingw64\bin`目录(例如`C:\rtools42\mingw64\bin`)到PATH中。 4. 重启R会话后再次验证`Sys.which("make")`。 #### 步骤4:重新编译模型 配置完成后,重新运行模型代码: ```r library(mrgsolve) model <- ' $PARAM ... $INIT ... $ODE ... ' mod <- mcode("model_name", model) # 此时应能成功编译 ``` ### 替代方案:使用预编译模型 如果问题依旧,可以尝试使用`mrgsolve`的预编译模型功能(避免每次重新编译): ```r # 首次运行:编译并保存为.dll mod <- mcode("model_name", model, compile = TRUE) save(mod, file = "model.rda") # 后续使用:直接加载.dll,无需编译 load("model.rda") ``` ### 验证示例 以下是一个最小验证示例,测试Rtools配置: ```r library(mrgsolve) # 定义一个简单模型 model_test <- ' $PARAM CL = 1, V = 20 $CMT CENT $ODE dxdt_CENT = - (CL/V) * CENT; ' mod_test <- mcode("test_model", model_test) # 应无错误 ``` ### 扩展支持:其他操作系统 - **Linux/macOS**:通常自带`make`,无需额外安装。若缺失,可通过终端安装: - Ubuntu/Debian: `sudo apt-get install build-essential` - macOS: 安装Xcode命令行工具:`xcode-select --install` ### 注意事项 - 确保R、RStudio和Rtools的位数一致(32位或64位)。 - 安装Rtools时,关闭R和RStudio。 通过以上步骤,您应能解决`'make' not found`错误,顺利运行PBPK模型。如果问题仍然存在,请提供详细的系统信息和错误日志以便进一步诊断。 §§相关问题§§ 1. 如何在R中配置C++编译器用于其他需要编译的包? 2. 使用`mrgsolve`时遇到模型编译错误如何调试? 3. 除了`mrgsolve`,R中有哪些替代的PBPK建模包?</think>### 解决R语言PBPK模型构建错误:`'make' not found` 这个错误通常发生在使用需要编译的R包(如`mrgsolve`或`RxODE`)时,系统缺少必要的编译工具。以下是完整的解决方案: --- #### 1. **错误原因分析** - **根本原因**:R包`mrgsolve`/`RxODE`需要C++编译器来编译模型代码,但系统缺少编译工具链 - **具体表现**: ```r Error in compile.model(model, project = basename(tempdir())) : 'make' not found in PATH ``` - **深层原因**: - Windows系统:未安装Rtools或环境变量未配置 - macOS/Linux:缺少命令行开发工具(Xcode或build-essential) - R包未正确链接到编译器[^1] --- #### 2. **解决方案(分系统操作)** ##### ▶ Windows系统 1. **安装Rtools**: - 访问[Rtools官网](https://cran.r-project.org/bin/windows/Rtools/) - 下载与您R版本匹配的Rtools(如R 4.2.x → Rtools42- 安装时勾选 **"Add rtools to system PATH"** 2. **验证安装**: ```r # 在R中运行 Sys.which("make") # 应返回类似: "C:/rtools42/usr/bin/make.exe" ``` 3. **配置环境变量**(若未自动设置): ```r # 在R中运行 writeLines('PATH="${RTOOLS42_HOME}\\usr\\bin;${PATH}"', con = "~/.Renviron") restart R session # 重启R会话 ``` ##### ▶ macOS系统 1. **安装Xcode命令行工具**: ```bash # 在终端执行 xcode-select --install ``` 2. **验证安装**: ```r Sys.which("make") # 应返回: "/usr/bin/make" ``` ##### ▶ Linux系统 ```bash # Ubuntu/Debian sudo apt-get install build-essential # CentOS/RHEL sudo yum groupinstall "Development Tools" ``` --- #### 3. **模型代码修正建议** 在解决编译问题后,建议检查模型代码是否完整。以下是整合您参数的完整PBPK模型模板: ```r library(mrgsolve) model <- ' $PARAM // 化合物参数 MW = 581.29, // 分子量 logP = 1.59, // logP pKa = 8.8, // pKa Clint = 45.55, // 人肝微粒体清除率 (μL/min/mg) Rbp = 0.954, // 血-血浆比 fup = 0.0818, // 血浆游离分数 // 生理参数 (标准成人值) BW = 70, // 体重(kg) Qh = 90, // 肝血流(L/h) Vh = 1.8, // 肝脏体积(L) // 吸收参数 (基于溶解度数据) Sol_pH7 = 0.07515, // pH7.4溶解度(mg/mL) Ka = 0.8 // 吸收速率常数(1/h) $INIT GUT = 0, // 肠道药量(mg) PLAS = 0, // 血浆药量(mg) LIVER = 0, // 肝脏药量(mg) $ODE // 吸收过程 dxdt_GUT = -Ka * GUT // 肝脏代谢 (Clint转换) double k_met = Clint * 1e-3 * 60 / Vh; // μL/min/mg → L/h dxdt_LIVER = (Qh/Vh)*(PLAS - LIVER) - k_met * LIVER * fup // 血浆平衡 dxdt_PLAS = Ka * GUT + (Qh/Vh)*(LIVER - PLAS) $TABLE double CP = PLAS / (0.04 * BW * Rbp); // 血浆浓度(mg/L) ' # 编译模型 (此时应无错误) mod <- mcode("PBPK_Model", model) ``` --- #### 4. **模型验证与参数优化** 使用动物数据校准模型: ```r # 小鼠数据校准 mouse_sim <- mod %>% param(list( BW = 0.025, # 小鼠体重 Clint = 27.98, # 小鼠Clint fup = 0.0873 # 小鼠fup )) %>% ev(amt = 10, cmt = "GUT") %>% # 10mg口服剂量 mrgsim(end = 24) # 计算预测PK参数 AUC <- auc(mouse_sim$time, mouse_sim$CP) CL_pred <- 10 / AUC # 预测清除率 ``` --- #### 5. **替代方案(避免编译问题)** 如果仍遇到编译问题,可使用纯R实现的PBPK包: ```r # 安装免编译包 install.packages("PKsim") # 运行模型 library(PKsim) results <- pbpkHumanModel( params = list( Clint = 45.55, fup = 0.0818, Rbp = 0.954, Dose = 100, # 剂量(mg) Route = "oral" # 给药途径 ) ) plot(results$Time, results$CP) ``` > **关键提示**:使用`mrgsolve`时,首次编译可能需要1-5分钟,R会显示`Compiling model...`提示[^2]。 通过上述步骤,您应能成功解决编译错误并运行PBPK模型。模型验证时可使用您提供的小鼠/大鼠数据(如Vd=0.213L,Cl=0.116L/h)进行校准[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

知初与修一

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值