diff --git a/.gitignore b/.gitignore
index c4929e4..2cdbe56 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
+.vscode
.idea
Cargo.lock
target
@@ -6,4 +7,4 @@ os/last-*
user
ci-user/
tools
-workplace/
\ No newline at end of file
+workplace/
diff --git a/Makefile b/Makefile
index 347ac58..103d75d 100644
--- a/Makefile
+++ b/Makefile
@@ -2,8 +2,19 @@ DOCKER_NAME ?= dinghao188/rcore-tutorial
DIR := workplace
.PHONY: docker build_docker
+
test: test3 test4 test5 test6 test7 test8
+lab1: test3
+
+lab2: test4
+
+lab3: test5
+
+lab4: test6 test7
+
+lab5: test8
+
setup:
rm -rf ${DIR}
mkdir ${DIR}
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..d67b1a3
--- /dev/null
+++ b/README.md
@@ -0,0 +1,26 @@
+# rust-based os comp 2022
+
+## Guide
+
+- Guide deployed version can be found [here](https://LearningOS.github.io/rCore-Tutorial-Guide-2022S/).
+- Source of Guide is in 'guide' DIR
+
+## os reference framework
+The 'os[1-8]-ref' are the 'os[1-8]' reference framework. You can read and copy some codes into os[1-8]
+
+## kernel labs
+There are five kernel labs.
+
+According to the [Guide](https://LearningOS.github.io/rCore-Tutorial-Guide-2022S/), write os codes for:
+- lab1 in 'os3' DIR
+- lab2 in 'os4' DIR
+- lab3 in 'os5' DIR
+- lab4 in 'os6' DIR
+- lab5 in 'os8' DIR
+
+## Check your results
+- lab1: `make test3`
+- lab2 `make test4`
+- lab3 `make test5`
+- lab4 `make test6` and `make test7`
+- lab5 `make test8`
\ No newline at end of file
diff --git a/guide/.gitignore b/guide/.gitignore
new file mode 100644
index 0000000..a32f3e9
--- /dev/null
+++ b/guide/.gitignore
@@ -0,0 +1,5 @@
+build/
+.vscode/
+.idea
+source/_build/
+.venv/
\ No newline at end of file
diff --git a/guide/LICENSE b/guide/LICENSE
new file mode 100644
index 0000000..f288702
--- /dev/null
+++ b/guide/LICENSE
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
diff --git a/guide/Makefile b/guide/Makefile
new file mode 100644
index 0000000..9c17f0c
--- /dev/null
+++ b/guide/Makefile
@@ -0,0 +1,23 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line, and also
+# from the environment for the first two.
+SPHINXOPTS ?=
+SPHINXBUILD ?= sphinx-build
+SOURCEDIR = source
+BUILDDIR = build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile deploy
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+view:
+ make html && firefox build/html/index.html
diff --git a/guide/all.sh b/guide/all.sh
new file mode 100755
index 0000000..cf20d3b
--- /dev/null
+++ b/guide/all.sh
@@ -0,0 +1,2 @@
+make clean && make html && google-chrome build/html/index.html
+
diff --git a/guide/make.bat b/guide/make.bat
new file mode 100644
index 0000000..6247f7e
--- /dev/null
+++ b/guide/make.bat
@@ -0,0 +1,35 @@
+@ECHO OFF
+
+pushd %~dp0
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
+set SOURCEDIR=source
+set BUILDDIR=build
+
+if "%1" == "" goto help
+
+%SPHINXBUILD% >NUL 2>NUL
+if errorlevel 9009 (
+ echo.
+ echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+ echo.installed, then set the SPHINXBUILD environment variable to point
+ echo.to the full path of the 'sphinx-build' executable. Alternatively you
+ echo.may add the Sphinx directory to PATH.
+ echo.
+ echo.If you don't have Sphinx installed, grab it from
+ echo.http://sphinx-doc.org/
+ exit /b 1
+)
+
+%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+goto end
+
+:help
+%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+
+:end
+popd
diff --git a/guide/requirements.txt b/guide/requirements.txt
new file mode 100644
index 0000000..d61da19
--- /dev/null
+++ b/guide/requirements.txt
@@ -0,0 +1,28 @@
+alabaster==0.7.12
+Babel==2.9.1
+certifi==2021.5.30
+charset-normalizer==2.0.4
+docutils==0.16
+idna==3.2
+imagesize==1.2.0
+jieba==0.42.1
+Jinja2==3.0.1
+MarkupSafe==2.0.1
+packaging==21.0
+Pygments==2.10.0
+pyparsing==2.4.7
+pytz==2021.1
+requests==2.26.0
+snowballstemmer==2.1.0
+Sphinx==4.1.2
+sphinx-comments==0.0.3
+sphinx-rtd-theme==0.5.2
+sphinx-tabs==3.2.0
+sphinxcontrib-applehelp==1.0.2
+sphinxcontrib-devhelp==1.0.2
+sphinxcontrib-htmlhelp==2.0.0
+sphinxcontrib-jsmath==1.0.1
+sphinxcontrib-qthelp==1.0.3
+sphinxcontrib-serializinghtml==1.1.5
+urllib3==1.26.6
+furo==2021.8.31
diff --git a/guide/show.sh b/guide/show.sh
new file mode 100755
index 0000000..207f71c
--- /dev/null
+++ b/guide/show.sh
@@ -0,0 +1 @@
+make html && google-chrome build/html/index.html
diff --git a/guide/source/0setup-devel-env.rst b/guide/source/0setup-devel-env.rst
new file mode 100644
index 0000000..1a8d0da
--- /dev/null
+++ b/guide/source/0setup-devel-env.rst
@@ -0,0 +1,267 @@
+第零章:实验环境配置
+================================
+
+.. toctree::
+ :hidden:
+ :maxdepth: 4
+
+本节我们将完成环境配置并成功运行 rCore-Tutorial 。整个流程分为下面几个部分:
+
+- OS 环境配置
+- Rust 开发环境配置
+- Qemu 模拟器安装
+- 其他工具安装
+- 试运行 rCore-Tutorial
+
+如果你在环境配置中遇到了无法解决的问题,请在本节讨论区留言,我们会尽力提供帮助。
+
+OS 环境配置
+-------------------------------
+
+目前,实验主要支持 Ubuntu18.04/20.04 操作系统。使用 Windows10 和 macOS 的读者,可以安装一台 Ubuntu18.04 虚拟机或 Docker
+进行实验。
+
+Windows10 用户可以通过系统内置的 **WSL2** 虚拟机(请不要使用 WSL1)来安装 Ubuntu 18.04 / 20.04 。读者请自行在互联网上搜索相关安装教程,或 `适用于 Linux 的 Windows 子系统安装指南 (Windows 10) `_ 。
+
+.. note::
+
+ **Docker 开发环境**
+
+ 感谢 dinghao188 和张汉东老师帮忙配置好的 Docker 开发环境,进入 Docker 开发环境之后不需要任何软件工具链的安装和配置,可以直接将 tutorial 运行起来,目前应该仅支持将 tutorial 运行在 Qemu 模拟器上。
+
+ 使用方法如下(以 Ubuntu18.04 为例):
+
+ 1. 通过 ``su`` 切换到管理员账户 ``root`` ;
+ 2. 在 ``rCore-Tutorial`` 根目录下 ``make docker`` 进入到 Docker 环境;
+ 3. 进入 Docker 之后,会发现当前处于根目录 ``/`` ,我们通过 ``cd mnt`` 将当前工作路径切换到 ``/mnt`` 目录;
+ 4. 通过 ``ls`` 可以发现 ``/mnt`` 目录下的内容和 ``rCore-Tutorial-v3`` 目录下的内容完全相同,接下来就可以在这个环境下运行 tutorial 了。例如 ``cd os && make run`` 。
+
+使用 macOS 进行实验理论上也是可行的,但本章节仅介绍 Ubuntu 下的环境配置方案。
+
+.. note::
+
+ 经初步测试,使用 M1 芯片的 macOS 也可以运行本实验的框架,即我们的实验对平台的要求不是很高。但我们仍建议同学配置 Ubuntu 环境,以避免未知的环境问题。
+
+Rust 开发环境配置
+-------------------------------------------
+
+首先安装 Rust 版本管理器 rustup 和 Rust 包管理器 cargo,可以使用官方安装脚本:
+
+.. code-block:: bash
+
+ curl https://sh.rustup.rs -sSf | sh
+
+如果因网络问题通过命令行下载脚本失败了,可以在浏览器地址栏中输入 ``_ 将脚本下载到本地运行。或者使用字节跳动提供的镜像源。
+
+建议将 rustup 的镜像地址修改为中科大的镜像服务器,以加速安装:
+
+.. code-block:: bash
+
+ export RUSTUP_DIST_SERVER=https://mirrors.ustc.edu.cn/rust-static
+ export RUSTUP_UPDATE_ROOT=https://mirrors.ustc.edu.cn/rust-static/rustup
+ curl https://sh.rustup.rs -sSf | sh
+
+或者使用 tuna 源来加速(建议清华同学在校园网中使用) `参见 rustup 帮助 `_:
+
+.. code-block:: bash
+
+ export RUSTUP_DIST_SERVER=https://mirrors.tuna.edu.cn/rustup
+ export RUSTUP_UPDATE_ROOT=https://mirrors.tuna.edu.cn/rustup/rustup
+ curl https://sh.rustup.rs -sSf | sh
+
+也可以设置科学上网代理:
+
+.. code-block:: bash
+
+ # e.g. Shadowsocks 代理,请根据自身配置灵活调整下面的链接
+ export https_proxy=http://127.0.0.1:1080
+ export http_proxy=http://127.0.0.1:1080
+ export ftp_proxy=http://127.0.0.1:1080
+
+安装中全程选择默认选项即可。
+
+安装完成后,我们可以重新打开一个终端来让新设置的环境变量生效,也可以手动将环境变量设置应用到当前终端,
+只需输入以下命令:
+
+.. code-block:: bash
+
+ source $HOME/.cargo/env
+
+确认一下我们正确安装了 Rust 工具链:
+
+.. code-block:: bash
+
+ rustc --version
+
+最好把 Rust 包管理器 cargo 镜像地址 crates.io 也替换成中国科学技术大学的镜像服务器,来加速三方库的下载。
+打开或新建 ``~/.cargo/config`` 文件,并把内容修改为:
+
+.. code-block:: toml
+
+ [source.crates-io]
+ registry = "https://github.com/rust-lang/crates.io-index"
+ replace-with = 'ustc'
+ [source.ustc]
+ registry = "git://mirrors.ustc.edu.cn/crates.io-index"
+
+同样,也可以使用tuna源 `参见 crates.io 帮助 `_:
+
+.. code-block:: toml
+
+ [source.crates-io]
+ replace-with = 'tuna'
+
+ [source.tuna]
+ registry = "https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git"
+
+
+推荐 JetBrains Clion + Rust插件 或者 Visual Studio Code 搭配 rust-analyzer 和 RISC-V Support 插件 进行代码阅读和开发。
+
+.. note::
+
+ * JetBrains Clion是付费商业软件,但对于学生和教师,只要在 JetBrains 网站注册账号,可以享受一定期限(半年左右)的免费使用的福利。
+ * Visual Studio Code 是开源软件。
+ * 当然,采用 VIM,Emacs 等传统的编辑器也是没有问题的。
+
+Qemu 模拟器安装
+----------------------------------------
+
+我们需要使用 Qemu 5.0.0 版本进行实验,为此,从源码手动编译安装 Qemu 模拟器:
+
+.. attention::
+
+ 也可以使用 Qemu6,但要小心潜在的不兼容问题!
+
+.. code-block:: bash
+
+ # 安装编译所需的依赖包
+ sudo apt install autoconf automake autotools-dev curl libmpc-dev libmpfr-dev libgmp-dev \
+ gawk build-essential bison flex texinfo gperf libtool patchutils bc \
+ zlib1g-dev libexpat-dev pkg-config libglib2.0-dev libpixman-1-dev git tmux python3
+ # 下载源码包
+ # 如果下载速度过慢可以使用我们提供的百度网盘链接:https://pan.baidu.com/s/1z-iWIPjxjxbdFS2Qf-NKxQ
+ # 提取码 8woe
+ wget https://download.qemu.org/qemu-5.0.0.tar.xz
+ # 解压
+ tar xvJf qemu-5.0.0.tar.xz
+ # 编译安装并配置 RISC-V 支持
+ cd qemu-5.0.0
+ ./configure --target-list=riscv64-softmmu,riscv64-linux-user
+ make -j$(nproc)
+
+.. note::
+
+ 注意,上面的依赖包可能并不完全,比如在 Ubuntu 18.04 上:
+
+ - 出现 ``ERROR: pkg-config binary 'pkg-config' not found`` 时,可以安装 ``pkg-config`` 包;
+ - 出现 ``ERROR: glib-2.48 gthread-2.0 is required to compile QEMU`` 时,可以安装
+ ``libglib2.0-dev`` 包;
+ - 出现 ``ERROR: pixman >= 0.21.8 not present`` 时,可以安装 ``libpixman-1-dev`` 包。
+
+ 另外一些 Linux 发行版编译 Qemu 的依赖包可以从 `这里 `_
+ 找到。
+
+ GCC 11 可能无法正常编译 Qemu5 ,而 GCC 9.3.0 (Ubuntu 20.04 自带) 及 GCC 10.3.0 经测试可以编译,请自行选择合适的编译器版本。
+
+之后我们可以在同目录下 ``sudo make install`` 将 Qemu 安装到 ``/usr/local/bin`` 目录下,但这样经常会引起
+冲突。个人来说更习惯的做法是,编辑 ``~/.bashrc`` 文件(如果使用的是默认的 ``bash`` 终端),在文件的末尾加入
+几行:
+
+.. code-block:: bash
+
+ # 请注意,qemu-5.0.0 的父目录可以随着你的实际安装位置灵活调整
+ export PATH=$PATH:/home/shinbokuow/Downloads/built/qemu-5.0.0
+ export PATH=$PATH:/home/shinbokuow/Downloads/built/qemu-5.0.0/riscv64-softmmu
+ export PATH=$PATH:/home/shinbokuow/Downloads/built/qemu-5.0.0/riscv64-linux-user
+
+随后即可在当前终端 ``source ~/.bashrc`` 更新系统路径,或者直接重启一个新的终端。
+
+确认 Qemu 的版本:
+
+.. code-block:: bash
+
+ qemu-system-riscv64 --version
+ qemu-riscv64 --version
+
+试运行 rCore-Tutorial
+------------------------------------------------------------
+
+.. code-block:: bash
+
+ git clone https://github.com/LearningOS/rCore-Tutorial-Code-2022S
+ cd rCore-Tutorial-Code-2022S
+
+我们先运行不需要处理用户代码的 ch1 分支:
+
+.. code-block:: bash
+
+ git checkout ch1
+ cd os
+ LOG=DEBUG make run
+
+如果你的环境配置正确,你应当会看到如下输出:
+
+.. code-block:: bash
+
+ [rustsbi] RustSBI version 0.2.0-alpha.4
+ .______ __ __ _______.___________. _______..______ __
+ | _ \ | | | | / | | / || _ \ | |
+ | |_) | | | | | | (----`---| |----`| (----`| |_) || |
+ | / | | | | \ \ | | \ \ | _ < | |
+ | |\ \----.| `--' |.----) | | | .----) | | |_) || |
+ | _| `._____| \______/ |_______/ |__| |_______/ |______/ |__|
+
+ [rustsbi] Implementation: RustSBI-QEMU Version 0.0.1
+ [rustsbi-dtb] Hart count: cluster0 with 1 cores
+ [rustsbi] misa: RV64ACDFIMSU
+ [rustsbi] mideleg: ssoft, stimer, sext (0x222)
+ [rustsbi] medeleg: ima, ia, bkpt, la, sa, uecall, ipage, lpage, spage (0xb1ab)
+ [rustsbi] pmp0: 0x80000000 ..= 0x800fffff (rwx)
+ [rustsbi] pmp1: 0x80000000 ..= 0x807fffff (rwx)
+ [rustsbi] pmp2: 0x0 ..= 0xffffffffffffff (---)
+ [rustsbi] enter supervisor 0x80200000
+ Hello, world!
+ [DEBUG] .rodata [0x80203000, 0x80205000)
+ [ INFO] .data [0x80205000, 0x80206000)
+ [ WARN] boot_stack [0x80206000, 0x80216000)
+ [ERROR] .bss [0x80216000, 0x80217000)
+ Panicked at src/main.rs:48 Shutdown machine!
+
+通常 rCore 会自动关闭 Qemu 。如果在某些情况下需要强制结束,可以先按下 ``Ctrl+A`` ,再按下 ``X`` 来退出 Qemu。
+
+.. attention::
+
+ 请务必执行 ``make run``,这将为你安装一些上文没有提及的 Rust 包依赖。
+
+ 如果卡在了
+
+ .. code-block::
+
+ Updating git repository `https://github.com/rcore-os/riscv`
+
+ 请通过更换 hosts 等方式解决科学上网问题,或者将 riscv 项目下载到本地,并修改 os/Cargo.toml 中的 riscv 包依赖路径
+
+ .. code-block::
+
+ [dependencies]
+ riscv = { path = "YOUR riscv PATH", features = ["inline-asm"] }
+
+恭喜你完成了实验环境的配置,可以开始阅读教程的正文部分了!
+
+GDB 调试支持*
+------------------------------
+
+.. attention::
+
+ 使用 GDB debug 并不是必须的,你可以暂时跳过本小节。
+
+
+
+在 ``os`` 目录下 ``make debug`` 可以调试我们的内核,这需要安装终端复用工具 ``tmux`` ,还需要基于 riscv64 平台的 gdb 调试器 ``riscv64-unknown-elf-gdb`` 。该调试器包含在 riscv64 gcc 工具链中,工具链的预编译版本可以在如下链接处下载:
+
+- `Ubuntu 平台 `_
+- `macOS 平台 `_
+- `Windows 平台 `_
+- `CentOS 平台 `_
+
+解压后在 ``bin`` 目录下即可找到 ``riscv64-unknown-elf-gdb`` 以及另外一些常用工具 ``objcopy/objdump/readelf`` 等。
diff --git a/guide/source/_static/dracula.css b/guide/source/_static/dracula.css
new file mode 100644
index 0000000..30def14
--- /dev/null
+++ b/guide/source/_static/dracula.css
@@ -0,0 +1,91 @@
+/* Dracula Theme v1.2.5
+ *
+ * https://github.com/zenorocha/dracula-theme
+ *
+ * Copyright 2016, All rights reserved
+ *
+ * Code licensed under the MIT license
+ * http://zenorocha.mit-license.org
+ *
+ * @author Rob G
+ * @author Chris Bracco
+ * @author Zeno Rocha
+ */
+
+ .highlight .hll { background-color: #111110 }
+ .highlight { background: #282a36; color: #f8f8f2 }
+ .highlight .c { color: #6272a4 } /* Comment */
+ .highlight .err { color: #f8f8f2 } /* Error */
+ .highlight .g { color: #f8f8f2 } /* Generic */
+ .highlight .k { color: #ff79c6 } /* Keyword */
+ .highlight .l { color: #f8f8f2 } /* Literal */
+ .highlight .n { color: #f8f8f2 } /* Name */
+ .highlight .o { color: #ff79c6 } /* Operator */
+ .highlight .x { color: #f8f8f2 } /* Other */
+ .highlight .p { color: #f8f8f2 } /* Punctuation */
+ .highlight .ch { color: #6272a4 } /* Comment.Hashbang */
+ .highlight .cm { color: #6272a4 } /* Comment.Multiline */
+ .highlight .cp { color: #ff79c6 } /* Comment.Preproc */
+ .highlight .cpf { color: #6272a4 } /* Comment.PreprocFile */
+ .highlight .c1 { color: #6272a4 } /* Comment.Single */
+ .highlight .cs { color: #6272a4 } /* Comment.Special */
+ .highlight .gd { color: #962e2f } /* Generic.Deleted */
+ .highlight .ge { color: #f8f8f2; text-decoration: underline } /* Generic.Emph */
+ .highlight .gr { color: #f8f8f2 } /* Generic.Error */
+ .highlight .gh { color: #f8f8f2; font-weight: bold } /* Generic.Heading */
+ .highlight .gi { color: #f8f8f2; font-weight: bold } /* Generic.Inserted */
+ .highlight .go { color: #44475a } /* Generic.Output */
+ .highlight .gp { color: #f8f8f2 } /* Generic.Prompt */
+ .highlight .gs { color: #f8f8f2 } /* Generic.Strong */
+ .highlight .gu { color: #f8f8f2; font-weight: bold } /* Generic.Subheading */
+ .highlight .gt { color: #f8f8f2 } /* Generic.Traceback */
+ .highlight .kc { color: #ff79c6 } /* Keyword.Constant */
+ .highlight .kd { color: #8be9fd; font-style: italic } /* Keyword.Declaration */
+ .highlight .kn { color: #ff79c6 } /* Keyword.Namespace */
+ .highlight .kp { color: #ff79c6 } /* Keyword.Pseudo */
+ .highlight .kr { color: #ff79c6 } /* Keyword.Reserved */
+ .highlight .kt { color: #8be9fd } /* Keyword.Type */
+ .highlight .ld { color: #f8f8f2 } /* Literal.Date */
+ .highlight .m { color: #bd93f9 } /* Literal.Number */
+ .highlight .s { color: #f1fa8c } /* Literal.String */
+ .highlight .na { color: #50fa7b } /* Name.Attribute */
+ .highlight .nb { color: #8be9fd; font-style: italic } /* Name.Builtin */
+ .highlight .nc { color: #50fa7b } /* Name.Class */
+ .highlight .no { color: #f8f8f2 } /* Name.Constant */
+ .highlight .nd { color: #f8f8f2 } /* Name.Decorator */
+ .highlight .ni { color: #f8f8f2 } /* Name.Entity */
+ .highlight .ne { color: #f8f8f2 } /* Name.Exception */
+ .highlight .nf { color: #50fa7b } /* Name.Function */
+ .highlight .nl { color: #8be9fd; font-style: italic } /* Name.Label */
+ .highlight .nn { color: #f8f8f2 } /* Name.Namespace */
+ .highlight .nx { color: #f8f8f2 } /* Name.Other */
+ .highlight .py { color: #f8f8f2 } /* Name.Property */
+ .highlight .nt { color: #ff79c6 } /* Name.Tag */
+ .highlight .nv { color: #8be9fd; font-style: italic } /* Name.Variable */
+ .highlight .ow { color: #ff79c6 } /* Operator.Word */
+ .highlight .w { color: #f8f8f2 } /* Text.Whitespace */
+ .highlight .mb { color: #bd93f9 } /* Literal.Number.Bin */
+ .highlight .mf { color: #bd93f9 } /* Literal.Number.Float */
+ .highlight .mh { color: #bd93f9 } /* Literal.Number.Hex */
+ .highlight .mi { color: #bd93f9 } /* Literal.Number.Integer */
+ .highlight .mo { color: #bd93f9 } /* Literal.Number.Oct */
+ .highlight .sa { color: #f1fa8c } /* Literal.String.Affix */
+ .highlight .sb { color: #f1fa8c } /* Literal.String.Backtick */
+ .highlight .sc { color: #f1fa8c } /* Literal.String.Char */
+ .highlight .dl { color: #f1fa8c } /* Literal.String.Delimiter */
+ .highlight .sd { color: #f1fa8c } /* Literal.String.Doc */
+ .highlight .s2 { color: #f1fa8c } /* Literal.String.Double */
+ .highlight .se { color: #f1fa8c } /* Literal.String.Escape */
+ .highlight .sh { color: #f1fa8c } /* Literal.String.Heredoc */
+ .highlight .si { color: #f1fa8c } /* Literal.String.Interpol */
+ .highlight .sx { color: #f1fa8c } /* Literal.String.Other */
+ .highlight .sr { color: #f1fa8c } /* Literal.String.Regex */
+ .highlight .s1 { color: #f1fa8c } /* Literal.String.Single */
+ .highlight .ss { color: #f1fa8c } /* Literal.String.Symbol */
+ .highlight .bp { color: #f8f8f2; font-style: italic } /* Name.Builtin.Pseudo */
+ .highlight .fm { color: #50fa7b } /* Name.Function.Magic */
+ .highlight .vc { color: #8be9fd; font-style: italic } /* Name.Variable.Class */
+ .highlight .vg { color: #8be9fd; font-style: italic } /* Name.Variable.Global */
+ .highlight .vi { color: #8be9fd; font-style: italic } /* Name.Variable.Instance */
+ .highlight .vm { color: #8be9fd; font-style: italic } /* Name.Variable.Magic */
+ .highlight .il { color: #bd93f9 } /* Literal.Number.Integer.Long */
diff --git a/guide/source/_static/my_style.css b/guide/source/_static/my_style.css
new file mode 100644
index 0000000..8aa6c28
--- /dev/null
+++ b/guide/source/_static/my_style.css
@@ -0,0 +1,3 @@
+.wy-nav-content {
+ max-width: 1200px !important;
+}
diff --git a/guide/source/appendix-a/index.rst b/guide/source/appendix-a/index.rst
new file mode 100644
index 0000000..23d27b2
--- /dev/null
+++ b/guide/source/appendix-a/index.rst
@@ -0,0 +1,56 @@
+附录 A:Rust 系统编程资料
+=============================
+
+.. toctree::
+ :hidden:
+ :maxdepth: 4
+
+
+.. .. note::
+
+.. **Rust 语法卡片:外部符号引用**
+
+.. extern "C" 可以引用一个外部的 C 函数接口(这意味着调用它的时候要遵从目标平台的 C 语言调用规范)。但我们这里只是引用位置标志
+.. 并将其转成 usize 获取它的地址。由此可以知道 ``.bss`` 段两端的地址。
+
+.. **Rust 语法卡片:迭代器与闭包**
+
+.. 代码第 7 行用到了 Rust 的迭代器与闭包的语法,它们在很多情况下能够提高开发效率。如读者感兴趣的话也可以将其改写为等价的 for
+.. 循环实现。
+
+.. .. _term-raw-pointer:
+.. .. _term-dereference:
+.. .. warning::
+
+.. **Rust 语法卡片:Unsafe**
+
+.. 代码第 8 行,我们将 ``.bss`` 段内的一个地址转化为一个 **裸指针** (Raw Pointer),并将它指向的值修改为 0。这在 C 语言中是
+.. 一种司空见惯的操作,但在 Rust 中我们需要将他包裹在 unsafe 块中。这是因为,Rust 认为对于裸指针的 **解引用** (Dereference)
+.. 是一种 unsafe 行为。
+
+.. 相比 C 语言,Rust 进行了更多的语义约束来保证安全性(内存安全/类型安全/并发安全),这在编译期和运行期都有所体现。但在某些时候,
+.. 尤其是与底层硬件打交道的时候,在 Rust 的语义约束之内没法满足我们的需求,这个时候我们就需要将超出了 Rust 语义约束的行为包裹
+.. 在 unsafe 块中,告知编译器不需要对它进行完整的约束检查,而是由程序员自己负责保证它的安全性。当代码不能正常运行的时候,我们往往也是
+.. 最先去检查 unsafe 块中的代码,因为它没有受到编译器的保护,出错的概率更大。
+
+.. C 语言中的指针相当于 Rust 中的裸指针,它无所不能但又太过于灵活,程序员对其不谨慎的使用常常会引起很多内存不安全问题,最常见的如
+.. 悬垂指针和多次回收的问题,Rust 编译器没法确认程序员对它的使用是否安全,因此将其划到 unsafe Rust 的领域。在 safe Rust 中,我们
+.. 有引用 ``&/&mut`` 以及各种功能各异的智能指针 ``Box/RefCell/Rc`` 可以使用,只要按照 Rust 的规则来使用它们便可借助
+.. 编译器在编译期就解决很多潜在的内存不安全问题。
+
+Rust编程相关
+--------------------------------
+
+- `OS Tutorial Summer of Code 2020:Rust系统编程入门指导 `_
+- `Stanford 新开的一门很值得学习的 Rust 入门课程 `_
+- `一份简单的 Rust 入门介绍 `_
+- `《RustOS Guide》中的 Rust 介绍部分 `_
+- `一份简单的Rust宏编程新手指南 `_
+
+
+Rust系统编程pattern
+---------------------------------
+
+- `Arc> in Rust `_
+- `Understanding Closures in Rust `_
+- `Closures in Rust `_
\ No newline at end of file
diff --git a/guide/source/appendix-b/index.rst b/guide/source/appendix-b/index.rst
new file mode 100644
index 0000000..22d9e13
--- /dev/null
+++ b/guide/source/appendix-b/index.rst
@@ -0,0 +1,318 @@
+附录 B:常见工具的使用方法
+========================================
+
+.. toctree::
+ :hidden:
+ :maxdepth: 4
+
+
+
+分析可执行文件
+------------------------
+
+对于Rust编译器生成的执行程序,可通过各种有效工具进行分析。如果掌握了对这些工具的使用,那么在后续的开发工作中,对碰到的各种奇怪问题就进行灵活处理和解决了。
+我们以Rust编译生成的一个简单的“Hello, world”应用执行程序为分析对象,看看如何进行分析。
+
+让我们先来通过 ``file`` 工具看看最终生成的可执行文件的格式:
+
+.. code-block:: console
+
+ $ cargo new os
+ $ cd os; cargo build
+ Compiling os v0.1.0 (/tmp/os)
+ Finished dev [unoptimized + debuginfo] target(s) in 0.26s
+
+ $ file target/debug/os
+ target/debug/os: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked,
+ interpreter /lib64/ld-linux-x86-64.so.2, ......
+
+ $
+
+.. _term-elf:
+.. _term-metadata:
+
+从中可以看出可执行文件的格式为 **可执行和链接格式** (Executable and Linkable Format, ELF),硬件平台是 x86-64。在 ELF 文件中,
+除了程序必要的代码、数据段(它们本身都只是一些二进制的数据)之外,还有一些 **元数据** (Metadata) 描述这些段在地址空间中的位置和在
+文件中的位置以及一些权限控制信息,这些元数据只能放在代码、数据段的外面。
+
+rust-readobj
+^^^^^^^^^^^^^^^^^^^^^^^
+
+我们可以通过二进制工具 ``rust-readobj`` 来看看 ELF 文件中究竟包含什么内容,输入命令:
+
+.. code-block:: console
+
+ $ rust-readobj -all target/debug/os
+
+首先可以看到一个 ELF header,它位于 ELF 文件的开头:
+
+.. code-block:: objdump
+ :linenos:
+ :emphasize-lines: 8,19,20,21,24,25,26,27
+
+ File: target/debug/os
+ Format: elf64-x86-64
+ Arch: x86_64
+ AddressSize: 64bit
+ LoadName:
+ ElfHeader {
+ Ident {
+ Magic: (7F 45 4C 46)
+ Class: 64-bit (0x2)
+ DataEncoding: LittleEndian (0x1)
+ FileVersion: 1
+ OS/ABI: SystemV (0x0)
+ ABIVersion: 0
+ Unused: (00 00 00 00 00 00 00)
+ }
+ Type: SharedObject (0x3)
+ Machine: EM_X86_64 (0x3E)
+ Version: 1
+ Entry: 0x5070
+ ProgramHeaderOffset: 0x40
+ SectionHeaderOffset: 0x32D8D0
+ Flags [ (0x0)
+ ]
+ HeaderSize: 64
+ ProgramHeaderEntrySize: 56
+ ProgramHeaderCount: 12
+ SectionHeaderEntrySize: 64
+ SectionHeaderCount: 42
+ StringTableSectionIndex: 41
+ }
+ ......
+
+.. _term-magic:
+
+- 第 8 行是一个称之为 **魔数** (Magic) 独特的常数,存放在 ELF header 的一个固定位置。当加载器将 ELF 文件加载到内存之前,通常会查看
+ 该位置的值是否正确,来快速确认被加载的文件是不是一个 ELF 。
+- 第 19 行给出了可执行文件的入口点为 ``0x5070`` 。
+- 从 20-21 行中,我们可以知道除了 ELF header 之外,还有另外两种不同的 header,分别称为 program header 和 section header,
+ 它们都有多个。ELF header 中给出了其他两种header 的大小、在文件中的位置以及数目。
+- 从 24-27 行中,可以看到有 12 个不同的 program header,它们从文件的 0x40 字节偏移处开始,每个 56 字节;
+ 有64个section header,它们从文件的 0x2D8D0 字节偏移处开始,每个 64 字节;
+
+
+有多个不同的 section header,下面是个具体的例子:
+
+.. code-block:: objdump
+
+ ......
+ Section {
+ Index: 14
+ Name: .text (157)
+ Type: SHT_PROGBITS (0x1)
+ Flags [ (0x6)
+ SHF_ALLOC (0x2)
+ SHF_EXECINSTR (0x4)
+ ]
+ Address: 0x5070
+ Offset: 0x5070
+ Size: 208067
+ Link: 0
+ Info: 0
+ AddressAlignment: 16
+ EntrySize: 0
+ }
+
+
+每个 section header 则描述一个段的元数据。
+
+其中,我们看到了代码段 ``.text`` 需要被加载到地址 ``0x5070`` ,大小 208067 字节,。
+它们分别由元数据的字段 Offset、 Size 和 Address 给出。。
+
+我们还能够看到程序中的符号表:
+
+.. code-block::
+
+ Symbol {
+ Name: _start (37994)
+ Value: 0x5070
+ Size: 47
+ Binding: Global (0x1)
+ Type: Function (0x2)
+ Other: 0
+ Section: .text (0xE)
+ }
+ Symbol {
+ Name: main (38021)
+ Value: 0x51A0
+ Size: 47
+ Binding: Global (0x1)
+ Type: Function (0x2)
+ Other: 0
+ Section: .text (0xE)
+ }
+
+里面包括了我们写的 ``main`` 函数的地址以及用户态执行环境的起始地址 ``_start`` 函数的地址。
+
+因此,从 ELF header 中可以看出,ELF 中的内容按顺序应该是:
+
+- ELF header
+- 若干个 program header
+- 程序各个段的实际数据
+- 若干的 section header
+
+
+rust-objdump
+^^^^^^^^^^^^^^^^^^^^^^^
+
+如果想了解正常的ELF文件的具体指令内容,可以通过 ``rust-objdump`` 工具反汇编ELF文件得到:
+
+.. code-block:: console
+
+ $ rust-objdump -all target/debug/os
+
+具体结果如下:
+
+.. code-block:: objdump
+
+ 505b: e9 c0 ff ff ff jmp 0x5020 <.plt>
+
+ Disassembly of section .plt.got:
+
+ 0000000000005060 <.plt.got>:
+ 5060: ff 25 5a 3f 04 00 jmpq *278362(%rip) # 48fc0 <_GLOBAL_OFFSET_TABLE_+0x628>
+ 5066: 66 90 nop
+
+ Disassembly of section .text:
+
+ 0000000000005070 <_start>:
+ 5070: f3 0f 1e fa endbr64
+ 5074: 31 ed xorl %ebp, %ebp
+ 5076: 49 89 d1 movq %rdx, %r9
+ 5079: 5e popq %rsi
+ 507a: 48 89 e2 movq %rsp, %rdx
+ 507d: 48 83 e4 f0 andq $-16, %rsp
+ 5081: 50 pushq %rax
+ 5082: 54 pushq %rsp
+ 5083: 4c 8d 05 86 2c 03 00 leaq 208006(%rip), %r8 # 37d10 <__libc_csu_fini>
+ 508a: 48 8d 0d 0f 2c 03 00 leaq 207887(%rip), %rcx # 37ca0 <__libc_csu_init>
+ 5091: 48 8d 3d 08 01 00 00 leaq 264(%rip), %rdi # 51a0
+ 5098: ff 15 d2 3b 04 00 callq *277458(%rip) # 48c70 <_GLOBAL_OFFSET_TABLE_+0x2d8>
+ ......
+ 00000000000051a0 :
+ 51a0: 48 83 ec 18 subq $24, %rsp
+ 51a4: 8a 05 db 7a 03 00 movb 228059(%rip), %al # 3cc85 <__rustc_debug_gdb_scripts_section__>
+ 51aa: 48 63 cf movslq %edi, %rcx
+ 51ad: 48 8d 3d ac ff ff ff leaq -84(%rip), %rdi # 5160 <_ZN2os4main17h717a6a6e05a70248E>
+ 51b4: 48 89 74 24 10 movq %rsi, 16(%rsp)
+ 51b9: 48 89 ce movq %rcx, %rsi
+ 51bc: 48 8b 54 24 10 movq 16(%rsp), %rdx
+ 51c1: 88 44 24 0f movb %al, 15(%rsp)
+ 51c5: e8 f6 00 00 00 callq 0x52c0 <_ZN3std2rt10lang_start17hc258028f546a93a1E>
+ 51ca: 48 83 c4 18 addq $24, %rsp
+ 51ce: c3 retq
+ 51cf: 90 nop
+ ......
+
+从上面的反汇编结果,我们可以看到用户态执行环境的入口函数 ``_start`` 以及应用程序的主函数 ``main`` 的地址和具体汇编代码内容。
+
+
+rust-objcopy
+^^^^^^^^^^^^^^^^^^^^^^^
+
+当前的ELF执行程序有许多与执行无直接关系的信息(如调试信息等),可以通过 ``rust-objcopy`` 工具来清除。
+
+.. code-block:: console
+
+ $ rust-objcopy --strip-all target/debug/os target/debug/os.bin
+ $ ls -l target/debug/os*
+ -rwxrwxr-x 2 chyyuu chyyuu 3334992 1月 19 22:26 target/debug/os
+ -rwxrwxr-x 1 chyyuu chyyuu 297200 1月 19 22:59 target/debug/os.bin
+
+ $ ./target/debug/os.bin
+ Hello, world!
+
+可以看到,经过处理的ELF文件 ``os.bin`` 在文件长度上大大减少了,但也能正常执行。
+
+另外,当将程序加载到内存的时候,对于每个 program header 所指向的区域,我们需要将对应的数据从文件复制到内存中。这就需要解析 ELF 的元数据
+才能知道数据在文件中的位置以及即将被加载到内存中的位置。但如果我们不需要从 ELF 中解析元数据就知道程序的内存布局
+(这个内存布局是我们按照需求自己指定的),我们可以手动完成加载任务。
+
+具体的做法是利用 ``rust-objcopy`` 工具删除掉 ELF 文件中的
+所有 header 只保留各个段的实际数据得到一个没有任何符号的纯二进制镜像文件:
+
+.. code-block:: console
+
+ $ rust-objcopy --strip-all target/debug/os -O binary target/debug/os.bin
+
+
+
+这样就生成了一个没有任何符号的纯二进制镜像文件。由于缺少了必要的元数据,我们的 ``file`` 工具也没有办法
+对它完成解析了。而后,我们可直接将这个二进制镜像文件手动载入到内存中合适位置即可。
+
+
+
+qemu 平台上可执行文件和二进制镜像的生成流程
+----------------------------------------------
+
+
+
+make & Makefile
+^^^^^^^^^^^^^^^^^^^^^^^
+
+首先我们还原一下可执行文件和二进制镜像的生成流程:
+
+.. code-block:: makefile
+
+ # os/Makefile
+ TARGET := riscv64gc-unknown-none-elf
+ MODE := release
+ KERNEL_ELF := target/$(TARGET)/$(MODE)/os
+ KERNEL_BIN := $(KERNEL_ELF).bin
+
+ $(KERNEL_BIN): kernel
+ @$(OBJCOPY) $(KERNEL_ELF) --strip-all -O binary $@
+
+ kernel:
+ @cargo build --release
+
+这里可以看出 ``KERNEL_ELF`` 保存最终可执行文件 ``os`` 的路径,而 ``KERNEL_BIN`` 保存只保留各个段数据的二进制镜像文件 ``os.bin``
+的路径。目标 ``kernel`` 直接通过 ``cargo build`` 以 release 模式最终可执行文件,目标 ``KERNEL_BIN`` 依赖于目标 ``kernel``,将
+可执行文件通过 ``rust-objcopy`` 工具加上适当的配置移除所有的 header 和符号得到二进制镜像。
+
+我们可以通过 ``make run`` 直接在 qemu 上运行我们的应用程序,qemu 是一个虚拟机,它完整的模拟了一整套硬件平台,就像是一台真正的计算机
+一样,我们来看运行 qemu 的具体命令:
+
+.. code-block:: makefile
+ :linenos:
+ :emphasize-lines: 11,12,13,14,15
+
+ KERNEL_ENTRY_PA := 0x80020000
+
+ BOARD ?= qemu
+ SBI ?= rustsbi
+ BOOTLOADER := ../bootloader/$(SBI)-$(BOARD).bin
+
+ run: run-inner
+
+ run-inner: build
+ @qemu-system-riscv64 \
+ -machine virt \
+ -nographic \
+ -bios $(BOOTLOADER) \
+ -device loader,file=$(KERNEL_BIN),addr=$(KERNEL_ENTRY_PA)
+
+
+
+qemu
+^^^^^^^^^^^^^^^^^^^^^^^
+
+注意其中高亮部分给出了传给 qemu 的参数。
+
+- ``-machine`` 告诉 qemu 使用预设的硬件配置。在整个项目中我们将一直沿用该配置。
+- ``-bios`` 告诉 qemu 使用我们放在 ``bootloader`` 目录下的预编译版本作为 bootloader。
+- ``-device`` 则告诉 qemu 将二进制镜像加载到内存指定的位置。
+
+可以先输入 Ctrl+A ,再输入 X 来退出 qemu 终端。
+
+.. warning::
+
+ **FIXME:使用 GDB 跟踪 qemu 的运行状态**
+
+其他工具和文件格式说明的参考
+-------------------------------------------------------
+
+- `链接脚本(Linker Scripts)语法和规则解析(翻译自官方手册) `_
+- `Make 命令教程 `_
diff --git a/guide/source/appendix-c/index.rst b/guide/source/appendix-c/index.rst
new file mode 100644
index 0000000..4a5d3ee
--- /dev/null
+++ b/guide/source/appendix-c/index.rst
@@ -0,0 +1,18 @@
+附录 C:深入机器模式:RustSBI
+=================================================
+
+.. toctree::
+ :hidden:
+ :maxdepth: 4
+
+RISC-V指令集的SBI标准规定了类Unix操作系统之下的运行环境规范。这个规范拥有多种实现,RustSBI是它的一种实现。
+
+RISC-V架构中,存在着定义于操作系统之下的运行环境。这个运行环境不仅将引导启动RISC-V下的操作系统, 还将常驻后台,为操作系统提供一系列二进制接口,以便其获取和操作硬件信息。 RISC-V给出了此类环境和二进制接口的规范,称为“操作系统二进制接口”,即“SBI”。
+
+SBI的实现是在M模式下运行的特定于平台的固件,它将管理S、U等特权上的程序或通用的操作系统。
+
+RustSBI项目发起于鹏城实验室的“rCore代码之夏-2020”活动,它是完全由Rust语言开发的SBI实现。 现在它能够在支持的RISC-V设备上运行rCore教程和其它操作系统内核。
+
+RustSBI项目的目标是,制作一个从固件启动的最小Rust语言SBI实现,为可能的复杂实现提供参考和支持。 RustSBI也可以作为一个库使用,帮助更多的SBI开发者适配自己的平台,以支持更多处理器核和片上系统。
+
+当前项目实现源码:https://github.com/luojia65/rustsbi
\ No newline at end of file
diff --git a/guide/source/appendix-d/index.rst b/guide/source/appendix-d/index.rst
new file mode 100644
index 0000000..135613d
--- /dev/null
+++ b/guide/source/appendix-d/index.rst
@@ -0,0 +1,32 @@
+附录 D:RISC-V相关信息
+=================================================
+
+RISCV汇编相关
+-----------------------------------------------
+
+- `RISC-V Assembly Programmer's Manual `_
+- `RISC-V Low-level Test Suits `_
+- `CoreMark®-PRO comprehensive, advanced processor benchmark `_
+- `riscv-tests的使用 `_
+
+RISCV硬件相关
+-----------------------------------------------
+
+Quick Reference
+
+- `Registers & ABI `_
+- `Interrupt `_
+- `ISA & Extensions `_
+- `Toolchain `_
+- `Control and Status Registers (CSRs) `_
+- `Accessing CSRs `_
+- `Assembler & Instructions `_
+
+ISA
+
+- `User-Level ISA, Version 1.12 `_
+- `4 Supervisor-Level ISA, Version 1.12 `_
+- `Vector Extension `_
+- `RISC-V Bitmanip Extension `_
+- `External Debug `_
+- `ISA Resources `_
\ No newline at end of file
diff --git a/guide/source/chapter1/0intro.rst b/guide/source/chapter1/0intro.rst
new file mode 100644
index 0000000..f3a18d5
--- /dev/null
+++ b/guide/source/chapter1/0intro.rst
@@ -0,0 +1,96 @@
+引言
+=====================
+
+本章导读
+--------------------------
+
+大多数程序员的职业生涯都从 ``Hello, world!`` 开始。
+
+.. code-block::
+
+ printf("Hello world!\n");
+ cout << "Hello world!\n";
+ print("Hello world!")
+ System.out.println("Hello world!");
+ echo "Hello world!"
+ println!("Hello world!");
+
+然而,要用几行代码向世界问好,并不像表面上那么简单。
+``Hello, world!`` 程序能够编译运行,靠的是以 **编译器** 为主的开发环境和以 **操作系统** 为主的执行环境。
+
+在本章中,我们将抽丝剥茧,一步步让 ``Hello, world!`` 程序脱离其依赖的执行环境,
+编写一个能打印 ``Hello, world!`` 的 OS。这趟旅途将让我们对应用程序及其执行环境有更深入的理解。
+
+.. attention::
+ 实验指导书存在的目的是帮助读者理解框架代码。
+
+ 为便于测试,完成编程实验时,请以框架代码为基础,不必跟着文档从零开始编写内核。
+
+为了做到这一步,首先需要让程序不依赖于标准库,
+并通过编译。
+
+接下来要让脱离了标准库的程序能输出(即支持 ``println!``),这对程序的开发和调试至关重要。
+我们先在用户态下实现该功能,在 `此处 `_ 获取相关代码。
+
+最后把程序移植到内核态,构建在裸机上支持输出的最小运行时环境。
+
+实践体验
+---------------------------
+
+本章一步步实现了支持打印字符串的简单操作系统。
+
+获取本章代码:
+
+.. code-block:: console
+
+ $ git clone https://github.com/LearningOS/rCore-Tutorial-Code-2022S
+ $ cd rCore-Tutorial-Code-2022S
+ $ git checkout ch1
+
+运行本章代码,并设置日志级别为 ``TRACE``:
+
+.. code-block:: console
+
+ $ cd os
+ $ make run LOG=TRACE
+
+
+预期输出:
+
+.. figure:: color-demo.png
+ :align: center
+
+除了 ``Hello, world!`` 之外还有一些额外的信息,最后关机。
+
+本章代码树
+------------------------------------------------
+
+
+.. code-block::
+
+ ├── bootloader (内核依赖的运行在 M 特权级的 SBI 实现,本项目中我们使用 RustSBI)
+ │ └── rustsbi-qemu.bin
+ ├── os
+ │ ├── Cargo.toml (cargo 项目配置文件)
+ │ ├── Makefile
+ │ └── src
+ │ ├── console.rs (将打印字符的 SBI 接口进一步封装实现更加强大的格式化输出)
+ │ ├── entry.asm (设置内核执行环境的的一段汇编代码)
+ │ ├── lang_items.rs (需要我们提供给 Rust 编译器的一些语义项,目前包含内核 panic 时的处理逻辑)
+ │ ├── linker.ld (控制内核内存布局的链接脚本以使内核运行在 qemu 虚拟机上)
+ │ ├── logging.rs (为本项目实现了日志功能)
+ │ ├── main.rs (内核主函数)
+ │ └── sbi.rs (封装底层 SBI 实现提供的 SBI 接口)
+ └── rust-toolchain (整个项目的工具链版本)
+
+ cloc os
+ -------------------------------------------------------------------------------
+ Language files blank comment code
+ -------------------------------------------------------------------------------
+ Rust 5 25 6 155
+ make 1 11 4 34
+ Assembly 1 1 0 11
+ TOML 1 2 1 7
+ -------------------------------------------------------------------------------
+ SUM: 8 39 11 207
+ -------------------------------------------------------------------------------
\ No newline at end of file
diff --git a/guide/source/chapter1/1app-ee-platform.rst b/guide/source/chapter1/1app-ee-platform.rst
new file mode 100644
index 0000000..4ccd02b
--- /dev/null
+++ b/guide/source/chapter1/1app-ee-platform.rst
@@ -0,0 +1,120 @@
+应用程序执行环境与平台支持
+================================================
+
+.. toctree::
+ :hidden:
+ :maxdepth: 5
+
+
+执行应用程序
+-------------------------------
+
+我们先从最简单的 Rust ``Hello, world`` 程序开始,用 Cargo 工具创建 Rust 项目。
+
+.. code-block:: console
+
+ $ cargo new os
+
+此时,项目的文件结构如下:
+
+.. code-block:: console
+
+ $ tree os
+ os
+ ├── Cargo.toml
+ └── src
+ └── main.rs
+
+ 1 directory, 2 files
+
+其中 ``Cargo.toml`` 中保存了项目的库依赖、作者信息等。
+
+cargo 为我们准备好了 ``Hello world!`` 源代码:
+
+.. code-block:: rust
+ :linenos:
+ :caption: 最简单的 Rust 应用
+
+ fn main() {
+ println!("Hello, world!");
+ }
+
+输入 ``cargo run`` 构建并运行项目:
+
+.. code-block:: console
+
+ Compiling os v0.1.0 (/home/shinbokuow/workspace/v3/rCore-Tutorial-v3/os)
+ Finished dev [unoptimized + debuginfo] target(s) in 1.15s
+ Running `target/debug/os`
+ Hello, world!
+
+我们在屏幕上看到了一行 ``Hello, world!`` ,但为了打印出 ``Hello, world!``,我们需要的不止几行源代码。
+
+理解应用程序执行环境
+-------------------------------
+
+在现代通用操作系统(如 Linux)上运行应用程序,需要多层次的执行环境栈支持:
+
+
+.. figure:: app-software-stack.png
+ :align: center
+
+ 应用程序执行环境栈:图中的白色块自上而下表示各级执行环境,黑色块则表示相邻两层执行环境之间的接口。
+ 下层作为上层的执行环境,支持上层代码运行。
+
+我们的应用程序通过调用标准库或第三方库提供的接口,仅需少量源代码就能完成复杂的功能;
+``Hello, world!`` 程序调用的 ``println!`` 宏就是由 Rust 标准库 std 和 GNU Libc 等提供的。
+这些库属于应用程序的 **执行环境** (Execution Environment),而它们的实现又依赖于操作系统提供的系统调用。
+
+平台与目标三元组
+---------------------------------------
+
+编译器在编译、链接得到可执行文件时需要知道,程序要在哪个 **平台** (Platform) 上运行,
+**目标三元组** (Target Triplet) 描述了目标平台的 CPU 指令集、操作系统类型和标准运行时库。
+
+我们研究一下现在 ``Hello, world!`` 程序的目标三元组是什么:
+
+.. code-block:: console
+
+ $ rustc --version --verbose
+ rustc 1.61.0-nightly (68369a041 2022-02-22)
+ binary: rustc
+ commit-hash: 68369a041cea809a87e5bd80701da90e0e0a4799
+ commit-date: 2022-02-22
+ host: x86_64-unknown-linux-gnu
+ release: 1.61.0-nightly
+ LLVM version: 14.0.0
+
+其中 host 一项表明默认目标平台是 ``x86_64-unknown-linux-gnu``,
+CPU 架构是 x86_64,CPU 厂商是 unknown,操作系统是 linux,运行时库是 gnu libc。
+
+接下来,我们希望把 ``Hello, world!`` 移植到 RICV 目标平台 ``riscv64gc-unknown-none-elf`` 上运行。
+
+.. note::
+
+ ``riscv64gc-unknown-none-elf`` 的 CPU 架构是 riscv64gc,厂商是 unknown,操作系统是 none,
+ elf 表示没有标准的运行时库。没有任何系统调用的封装支持,但可以生成 ELF 格式的执行程序。
+ 我们不选择有 linux-gnu 支持的 ``riscv64gc-unknown-linux-gnu``,是因为我们的目标是开发操作系统内核,而非在 linux 系统上运行的应用程序。
+
+修改目标平台
+----------------------------------
+
+将程序的目标平台换成 ``riscv64gc-unknown-none-elf``,试试看会发生什么:
+
+.. code-block:: console
+
+ $ cargo run --target riscv64gc-unknown-none-elf
+ Compiling os v0.1.0 (/home/shinbokuow/workspace/v3/rCore-Tutorial-v3/os)
+ error[E0463]: can't find crate for `std`
+ |
+ = note: the `riscv64gc-unknown-none-elf` target may not be installed
+
+
+报错的原因是目标平台上确实没有 Rust 标准库 std,也不存在任何受 OS 支持的系统调用。
+这样的平台被我们称为 **裸机平台** (bare-metal)。
+
+幸运的是,除了 std 之外,Rust 还有一个不需要任何操作系统支持的核心库 core,
+它包含了 Rust 语言相当一部分核心机制,可以满足本门课程的需求。
+有很多第三方库也不依赖标准库 std,而仅仅依赖核心库 core。
+
+为了以裸机平台为目标编译程序,我们要将对标准库 std 的引用换成核心库 core。
\ No newline at end of file
diff --git a/guide/source/chapter1/2remove-std.rst b/guide/source/chapter1/2remove-std.rst
new file mode 100644
index 0000000..f70f4d8
--- /dev/null
+++ b/guide/source/chapter1/2remove-std.rst
@@ -0,0 +1,158 @@
+.. _term-remove-std:
+
+移除标准库依赖
+==========================
+
+.. toctree::
+ :hidden:
+ :maxdepth: 5
+
+
+首先在 ``os`` 目录下新建 ``.cargo`` 目录,并在这个目录下创建 ``config`` 文件,输入如下内容:
+
+.. code-block:: toml
+
+ # os/.cargo/config
+ [build]
+ target = "riscv64gc-unknown-none-elf"
+
+
+这将使 cargo 工具在 os 目录下默认会使用 riscv64gc-unknown-none-elf 作为目标平台。
+这种编译器运行的平台(x86_64)与可执行文件运行的目标平台不同的情况,称为 **交叉编译** (Cross Compile)。
+
+移除 println! 宏
+----------------------------------
+
+
+我们在 ``main.rs`` 的开头加上一行 ``#![no_std]``,
+告诉 Rust 编译器不使用 Rust 标准库 std 转而使用核心库 core。重新编译,报错如下:
+
+.. error::
+
+ .. code-block:: console
+
+ $ cargo build
+ Compiling os v0.1.0 (/home/shinbokuow/workspace/v3/rCore-Tutorial-v3/os)
+ error: cannot find macro `println` in this scope
+ --> src/main.rs:4:5
+ |
+ 4 | println!("Hello, world!");
+ | ^^^^^^^
+
+println! 宏是由标准库 std 提供的,且会使用到一个名为 write 的系统调用。
+无论如何,我们先将这行代码注释掉。
+
+
+提供语义项 panic_handler
+----------------------------------------------------
+
+.. error::
+
+ .. code-block:: console
+
+ $ cargo build
+ Compiling os v0.1.0 (/home/shinbokuow/workspace/v3/rCore-Tutorial-v3/os)
+ error: `#[panic_handler]` function required, but not found
+
+标准库 std 提供了 Rust 错误处理函数 ``#[panic_handler]``,其大致功能是打印出错位置和原因并杀死当前应用。
+但核心库 core 并没有提供这项功能,得靠我们自己实现。
+
+新建一个子模块 ``lang_items.rs``,在里面编写 panic 处理函数,通过标记 ``#[panic_handler]`` 告知编译器采用我们的实现:
+
+.. code-block:: rust
+
+ // os/src/lang_items.rs
+ use core::panic::PanicInfo;
+
+ #[panic_handler]
+ fn panic(_info: &PanicInfo) -> ! {
+ loop {}
+ }
+
+目前我们遇到错误什么都不做,只在原地 ``loop`` 。
+
+移除 main 函数
+-----------------------------
+
+重新编译,又有了新错误:
+
+.. error::
+
+ .. code-block::
+
+ $ cargo build
+ Compiling os v0.1.0 (/home/shinbokuow/workspace/v3/rCore-Tutorial-v3/os)
+ error: requires `start` lang_item
+
+编译器提醒我们缺少一个名为 ``start`` 的语义项。
+``start`` 语义项代表了标准库 std 在执行应用程序之前需要进行的一些初始化工作。由于我们禁用了标准库,编译器也就找不到这项功能的实现了。
+
+在 ``main.rs`` 的开头加入设置 ``#![no_main]`` 告诉编译器我们没有一般意义上的 ``main`` 函数,
+并将原来的 ``main`` 函数删除。这样编译器也就不需要考虑初始化工作了。
+
+.. code-block:: console
+
+ $ cargo build
+ Compiling os v0.1.0 (/home/shinbokuow/workspace/v3/rCore-Tutorial-v3/os)
+ Finished dev [unoptimized + debuginfo] target(s) in 0.06s
+
+至此,我们终于移除了所有标准库依赖,目前的代码如下:
+
+.. code-block:: rust
+
+ // os/src/main.rs
+ #![no_std]
+ #![no_main]
+
+ mod lang_items;
+
+ // os/src/lang_items.rs
+ use core::panic::PanicInfo;
+
+ #[panic_handler]
+ fn panic(_info: &PanicInfo) -> ! {
+ loop {}
+ }
+
+
+分析被移除标准库的程序
+-----------------------------
+
+我们可以通过一些工具来分析目前的程序:
+
+.. code-block:: console
+
+ [文件格式]
+ $ file target/riscv64gc-unknown-none-elf/debug/os
+ target/riscv64gc-unknown-none-elf/debug/os: ELF 64-bit LSB executable, UCB RISC-V, ......
+
+ [文件头信息]
+ $ rust-readobj -h target/riscv64gc-unknown-none-elf/debug/os
+ File: target/riscv64gc-unknown-none-elf/debug/os
+ Format: elf64-littleriscv
+ Arch: riscv64
+ AddressSize: 64bit
+ ......
+ Type: Executable (0x2)
+ Machine: EM_RISCV (0xF3)
+ Version: 1
+ Entry: 0x0
+ ......
+ }
+
+ [反汇编导出汇编程序]
+ $ rust-objdump -S target/riscv64gc-unknown-none-elf/debug/os
+ target/riscv64gc-unknown-none-elf/debug/os: file format elf64-littleriscv
+
+
+通过 ``file`` 工具对二进制程序 ``os`` 的分析可以看到,它好像是一个合法的 RV64 执行程序,
+但 ``rust-readobj`` 工具告诉我们它的入口地址 Entry 是 ``0``。
+再通过 ``rust-objdump`` 工具把它反汇编,没有生成任何汇编代码。
+可见,这个二进制程序虽然合法,但它是一个空程序,原因是缺少了编译器规定的入口函数 ``_start`` 。
+
+从下一节开始,我们将着手实现本节移除的、由用户态执行环境提供的功能。
+
+.. note::
+
+ 本节内容部分参考自 `BlogOS 的相关章节 `_ 。
+
diff --git a/guide/source/chapter1/3mini-rt-usrland.rst b/guide/source/chapter1/3mini-rt-usrland.rst
new file mode 100644
index 0000000..b466f9d
--- /dev/null
+++ b/guide/source/chapter1/3mini-rt-usrland.rst
@@ -0,0 +1,282 @@
+.. _term-print-userminienv:
+
+构建用户态执行环境
+=================================
+
+.. toctree::
+ :hidden:
+ :maxdepth: 5
+
+.. note::
+
+ 前三小节的用户态程序案例代码在 `此处 `_ 获取。
+
+
+用户态最小化执行环境
+----------------------------
+
+执行环境初始化
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+首先我们要给 Rust 编译器编译器提供入口函数 ``_start()`` ,
+在 ``main.rs`` 中添加如下内容:
+
+
+.. code-block:: rust
+
+ // os/src/main.rs
+ #[no_mangle]
+ extern "C" fn _start() {
+ loop{};
+ }
+
+
+对上述代码重新编译,再用分析工具分析:
+
+
+.. code-block:: console
+
+ $ cargo build
+ Compiling os v0.1.0 (/home/shinbokuow/workspace/v3/rCore-Tutorial-v3/os)
+ Finished dev [unoptimized + debuginfo] target(s) in 0.06s
+
+ [反汇编导出汇编程序]
+ $ rust-objdump -S target/riscv64gc-unknown-none-elf/debug/os
+ target/riscv64gc-unknown-none-elf/debug/os: file format elf64-littleriscv
+
+ Disassembly of section .text:
+
+ 0000000000011120 <_start>:
+ ; loop {}
+ 11120: 09 a0 j 2 <_start+0x2>
+ 11122: 01 a0 j 0 <_start+0x2>
+
+
+反汇编出的两条指令就是一个死循环,
+这说明编译器生成的已经是一个合理的程序了。
+用 ``qemu-riscv64 target/riscv64gc-unknown-none-elf/debug/os`` 命令可以执行这个程序。
+
+
+程序正常退出
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+我们把 ``_start()`` 函数中的循环语句注释掉,重新编译并分析,看到其汇编代码是:
+
+
+.. code-block:: console
+
+ $ rust-objdump -S target/riscv64gc-unknown-none-elf/debug/os
+
+ target/riscv64gc-unknown-none-elf/debug/os: file format elf64-littleriscv
+
+
+ Disassembly of section .text:
+
+ 0000000000011120 <_start>:
+ ; }
+ 11120: 82 80 ret
+
+看起来是合法的执行程序。但如果我们执行它,会引发问题:
+
+.. code-block:: console
+
+ $ qemu-riscv64 target/riscv64gc-unknown-none-elf/debug/os
+ 段错误 (核心已转储)
+
+这个简单的程序导致 ``qemu-riscv64`` 崩溃了!为什么会这样?
+
+.. note::
+
+ QEMU有两种运行模式:
+
+ ``User mode`` 模式,即用户态模拟,如 ``qemu-riscv64`` 程序,
+ 能够模拟不同处理器的用户态指令的执行,并可以直接解析ELF可执行文件,
+ 加载运行那些为不同处理器编译的用户级Linux应用程序。
+
+ ``System mode`` 模式,即系统态模式,如 ``qemu-system-riscv64`` 程序,
+ 能够模拟一个完整的基于不同CPU的硬件系统,包括处理器、内存及其他外部设备,支持运行完整的操作系统。
+
+
+目前的执行环境还缺了一个退出机制,我们需要操作系统提供的 ``exit`` 系统调用来退出程序。这里先给出代码:
+
+.. code-block:: rust
+
+ // os/src/main.rs
+
+ const SYSCALL_EXIT: usize = 93;
+
+ fn syscall(id: usize, args: [usize; 3]) -> isize {
+ let mut ret;
+ unsafe {
+ core::arch::asm!(
+ "ecall",
+ inlateout("x10") args[0] => ret,
+ in("x11") args[1],
+ in("x12") args[2],
+ in("x17") id,
+ );
+ }
+ ret
+ }
+
+ pub fn sys_exit(xstate: i32) -> isize {
+ syscall(SYSCALL_EXIT, [xstate as usize, 0, 0])
+ }
+
+ #[no_mangle]
+ extern "C" fn _start() {
+ sys_exit(9);
+ }
+
+``main.rs`` 增加的内容不多,但还是有点与一般的应用程序有所不同,因为它引入了汇编和系统调用。
+第二章的第二节 :doc:`/chapter2/2application` 会详细介绍上述代码的含义。
+这里读者只需要知道 ``_start`` 函数调用了一个 ``sys_exit`` 函数,
+向操作系统发出了退出的系统调用请求,退出码为 ``9`` 。
+
+我们编译执行以下修改后的程序:
+
+.. code-block:: console
+
+ $ cargo build --target riscv64gc-unknown-none-elf
+ Compiling os v0.1.0 (/media/chyyuu/ca8c7ba6-51b7-41fc-8430-e29e31e5328f/thecode/rust/os_kernel_lab/os)
+ Finished dev [unoptimized + debuginfo] target(s) in 0.26s
+
+ [打印程序的返回值]
+ $ qemu-riscv64 target/riscv64gc-unknown-none-elf/debug/os; echo $?
+ 9
+
+可以看到,返回的结果确实是 ``9`` 。这样,我们勉强完成了一个简陋的用户态最小化执行环境。
+
+
+有显示支持的用户态执行环境
+----------------------------
+
+没有 ``println`` 输出信息,终究觉得缺了点啥。
+
+Rust 的 core 库内建了以一系列帮助实现显示字符的基本 Trait 和数据结构,函数等,我们可以对其中的关键部分进行扩展,就可以实现定制的 ``println!`` 功能。
+
+
+实现输出字符串的相关函数
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. attention::
+
+ 如果你觉得理解 Rust 宏有困难,把它当成黑盒就好!
+
+
+首先封装一下对 ``SYSCALL_WRITE`` 系统调用。
+
+.. code-block:: rust
+
+ const SYSCALL_WRITE: usize = 64;
+
+ pub fn sys_write(fd: usize, buffer: &[u8]) -> isize {
+ syscall(SYSCALL_WRITE, [fd, buffer.as_ptr() as usize, buffer.len()])
+ }
+
+然后实现基于 ``Write`` Trait 的数据结构,并完成 ``Write`` Trait 所需要的 ``write_str`` 函数,并用 ``print`` 函数进行包装。
+
+
+.. code-block:: rust
+
+ struct Stdout;
+
+ impl Write for Stdout {
+ fn write_str(&mut self, s: &str) -> fmt::Result {
+ sys_write(1, s.as_bytes());
+ Ok(())
+ }
+ }
+
+ pub fn print(args: fmt::Arguments) {
+ Stdout.write_fmt(args).unwrap();
+ }
+
+最后,实现基于 ``print`` 函数,实现Rust语言 **格式化宏** ( `formatting macros `_ )。
+
+
+.. code-block:: rust
+
+ #[macro_export]
+ macro_rules! print {
+ ($fmt: literal $(, $($arg: tt)+)?) => {
+ $crate::console::print(format_args!($fmt $(, $($arg)+)?));
+ }
+ }
+
+ #[macro_export]
+ macro_rules! println {
+ ($fmt: literal $(, $($arg: tt)+)?) => {
+ print(format_args!(concat!($fmt, "\n") $(, $($arg)+)?));
+ }
+ }
+
+接下来,我们调整一下应用程序,让它发出显示字符串和退出的请求:
+
+.. code-block:: rust
+
+ #[no_mangle]
+ extern "C" fn _start() {
+ println!("Hello, world!");
+ sys_exit(9);
+ }
+
+
+现在,我们编译并执行一下,可以看到正确的字符串输出,且程序也能正确退出!
+
+
+.. code-block:: console
+
+ $ cargo build --target riscv64gc-unknown-none-elf
+ Compiling os v0.1.0 (/media/chyyuu/ca8c7ba6-51b7-41fc-8430-e29e31e5328f/thecode/rust/os_kernel_lab/os)
+ Finished dev [unoptimized + debuginfo] target(s) in 0.61s
+
+ $ qemu-riscv64 target/riscv64gc-unknown-none-elf/debug/os; echo $?
+ Hello, world!
+ 9
+
+
+.. 下面出错的情况是会在采用 linker.ld,加入了 .cargo/config
+.. 的内容后会出错:
+.. .. [build]
+.. .. target = "riscv64gc-unknown-none-elf"
+.. .. [target.riscv64gc-unknown-none-elf]
+.. .. rustflags = [
+.. .. "-Clink-arg=-Tsrc/linker.ld", "-Cforce-frame-pointers=yes"
+.. .. ]
+
+.. 重新定义了栈和地址空间布局后才会出错
+
+.. 段错误 (核心已转储)
+
+.. 系统崩溃了!借助以往的操作系统内核编程经验和与下一节调试kernel的成果经验,我们直接定位为是 **栈** (Stack) 没有设置的问题。我们需要添加建立栈的代码逻辑。
+
+.. .. code-block:: asm
+
+.. # entry.asm
+
+.. .section .text.entry
+.. .globl _start
+.. _start:
+.. la sp, boot_stack_top
+.. call rust_main
+
+.. .section .bss.stack
+.. .globl boot_stack
+.. boot_stack:
+.. .space 4096 * 16
+.. .globl boot_stack_top
+.. boot_stack_top:
+
+.. 然后把汇编代码嵌入到 ``main.rs`` 中,并进行微调。
+
+.. .. code-block:: rust
+
+.. #![feature(global_asm)]
+
+.. global_asm!(include_str!("entry.asm"));
+
+.. #[no_mangle]
+.. #[link_section=".text.entry"]
+.. extern "C" fn rust_main() {
+
+.. 再次编译执行,可以看到正确的字符串输出,且程序也能正确结束!
diff --git a/guide/source/chapter1/4mini-rt-baremetal.rst b/guide/source/chapter1/4mini-rt-baremetal.rst
new file mode 100644
index 0000000..7cba2f9
--- /dev/null
+++ b/guide/source/chapter1/4mini-rt-baremetal.rst
@@ -0,0 +1,328 @@
+.. _term-print-kernelminienv:
+
+构建裸机执行环境
+=================================
+
+.. toctree::
+ :hidden:
+ :maxdepth: 5
+
+有了上一节实现的用户态的最小执行环境,稍加改造,就可以完成裸机上的最小执行环境了。
+本节中,我们将把 ``Hello world!`` 应用程序从用户态搬到内核态。
+
+
+裸机启动过程
+----------------------------
+
+用 QEMU 软件 ``qemu-system-riscv64`` 来模拟 RISC-V 64 计算机。加载内核程序的命令如下:
+
+.. code-block:: bash
+
+ qemu-system-riscv64 \
+ -machine virt \
+ -nographic \
+ -bios $(BOOTLOADER) \
+ -device loader,file=$(KERNEL_BIN),addr=$(KERNEL_ENTRY_PA)
+
+
+- ``-bios $(BOOTLOADER)`` 意味着硬件加载了一个 BootLoader 程序,即 RustSBI
+- ``-device loader,file=$(KERNEL_BIN),addr=$(KERNEL_ENTRY_PA)`` 表示硬件内存中的特定位置 ``$(KERNEL_ENTRY_PA)`` 放置了操作系统的二进制代码 ``$(KERNEL_BIN)`` 。 ``$(KERNEL_ENTRY_PA)`` 的值是 ``0x80200000`` 。
+
+当我们执行包含上述启动参数的 qemu-system-riscv64 软件,就意味给这台虚拟的 RISC-V64 计算机加电了。
+此时,CPU 的其它通用寄存器清零,而 PC 会指向 ``0x1000`` 的位置,这里有固化在硬件中的一小段引导代码,
+它会很快跳转到 ``0x80000000`` 的 RustSBI 处。
+RustSBI完成硬件初始化后,会跳转到 ``$(KERNEL_BIN)`` 所在内存位置 ``0x80200000`` 处,
+执行操作系统的第一条指令。
+
+.. figure:: chap1-intro.png
+ :align: center
+
+.. note::
+
+ **RustSBI 是什么?**
+
+ SBI 是 RISC-V 的一种底层规范,RustSBI 是它的一种实现。
+ 操作系统内核与 RustSBI 的关系有点像应用与操作系统内核的关系,后者向前者提供一定的服务。只是SBI提供的服务很少,
+ 比如关机,显示字符串等。
+
+实现关机功能
+----------------------------
+
+对上一节实现的代码稍作调整,通过 ``ecall`` 调用 RustSBI 实现关机功能:
+
+.. _term-llvm-sbicall:
+
+.. code-block:: rust
+
+ // bootloader/rustsbi-qemu.bin 直接添加的SBI规范实现的二进制代码,给操作系统提供基本支持服务
+
+ // os/src/sbi.rs
+ fn sbi_call(which: usize, arg0: usize, arg1: usize, arg2: usize) -> usize {
+ let mut ret;
+ unsafe {
+ core::arch::asm!(
+ "ecall",
+ ...
+
+ const SBI_SHUTDOWN: usize = 8;
+
+ pub fn shutdown() -> ! {
+ sbi_call(SBI_SHUTDOWN, 0, 0, 0);
+ panic!("It should shutdown!");
+ }
+
+ // os/src/main.rs
+ #[no_mangle]
+ extern "C" fn _start() {
+ shutdown();
+ }
+
+
+应用程序访问操作系统提供的系统调用的指令是 ``ecall`` ,操作系统访问
+RustSBI提供的SBI调用的指令也是 ``ecall`` ,
+虽然指令一样,但它们所在的特权级是不一样的。
+简单地说,应用程序位于最弱的用户特权级(User Mode),
+操作系统位于内核特权级(Supervisor Mode),
+RustSBI位于机器特权级(Machine Mode)。
+下一章会进一步阐释具体细节。
+
+编译执行,结果如下:
+
+.. code-block:: bash
+
+ # 编译生成ELF格式的执行文件
+ $ cargo build --release
+ Compiling os v0.1.0 (/media/chyyuu/ca8c7ba6-51b7-41fc-8430-e29e31e5328f/thecode/rust/os_kernel_lab/os)
+ Finished release [optimized] target(s) in 0.15s
+ # 把ELF执行文件转成bianary文件
+ $ rust-objcopy --binary-architecture=riscv64 target/riscv64gc-unknown-none-elf/release/os --strip-all -O binary target/riscv64gc-unknown-none-elf/release/os.bin
+
+ # 加载运行
+ $ qemu-system-riscv64 -machine virt -nographic -bios ../bootloader/rustsbi-qemu.bin -device loader,file=target/riscv64gc-unknown-none-elf/release/os.bin,addr=0x80200000
+ # 无法退出,风扇狂转,感觉碰到死循环
+
+问题在哪?通过 rust-readobj 分析 ``os`` 可执行程序,发现其入口地址不是
+RustSBI 约定的 ``0x80200000`` 。我们需要修改程序的内存布局并设置好栈空间。
+
+
+设置正确的程序内存布局
+----------------------------
+
+可以通过 **链接脚本** (Linker Script) 调整链接器的行为,使得最终生成的可执行文件的内存布局符合我们的预期。
+
+修改 Cargo 的配置文件来使用我们自己的链接脚本 ``os/src/linker.ld``:
+
+.. code-block::
+ :linenos:
+ :emphasize-lines: 5,6,7,8
+
+ // os/.cargo/config
+ [build]
+ target = "riscv64gc-unknown-none-elf"
+
+ [target.riscv64gc-unknown-none-elf]
+ rustflags = [
+ "-Clink-arg=-Tsrc/linker.ld", "-Cforce-frame-pointers=yes"
+ ]
+
+具体的链接脚本 ``os/src/linker.ld`` 如下:
+
+.. code-block::
+ :linenos:
+
+ OUTPUT_ARCH(riscv)
+ ENTRY(_start)
+ BASE_ADDRESS = 0x80200000;
+
+ SECTIONS
+ {
+ . = BASE_ADDRESS;
+ skernel = .;
+
+ stext = .;
+ .text : {
+ *(.text.entry)
+ *(.text .text.*)
+ }
+
+ . = ALIGN(4K);
+ etext = .;
+ srodata = .;
+ .rodata : {
+ *(.rodata .rodata.*)
+ }
+
+ . = ALIGN(4K);
+ erodata = .;
+ sdata = .;
+ .data : {
+ *(.data .data.*)
+ }
+
+ . = ALIGN(4K);
+ edata = .;
+ .bss : {
+ *(.bss.stack)
+ sbss = .;
+ *(.bss .bss.*)
+ }
+
+ . = ALIGN(4K);
+ ebss = .;
+ ekernel = .;
+
+ /DISCARD/ : {
+ *(.eh_frame)
+ }
+ }
+
+第 1 行我们设置了目标平台为 riscv ;第 2 行我们设置了整个程序的入口点为之前定义的全局符号 ``_start``;
+第 3 行定义了一个常量 ``BASE_ADDRESS`` 为 ``0x80200000`` ,RustSBI 期望的 OS 起始地址;
+
+.. attention::
+
+ linker 脚本的语法不做要求,感兴趣的同学可以自行查阅相关资料。
+
+从 ``BASE_ADDRESS`` 开始,代码段 ``.text``, 只读数据段 ``.rodata``,数据段 ``.data``, bss 段 ``.bss`` 由低到高依次放置,
+且每个段都有两个全局变量给出其起始和结束地址(比如 ``.text`` 段的开始和结束地址分别是 ``stext`` 和 ``etext`` )。
+
+
+正确配置栈空间布局
+----------------------------
+
+用另一段汇编代码初始化栈空间:
+
+.. code-block:: asm
+ :linenos:
+
+ # os/src/entry.asm
+ .section .text.entry
+ .globl _start
+ _start:
+ la sp, boot_stack_top
+ call rust_main
+
+ .section .bss.stack
+ .globl boot_stack
+ boot_stack:
+ .space 4096 * 16
+ .globl boot_stack_top
+ boot_stack_top:
+
+在第 8 行,我们预留了一块大小为 4096 * 16 字节,也就是 :math:`64\text{KiB}` 的空间,
+用作操作系统的栈空间。
+栈顶地址被全局符号 ``boot_stack_top`` 标识,栈底则被全局符号 ``boot_stack`` 标识。
+同时,这块栈空间被命名为
+``.bss.stack`` ,链接脚本里有它的位置。
+
+``_start`` 作为操作系统的入口地址,将依据链接脚本被放在 ``BASE_ADDRESS`` 处。
+``la sp, boot_stack_top`` 作为 OS 的第一条指令,
+将 sp 设置为栈空间的栈顶。
+简单起见,我们目前不考虑 sp 越过栈底 ``boot_stack`` ,也就是栈溢出的情形。
+第二条指令则是函数调用 ``rust_main`` ,这里的 ``rust_main`` 是我们稍后自己编写的应用入口。
+
+接着,我们在 ``main.rs`` 中嵌入这些汇编代码并声明应用入口 ``rust_main`` :
+
+.. code-block:: rust
+ :linenos:
+ :emphasize-lines: 7,9,10,11,12
+
+ // os/src/main.rs
+ #![no_std]
+ #![no_main]
+
+ mod lang_items;
+
+ core::arch::global_asm!(include_str!("entry.asm"));
+
+ #[no_mangle]
+ pub fn rust_main() -> ! {
+ shutdown();
+ }
+
+背景高亮指出了 ``main.rs`` 中新增的代码。
+
+第 7 行,我们使用 ``global_asm`` 宏,将同目录下的汇编文件 ``entry.asm`` 嵌入到代码中。
+
+从第 9 行开始,
+我们声明了应用的入口点 ``rust_main`` ,需要注意的是,这里通过宏将 ``rust_main``
+标记为 ``#[no_mangle]`` 以避免编译器对它的名字进行混淆,不然在链接时,
+``entry.asm`` 将找不到 ``main.rs`` 提供的外部符号 ``rust_main``,导致链接失败。
+
+再次使用上节中的编译,生成和运行操作,我们看到QEMU模拟的RISC-V 64计算机 **优雅** 地退出了!
+
+.. code-block:: console
+ # 教程使用的 RustSBI 版本比代码框架稍旧,输出有所不同
+ $ qemu-system-riscv64 \
+ > -machine virt \
+ > -nographic \
+ > -bios ../bootloader/rustsbi-qemu.bin \
+ > -device loader,file=target/riscv64gc-unknown-none-elf/release/os.bin,addr=0x80200000
+ [rustsbi] Version 0.1.0
+ .______ __ __ _______.___________. _______..______ __
+ | _ \ | | | | / | | / || _ \ | |
+ | |_) | | | | | | (----`---| |----`| (----`| |_) || |
+ | / | | | | \ \ | | \ \ | _ < | |
+ | |\ \----.| `--' |.----) | | | .----) | | |_) || |
+ | _| `._____| \______/ |_______/ |__| |_______/ |______/ |__|
+
+ [rustsbi] Platform: QEMU
+ [rustsbi] misa: RV64ACDFIMSU
+ [rustsbi] mideleg: 0x222
+ [rustsbi] medeleg: 0xb1ab
+ [rustsbi] Kernel entry: 0x80200000
+
+
+清空 .bss 段
+----------------------------------
+
+等一等,与内存相关的部分太容易出错了, **清零 .bss 段** 的工作我们还没有完成。
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/main.rs
+ fn clear_bss() {
+ extern "C" {
+ fn sbss();
+ fn ebss();
+ }
+ (sbss as usize..ebss as usize).for_each(|a| {
+ unsafe { (a as *mut u8).write_volatile(0) }
+ });
+ }
+
+ pub fn rust_main() -> ! {
+ clear_bss();
+ shutdown();
+ }
+
+链接脚本 ``linker.ld`` 中给出的全局符号 ``sbss`` 和 ``ebss`` 让我们能轻松确定 ``.bss`` 段的位置。
+
+
+添加裸机打印相关函数
+----------------------------------
+
+在上一节中我们为用户态程序实现的 ``println`` 宏,略作修改即可用于本节的内核态操作系统。
+详见 ``os/src/console.rs``。
+
+利用 ``println`` 宏,我们重写异常处理函数 ``panic``,使其在 panic 时能打印错误发生的位置。
+相关代码位于 ``os/src/lang_items.rs`` 中。
+
+我们还使用第三方库 ``log`` 为你实现了日志模块,相关代码位于 ``os/src/logging.rs`` 中。
+
+.. note::
+
+ 在 cargo 项目中引入外部库 log,需要修改 ``Cargo.toml`` 加入相应的依赖信息。
+
+现在,让我们重复一遍本章开头的试验,``make run LOG=TRACE``!
+
+.. figure:: color-demo.png
+ :align: center
+
+产生 panic 的地点与源码中的实际位置一致!至此,我们完成了第一章的实验内容,
+
+
+.. note::
+
+ 背景知识:`理解应用程序和执行环境 `_
\ No newline at end of file
diff --git a/guide/source/chapter1/5exercise.rst b/guide/source/chapter1/5exercise.rst
new file mode 100644
index 0000000..268ccc9
--- /dev/null
+++ b/guide/source/chapter1/5exercise.rst
@@ -0,0 +1,143 @@
+chapter1练习(已经废弃,没删是怕以后有用)
+=====================================================
+
+.. toctree::
+ :hidden:
+ :maxdepth: 4
+
+- 本节难度: **低**
+
+编程作业
+-------------------------------
+
+彩色化 LOG
++++++++++++++++++++++++++++++++
+
+.. lab1 的工作使得我们从硬件世界跳入了软件世界,当看到自己的小 os 可以在裸机硬件上输出 ``hello world`` 是不是很高兴呢?但是为了后续的一步开发,更好的调试环境也是必不可少的,第一章的练习要求大家实现更加炫酷的彩色log。
+
+.. 详细的原理不多说,感兴趣的同学可以参考 `ANSI转义序列 `_ ,现在执行如下这条命令试试
+
+.. .. code-block:: console
+
+.. $ echo -e "\x1b[31mhello world\x1b[0m"
+
+.. 如果你明白了我们是如何利用串口实现输出,那么要实现彩色输出就十分容易了,只需要用需要输出的字符串替换上一条命令中的 ``hello world``,用期望颜色替换 ``31(代表红色)`` 即可。
+
+.. .. warning::
+
+.. 以下内容仅为推荐实现,不是练习要求,有时间和兴趣的同学可以尝试。
+
+.. 我们推荐实现如下几个等级的输出,输出优先级依次降低:
+
+.. .. list-table:: log 等级推荐
+.. :header-rows: 1
+.. :align: center
+
+.. * - 名称
+.. - 颜色
+.. - 用途
+.. * - ERROR
+.. - 红色(31)
+.. - 表示发生严重错误,很可能或者已经导致程序崩溃
+.. * - WARN
+.. - 黄色(93)
+.. - 表示发生不常见情况,但是并不一定导致系统错误
+.. * - INFO
+.. - 蓝色(34)
+.. - 比较中庸的选项,输出比较重要的信息,比较常用
+.. * - DEBUG
+.. - 绿色(32)
+.. - 输出信息较多,在 debug 时使用
+.. * - TRACE
+.. - 灰色(90)
+.. - 最详细的输出,跟踪了每一步关键路径的执行
+
+.. 我们可以输出比设定输出等级以及更高输出等级的信息,如设置 ``LOG = INFO``,则输出 ``ERROR``、``WARN``、``INFO`` 等级的信息。简单 demo 如下,输出等级为 INFO:
+
+.. .. image:: color-demo.png
+
+.. 为了方便使用彩色输出,我们要求同学们实现彩色输出的宏或者函数,用以代替 print 完成输出内核信息的功能,它们有着和 prinf 十分相似的使用格式,要求支持可变参数解析,形如:
+
+.. .. code-block:: rust
+
+.. // 这段代码输出了 os 内存空间布局,这到这些信息对于编写 os 十分重要
+
+.. info!(".text [{:#x}, {:#x})", s_text as usize, e_text as usize);
+.. debug!(".rodata [{:#x}, {:#x})", s_rodata as usize, e_rodata as usize);
+.. error!(".data [{:#x}, {:#x})", s_data as usize, e_data as usize);
+
+.. .. code-block:: c
+
+.. info("load range : [%d, %d] start = %d\n", s, e, start);
+
+.. 在以后,我们还可以在 log 信息中增加线程、CPU等信息(只是一个推荐,不做要求),这些信息将极大的方便你的代码调试。
+
+
+实验要求
++++++++++++++++++++++++++++++++
+
+.. - 实现分支:ch1。
+.. - 完成实验指导书中的内容,在裸机上实现 ``hello world`` 输出。
+.. - 实现彩色输出宏(只要求可以彩色输出,不要求 log 等级控制,不要求多种颜色)。
+.. - 隐形要求:可以关闭内核所有输出。从 lab2 开始要求关闭内核所有输出(如果实现了 log 等级控制,那么这一点自然就实现了)。
+.. - 利用彩色输出宏输出 os 内存空间布局,即:输出 ``.text``、``.data``、``.rodata``、``.bss`` 各段位置,输出等级为 ``INFO``。
+
+实验检查
++++++++++++++++++++++++++++++++
+
+.. - 实验目录要求(Rust)
+
+.. .. code-block::
+
+.. ├── os(内核实现)
+.. │ ├── Cargo.toml(配置文件)
+.. │ ├── Makefile (要求 make run LOG=xxx 可以正确执行,可以不实现对 LOG 这一属性的支持,设置默认输出等级为 INFO)
+.. │ └── src(所有内核的源代码放在 os/src 目录下)
+.. │ ├── main.rs(内核主函数)
+.. │ └── ...
+.. ├── reports
+.. │ ├── lab1.md/pdf
+.. │ └── ...
+.. ├── README.md(其他必要的说明)
+.. ├── ...
+
+.. 报告命名 labx.md/pdf,统一放在 reports 目录下。每个实验新增一个报告,为了方便修改,检查报告是以最新分支的所有报告为准。
+
+.. - 检查
+
+.. .. code-block:: console
+
+.. $ cd os
+.. $ git checkout ch1
+.. $ make run LOG=INFO
+
+.. 可以正确执行(可以不支持LOG参数,只有要彩色输出就好),可以看到正确的内存布局输出,根据实现不同数值可能有差异,但应该位于 ``linker.ld`` 中指示 ``BASE_ADDRESS`` 后一段内存,输出之后关机。
+
+问答作业
+-------------------------------
+
+.. 1. 为了方便 os 处理,M态软件会将 S 态异常/中断委托给 S 态软件,请指出有哪些寄存器记录了委托信息,rustsbi 委托了哪些异常/中断?(也可以直接给出寄存器的值)
+
+.. 2. 请学习 gdb 调试工具的使用(这对后续调试很重要),并通过 gdb 简单跟踪从机器加电到跳转到 0x80200000 的简单过程。只需要描述重要的跳转即可,只需要描述在 qemu 上的情况。
+
+.. 3. tips:
+
+.. - 事实上进入 rustsbi 之后就不需要使用 gdb 调试了。可以直接阅读代码。`rustsbi起始代码 `_ 。
+.. - 可以使用示例代码 Makefile 中的 ``make debug`` 指令。
+.. - 一些可能用到的 gdb 指令:
+.. - ``x/10i 0x80000000`` : 显示 0x80000000 处的10条汇编指令。
+.. - ``x/10i $pc`` : 显示即将执行的10条汇编指令。
+.. - ``x/10xw 0x80000000`` : 显示 0x80000000 处的10条数据,格式为16进制32bit。
+.. - ``info register``: 显示当前所有寄存器信息。
+.. - ``info r t0``: 显示 t0 寄存器的值。
+.. - ``break funcname``: 在目标函数第一条指令处设置断点。
+.. - ``break *0x80200000``: 在 0x80200000 出设置断点。
+.. - ``continue``: 执行直到碰到断点。
+.. - ``si``: 单步执行一条汇编指令。
+
+报告要求
+-------------------------------
+
+- 简单总结你实现的功能(200字以内,不要贴代码)。
+- 完成问答题。
+- (optional) 你对本次实验设计及难度/工作量的看法,以及有哪些需要改进的地方,欢迎畅所欲言。
\ No newline at end of file
diff --git a/guide/source/chapter1/app-software-stack.png b/guide/source/chapter1/app-software-stack.png
new file mode 100644
index 0000000..021f145
Binary files /dev/null and b/guide/source/chapter1/app-software-stack.png differ
diff --git a/guide/source/chapter1/chap1-intro.png b/guide/source/chapter1/chap1-intro.png
new file mode 100644
index 0000000..2064f32
Binary files /dev/null and b/guide/source/chapter1/chap1-intro.png differ
diff --git a/guide/source/chapter1/color-demo.png b/guide/source/chapter1/color-demo.png
new file mode 100644
index 0000000..c07f516
Binary files /dev/null and b/guide/source/chapter1/color-demo.png differ
diff --git a/guide/source/chapter1/index.rst b/guide/source/chapter1/index.rst
new file mode 100644
index 0000000..356ff69
--- /dev/null
+++ b/guide/source/chapter1/index.rst
@@ -0,0 +1,13 @@
+.. _link-chapter1:
+
+第一章:应用程序与基本执行环境
+==============================================
+
+.. toctree::
+ :maxdepth: 4
+
+ 0intro
+ 1app-ee-platform
+ 2remove-std
+ 3mini-rt-usrland
+ 4mini-rt-baremetal
diff --git a/guide/source/chapter2/0intro.rst b/guide/source/chapter2/0intro.rst
new file mode 100644
index 0000000..610d70a
--- /dev/null
+++ b/guide/source/chapter2/0intro.rst
@@ -0,0 +1,149 @@
+引言
+================================
+
+本章导读
+---------------------------------
+
+
+**批处理系统** (Batch System) 出现于计算资源匮乏的年代,其核心思想是:
+将多个程序打包到一起输入计算机;当一个程序运行结束后,计算机会 *自动* 执行下一个程序。
+
+应用程序难免会出错,如果一个程序的错误导致整个操作系统都无法运行,那就太糟糕了。
+*保护* 操作系统不受出错程序破坏的机制被称为 **特权级** (Privilege) 机制,
+它实现了用户态和内核态的隔离。
+
+本章在上一章的基础上,让我们的 OS 内核能以批处理的形式一次运行多个应用程序,同时利用特权级机制,
+令 OS 不因出错的用户态程序而崩溃。
+
+本章首先为批处理操作系统设计用户程序,再阐述如何将这些应用程序链接到内核中,最后介绍如何利用特权级机制处理 Trap.
+
+实践体验
+---------------------------
+
+本章我们引入了用户程序。为了将内核与应用解耦,我们将二者分成了两个仓库,分别是存放内核程序的 ``rCore-Tutorial-Code-20xxx`` (下称代码仓库,最后几位 x 表示学期)与存放用户程序的 ``rCore-Tutorial-Test-20xxx`` (下称测例仓库)。 你首先需要进入代码仓库文件夹并 clone 用户程序仓库(如果已经执行过该步骤则不需要再重复执行):
+
+.. code-block:: console
+
+ $ git clone https://github.com/LearningOS/rCore-Tutorial-Code-2022S.git
+ $ cd rCore-Tutorial-Code-2022S
+ $ git checkout ch2
+ $ git clone https://github.com/LearningOS/rCore-Tutorial-Test-2022S.git user
+
+上面的指令会将测例仓库克隆到代码仓库下并命名为 ``user`` ,注意 ``/user`` 在代码仓库的 ``.gitignore`` 文件中,因此不会出现 ``.git`` 文件夹嵌套的问题,并且你在代码仓库进行 checkout 操作时也不会影响测例仓库的内容。
+
+在 qemu 模拟器上运行本章代码:
+
+.. code-block:: console
+
+ $ cd os
+ $ make run LOG=INFO
+
+批处理系统自动加载并运行了所有的用户程序,尽管某些程序出错了:
+
+.. code-block::
+
+ [rustsbi] RustSBI version 0.2.0-alpha.4
+ .______ __ __ _______.___________. _______..______ __
+ | _ \ | | | | / | | / || _ \ | |
+ | |_) | | | | | | (----`---| |----`| (----`| |_) || |
+ | / | | | | \ \ | | \ \ | _ < | |
+ | |\ \----.| `--' |.----) | | | .----) | | |_) || |
+ | _| `._____| \______/ |_______/ |__| |_______/ |______/ |__|
+
+ [rustsbi] Implementation: RustSBI-QEMU Version 0.0.1
+ [rustsbi-dtb] Hart count: cluster0 with 1 cores
+ [rustsbi] misa: RV64ACDFIMSU
+ [rustsbi] mideleg: ssoft, stimer, sext (0x222)
+ [rustsbi] medeleg: ima, ia, bkpt, la, sa, uecall, ipage, lpage, spage (0xb1ab)
+ [rustsbi] pmp0: 0x80000000 ..= 0x800fffff (rwx)
+ [rustsbi] pmp1: 0x80000000 ..= 0x807fffff (rwx)
+ [rustsbi] pmp2: 0x0 ..= 0xffffffffffffff (---)
+ [rustsbi] enter supervisor 0x80200000
+ [kernel] Hello, world!
+ [ INFO] [kernel] num_app = 6
+ [ INFO] [kernel] app_0 [0x8020b040, 0x8020f868)
+ [ INFO] [kernel] app_1 [0x8020f868, 0x80214090)
+ [ INFO] [kernel] app_2 [0x80214090, 0x80218988)
+ [ INFO] [kernel] app_3 [0x80218988, 0x8021d160)
+ [ INFO] [kernel] app_4 [0x8021d160, 0x80221a68)
+ [ INFO] [kernel] app_5 [0x80221a68, 0x80226538)
+ [ INFO] [kernel] Loading app_0
+ [ERROR] [kernel] PageFault in application, core dumped.
+ [ INFO] [kernel] Loading app_1
+ [ERROR] [kernel] IllegalInstruction in application, core dumped.
+ [ INFO] [kernel] Loading app_2
+ [ERROR] [kernel] IllegalInstruction in application, core dumped.
+ [ INFO] [kernel] Loading app_3
+ [ INFO] [kernel] Application exited with code 1234
+ [ INFO] [kernel] Loading app_4
+ Hello, world from user mode program!
+ [ INFO] [kernel] Application exited with code 0
+ [ INFO] [kernel] Loading app_5
+ 3^10000=5079(MOD 10007)
+ 3^20000=8202(MOD 10007)
+ 3^30000=8824(MOD 10007)
+ 3^40000=5750(MOD 10007)
+ 3^50000=3824(MOD 10007)
+ 3^60000=8516(MOD 10007)
+ 3^70000=2510(MOD 10007)
+ 3^80000=9379(MOD 10007)
+ 3^90000=2621(MOD 10007)
+ 3^100000=2749(MOD 10007)
+ Test power OK!
+ [ INFO] [kernel] Application exited with code 0
+ Panicked at src/batch.rs:68 All applications completed!
+
+本章代码树
+-------------------------------------------------
+
+.. code-block::
+
+ ── os
+ │ ├── Cargo.toml
+ │ ├── Makefile (修改:构建内核之前先构建应用)
+ │ ├── build.rs (新增:生成 link_app.S 将应用作为一个数据段链接到内核)
+ │ └── src
+ │ ├── batch.rs(新增:实现了一个简单的批处理系统)
+ │ ├── console.rs
+ │ ├── entry.asm
+ │ ├── lang_items.rs
+ │ ├── link_app.S(构建产物,由 os/build.rs 输出)
+ │ ├── linker.ld
+ │ ├── logging.rs
+ │ ├── main.rs(修改:主函数中需要初始化 Trap 处理并加载和执行应用)
+ │ ├── sbi.rs
+ │ ├── sync(新增:包装了RefCell,暂时不用关心)
+ │ │ ├── mod.rs
+ │ │ └── up.rs
+ │ ├── syscall(新增:系统调用子模块 syscall)
+ │ │ ├── fs.rs(包含文件 I/O 相关的 syscall)
+ │ │ ├── mod.rs(提供 syscall 方法根据 syscall ID 进行分发处理)
+ │ │ └── process.rs(包含任务处理相关的 syscall)
+ │ └── trap(新增:Trap 相关子模块 trap)
+ │ ├── context.rs(包含 Trap 上下文 TrapContext)
+ │ ├── mod.rs(包含 Trap 处理入口 trap_handler)
+ │ └── trap.S(包含 Trap 上下文保存与恢复的汇编代码)
+ └── user(新增:应用测例保存在 user 目录下)
+ ├── Cargo.toml
+ ├── Makefile
+ └── src
+ ├── bin(基于用户库 user_lib 开发的应用,每个应用放在一个源文件中)
+ │ ├── ...
+ ├── console.rs
+ ├── lang_items.rs
+ ├── lib.rs(用户库 user_lib)
+ ├── linker.ld(应用的链接脚本)
+ └── syscall.rs(包含 syscall 方法生成实际用于系统调用的汇编指令,
+ 各个具体的 syscall 都是通过 syscall 来实现的)
+
+ cloc os
+ -------------------------------------------------------------------------------
+ Language files blank comment code
+ -------------------------------------------------------------------------------
+ Rust 14 62 21 435
+ Assembly 3 9 16 106
+ make 1 12 4 36
+ TOML 1 2 1 9
+ -------------------------------------------------------------------------------
+ SUM: 19 85 42 586
+ -------------------------------------------------------------------------------
diff --git a/guide/source/chapter2/2application.rst b/guide/source/chapter2/2application.rst
new file mode 100644
index 0000000..99dbed7
--- /dev/null
+++ b/guide/source/chapter2/2application.rst
@@ -0,0 +1,214 @@
+实现应用程序
+===========================
+
+.. toctree::
+ :hidden:
+ :maxdepth: 5
+
+.. note::
+
+ 拓展阅读:`RISC-V 特权级机制 `_
+
+
+应用程序设计
+-----------------------------
+
+.. attention::
+
+ 用户库看起来很复杂,它预留了直到 ch7 内核才能实现的系统调用接口,console 模块还实现了输出缓存区。它们不是为本章准备的,你只需关注本节提到的部分即可。
+
+
+应用程序、用户库(包括入口函数、初始化函数、I/O函数和系统调用接口等多个rs文件组成)放在项目根目录的 ``user`` 目录下:
+
+- user/src/bin/*.rs:各个应用程序
+- user/src/*.rs:用户库(包括入口函数、初始化函数、I/O函数和系统调用接口等)
+- user/src/linker.ld:应用程序的内存布局说明
+
+项目结构
+^^^^^^^^^^^^^^^^^^^^^^
+
+``user/src/bin`` 里面有多个文件,其中三个是:
+
+- ``hello_world``:在屏幕上打印一行 ``Hello, world!``
+- ``bad_address``:访问一个非法的物理地址,测试批处理系统是否会被该错误影响
+- ``power``:不断在计算操作和打印字符串操作之间切换
+
+批处理系统会按照文件名顺序加载并运行它们。
+
+每个应用程序的实现都在对应的单个文件中。打开 ``hello_world.rs``,能看到一个 ``main`` 函数,还有外部库引用:
+
+.. code-block:: rust
+
+ #[macro_use]
+ extern crate user_lib;
+
+这个外部库其实就是 ``user`` 目录下的 ``lib.rs`` 以及它引用的若干子模块。
+在 ``user/Cargo.toml`` 中我们对于库的名字进行了设置: ``name = "user_lib"`` 。
+它作为 ``bin`` 目录下的源程序所依赖的用户库,等价于其他编程语言提供的标准库。
+
+在 ``lib.rs`` 中我们定义了用户库的入口点 ``_start`` :
+
+.. code-block:: rust
+ :linenos:
+
+ #[no_mangle]
+ #[link_section = ".text.entry"]
+ pub extern "C" fn _start() -> ! {
+ clear_bss();
+ exit(main());
+ }
+
+第 2 行使用 ``link_section`` 宏将 ``_start`` 函数编译后的汇编代码放在名为 ``.text.entry`` 的代码段中,
+方便用户库链接脚本将它作为用户程序的入口。
+
+而从第 4 行开始,我们手动清零 ``.bss`` 段,然后调用 ``main`` 函数得到一个类型为 ``i32`` 的返回值,
+最后,调用用户库提供的 ``exit`` 接口退出,并将返回值告知批处理系统。
+
+我们在 ``lib.rs`` 中看到了另一个 ``main`` :
+
+.. code-block:: rust
+ :linenos:
+
+ #![feature(linkage)] // 启用弱链接特性
+
+ #[linkage = "weak"]
+ #[no_mangle]
+ fn main() -> i32 {
+ panic!("Cannot find main!");
+ }
+
+我们使用 Rust 宏将其标志为弱链接。这样在最后链接的时候,
+虽然 ``lib.rs`` 和 ``bin`` 目录下的某个应用程序中都有 ``main`` 符号,
+但由于 ``lib.rs`` 中的 ``main`` 符号是弱链接,
+链接器会使用 ``bin`` 目录下的函数作为 ``main`` 。
+如果在 ``bin`` 目录下找不到任何 ``main`` ,那么编译也能通过,但会在运行时报错。
+
+内存布局
+^^^^^^^^^^^^^^^^^^^^^^
+
+我们使用链接脚本 ``user/src/linker.ld`` 规定用户程序的内存布局:
+
+- 将程序的起始物理地址调整为 ``0x80400000`` ,三个应用程序都会被加载到这个物理地址上运行;
+- 将 ``_start`` 所在的 ``.text.entry`` 放在整个程序的开头 ``0x80400000``;
+ 批处理系统在加载应用后,跳转到 ``0x80400000``,就进入了用户库的 ``_start`` 函数;
+- 提供了最终生成可执行文件的 ``.bss`` 段的起始和终止地址,方便 ``clear_bss`` 函数使用。
+
+其余的部分和第一章基本相同。
+
+系统调用
+^^^^^^^^^^^^^^^^^^^^^^
+
+在子模块 ``syscall`` 中我们来通过 ``ecall`` 调用批处理系统提供的接口,
+由于应用程序运行在用户态(即 U 模式), ``ecall`` 指令会触发名为 ``Environment call from U-mode`` 的异常,
+并 Trap 进入 S 模式执行批处理系统针对这个异常特别提供的服务程序。
+这个接口被称为 ABI 或者系统调用。
+现在我们不关心 S 态的批处理系统如何提供应用程序所需的功能,只考虑如何使用它。
+
+在本章中,应用程序和批处理系统约定如下两个系统调用:
+
+.. code-block:: rust
+ :caption: 第二章新增系统调用
+
+ /// 功能:将内存中缓冲区中的数据写入文件。
+ /// 参数:`fd` 表示待写入文件的文件描述符;
+ /// `buf` 表示内存中缓冲区的起始地址;
+ /// `len` 表示内存中缓冲区的长度。
+ /// 返回值:返回成功写入的长度。
+ /// syscall ID:64
+ fn sys_write(fd: usize, buf: *const u8, len: usize) -> isize;
+
+ /// 功能:退出应用程序并将返回值告知批处理系统。
+ /// 参数:`xstate` 表示应用程序的返回值。
+ /// 返回值:该系统调用不应该返回。
+ /// syscall ID:93
+ fn sys_exit(xstate: usize) -> !;
+
+实际调用时,我们要按照 RISC-V 调用规范,在合适的寄存器中放置参数,
+然后执行 ``ecall`` 指令触发 Trap。当 Trap 结束,回到 U 模式后,
+用户程序会从 ``ecall`` 的下一条指令继续执行,同时在合适的寄存器中读取返回值。
+
+.. note::
+
+ RISC-V 寄存器编号从 ``0~31`` ,表示为 ``x0~x31`` 。 其中:
+ - ``x10~x17`` : 对应 ``a0~a7``
+ - ``x1`` :对应 ``ra``
+
+约定寄存器 ``a0~a6`` 保存系统调用的参数, ``a0`` 保存系统调用的返回值,
+寄存器 ``a7`` 用来传递 syscall ID。
+这超出了 Rust 语言的表达能力,我们需要内嵌汇编来完成参数/返回值绑定和 ``ecall`` 指令的插入:
+
+.. code-block:: rust
+ :linenos:
+
+ // user/src/syscall.rs
+
+ fn syscall(id: usize, args: [usize; 3]) -> isize {
+ let mut ret: isize;
+ unsafe {
+ core::arch::asm!(
+ "ecall",
+ inlateout("x10") args[0] => ret,
+ in("x11") args[1],
+ in("x12") args[2],
+ in("x17") id
+ );
+ }
+ ret
+ }
+
+第 3 行,我们将所有的系统调用都封装成 ``syscall`` 函数,可以看到它支持传入 syscall ID 和 3 个参数。
+
+第 6 行开始,我们使用 Rust 提供的 ``asm!`` 宏在代码中内嵌汇编。
+Rust 编译器无法判定汇编代码的安全性,所以我们需要将其包裹在 unsafe 块中。
+
+简而言之,这条汇编代码的执行结果是以寄存器 ``a0~a2`` 来保存系统调用的参数,以及寄存器 ``a7`` 保存 syscall ID,
+返回值通过寄存器 ``a0`` 传递给局部变量 ``ret``。
+
+这段汇编代码与第一章中出现过的内嵌汇编很像,读者可以查看 ``os/src/sbi.rs`` 。
+
+.. note::
+
+ 可以查看 `Inline assembly `_ 了解 ``asm`` 宏。
+
+于是 ``sys_write`` 和 ``sys_exit`` 只需将 ``syscall`` 进行包装:
+
+.. code-block:: rust
+ :linenos:
+
+ // user/src/syscall.rs
+
+ const SYSCALL_WRITE: usize = 64;
+ const SYSCALL_EXIT: usize = 93;
+
+ pub fn sys_write(fd: usize, buffer: &[u8]) -> isize {
+ syscall(SYSCALL_WRITE, [fd, buffer.as_ptr() as usize, buffer.len()])
+ }
+
+ pub fn sys_exit(xstate: i32) -> isize {
+ syscall(SYSCALL_EXIT, [xstate as usize, 0, 0])
+ }
+
+我们将上述两个系统调用在用户库 ``user_lib`` 中进一步封装,像标准库一样:
+
+.. code-block:: rust
+ :linenos:
+
+ // user/src/lib.rs
+ use syscall::*;
+
+ pub fn write(fd: usize, buf: &[u8]) -> isize { sys_write(fd, buf) }
+ pub fn exit(exit_code: i32) -> isize { sys_exit(exit_code) }
+
+在 ``console`` 子模块中,借助 ``write``,我们为应用程序实现了 ``println!`` 宏。
+传入到 ``write`` 的 ``fd`` 参数设置为 1,代表标准输出 STDOUT,暂时不用考虑其他的 ``fd`` 选取情况。
+
+
+编译生成应用程序二进制码
+-------------------------------
+
+简要介绍一下应用程序的构建,在 ``user`` 目录下 ``make build``:
+
+1. 对于 ``src/bin`` 下的每个应用程序,
+ 在 ``target/riscv64gc-unknown-none-elf/release`` 目录下生成一个同名的 ELF 可执行文件;
+2. 使用 objcopy 二进制工具删除所有 ELF header 和符号,得到 ``.bin`` 后缀的纯二进制镜像文件。
+ 它们将被链接进内核,并由内核在合适的时机加载到内存。
diff --git a/guide/source/chapter2/3batch-system.rst b/guide/source/chapter2/3batch-system.rst
new file mode 100644
index 0000000..ce2786b
--- /dev/null
+++ b/guide/source/chapter2/3batch-system.rst
@@ -0,0 +1,168 @@
+
+.. _term-batchos:
+
+实现批处理操作系统
+==============================
+
+.. toctree::
+ :hidden:
+ :maxdepth: 5
+
+将应用程序链接到内核
+--------------------------------------------
+
+在本章中,我们要把应用程序的二进制镜像文件作为数据段链接到内核里,
+内核需要知道应用程序的数量和它们的位置。
+
+在 ``os/src/main.rs`` 中能够找到这样一行:
+
+.. code-block:: rust
+
+ core::arch::global_asm!(include_str!("link_app.S"));
+
+这里我们引入了一段汇编代码 ``link_app.S`` ,它是在 ``make run`` 构建操作系统时自动生成的,里面的内容大致如下:
+
+.. code-block:: asm
+ :linenos:
+
+ # os/src/link_app.S
+
+ .align 3
+ .section .data
+ .global _num_app
+ _num_app:
+ .quad 3
+ .quad app_0_start
+ .quad app_1_start
+ .quad app_2_start
+ .quad app_2_end
+
+ .section .data
+ .global app_0_start
+ .global app_0_end
+ app_0_start:
+ .incbin "../user/target/riscv64gc-unknown-none-elf/release/hello_world.bin"
+ app_0_end:
+
+ .section .data
+ .global app_1_start
+ .global app_1_end
+ app_1_start:
+ .incbin "../user/target/riscv64gc-unknown-none-elf/release/bad_address.bin"
+ app_1_end:
+
+ .section .data
+ .global app_2_start
+ .global app_2_end
+ app_2_start:
+ .incbin "../user/target/riscv64gc-unknown-none-elf/release/power.bin"
+ app_2_end:
+
+第 13 行开始的三个数据段分别插入了三个应用程序的二进制镜像,
+并且各自有一对全局符号 ``app_*_start, app_*_end`` 指示它们的开始和结束位置。
+而第 3 行开始的另一个数据段相当于一个 64 位整数数组。
+数组中的第一个元素表示应用程序的数量,后面则按照顺序放置每个应用程序的起始地址,
+最后一个元素放置最后一个应用程序的结束位置。这样数组中相邻两个元素记录了每个应用程序的始末位置,
+这个数组所在的位置由全局符号 ``_num_app`` 所指示。
+
+这个文件是在 ``cargo build`` 时,由脚本 ``os/build.rs`` 控制生成的。
+
+找到并加载应用程序二进制码
+-----------------------------------------------
+
+我们在 ``os`` 的 ``batch`` 子模块中实现一个应用管理器 ``AppManager`` ,结构体定义如下:
+
+.. code-block:: rust
+
+ struct AppManager {
+ num_app: usize,
+ current_app: usize,
+ app_start: [usize; MAX_APP_NUM + 1],
+ }
+
+初始化 ``AppManager`` 的全局实例:
+
+.. code-block:: rust
+
+ lazy_static! {
+ static ref APP_MANAGER: UPSafeCell = unsafe {
+ UPSafeCell::new({
+ extern "C" {
+ fn _num_app();
+ }
+ let num_app_ptr = _num_app as usize as *const usize;
+ let num_app = num_app_ptr.read_volatile();
+ let mut app_start: [usize; MAX_APP_NUM + 1] = [0; MAX_APP_NUM + 1];
+ let app_start_raw: &[usize] =
+ core::slice::from_raw_parts(num_app_ptr.add(1), num_app + 1);
+ app_start[..=num_app].copy_from_slice(app_start_raw);
+ AppManager {
+ num_app,
+ current_app: 0,
+ app_start,
+ }
+ })
+ };
+ }
+
+初始化的逻辑很简单,就是找到 ``link_app.S`` 中提供的符号 ``_num_app`` ,并从这里开始解析出应用数量以及各个应用的开头地址。
+用容器 ``UPSafeCell`` 包裹 ``AppManager`` 是为了防止全局对象 ``APP_MANAGER`` 被重复获取。
+
+.. note::
+
+ ``UPSafeCell`` 实现在 ``sync`` 模块中,调用 ``exclusive_access`` 方法能获取其内部对象的可变引用,
+ 如果程序运行中同时存在多个这样的引用,会触发 ``already borrowed: BorrowMutError``。
+
+ ``UPSafeCell`` 既提供了内部可变性,又在单核情境下防止了内部对象被重复借用,我们将在后文中多次见到它。
+
+这里使用了外部库 ``lazy_static`` 提供的 ``lazy_static!`` 宏。
+
+``lazy_static!`` 宏提供了全局变量的运行时初始化功能。一般情况下,全局变量必须在编译期设置初始值,
+但是有些全局变量的初始化依赖于运行期间才能得到的数据。
+如这里我们借助 ``lazy_static!`` 声明了一个 ``AppManager`` 结构的名为 ``APP_MANAGER`` 的全局实例,
+只有在它第一次被使用到的时候才会进行实际的初始化工作。
+
+``AppManager`` 的方法中, ``print_app_info/get_current_app/move_to_next_app`` 都相当简单直接,需要说明的是 ``load_app``:
+
+.. code-block:: rust
+ :linenos:
+
+ unsafe fn load_app(&self, app_id: usize) {
+ if app_id >= self.num_app {
+ panic!("All applications completed!");
+ }
+ info!("[kernel] Loading app_{}", app_id);
+ // clear icache
+ core::arch::asm!("fence.i");
+ // clear app area
+ core::slice::from_raw_parts_mut(APP_BASE_ADDRESS as *mut u8, APP_SIZE_LIMIT).fill(0);
+ let app_src = core::slice::from_raw_parts(
+ self.app_start[app_id] as *const u8,
+ self.app_start[app_id + 1] - self.app_start[app_id],
+ );
+ let app_dst = core::slice::from_raw_parts_mut(APP_BASE_ADDRESS as *mut u8, app_src.len());
+ app_dst.copy_from_slice(app_src);
+ }
+
+这个方法负责将参数 ``app_id`` 对应的应用程序的二进制镜像加载到物理内存以 ``0x80400000`` 起始的位置,
+这个位置是批处理操作系统和应用程序之间约定的常数地址。
+我们将从这里开始的一块内存清空,然后找到待加载应用二进制镜像的位置,并将它复制到正确的位置。
+
+清空内存前,我们插入了一条奇怪的汇编指令 ``fence.i`` ,它是用来清理 i-cache 的。
+我们知道, 缓存又分成 **数据缓存** (d-cache) 和 **指令缓存** (i-cache) 两部分,分别在 CPU 访存和取指的时候使用。
+通常情况下, CPU 会认为程序的代码段不会发生变化,因此 i-cache 是一种只读缓存。
+但在这里,我们会修改会被 CPU 取指的内存区域,使得 i-cache 中含有与内存不一致的内容,
+必须使用 ``fence.i`` 指令手动清空 i-cache ,让里面所有的内容全部失效,
+才能够保证程序执行正确性。
+
+.. warning::
+
+ **模拟器与真机的不同之处**
+
+ 在 Qemu 模拟器上,即使不加刷新 i-cache 的指令,大概率也能正常运行,但在物理计算机上不是这样。
+
+``batch`` 子模块对外暴露出如下接口:
+
+- ``init`` :调用 ``print_app_info`` 的时第一次用到了全局变量 ``APP_MANAGER`` ,它在这时完成初始化;
+- ``run_next_app`` :批处理操作系统的核心操作,即加载并运行下一个应用程序。
+ 批处理操作系统完成初始化,或者应用程序运行结束/出错后会调用该函数。下节再介绍其具体实现。
\ No newline at end of file
diff --git a/guide/source/chapter2/4trap-handling.rst b/guide/source/chapter2/4trap-handling.rst
new file mode 100644
index 0000000..89de48b
--- /dev/null
+++ b/guide/source/chapter2/4trap-handling.rst
@@ -0,0 +1,519 @@
+.. _term-trap-handle:
+
+实现特权级的切换
+===========================
+
+.. toctree::
+ :hidden:
+ :maxdepth: 5
+
+RISC-V特权级切换
+---------------------------------------
+
+特权级切换的起因
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+批处理操作系统为了建立好应用程序的执行环境,需要在执行应用程序前进行一些初始化工作,
+并监控应用程序的执行,具体体现在:
+
+- 启动应用程序时,需要初始化应用程序的用户态上下文,并能切换到用户态执行应用程序;
+- 应用程序发起系统调用后,需要切换到批处理操作系统中进行处理;
+- 应用程序执行出错时,批处理操作系统要杀死该应用并加载运行下一个应用;
+- 应用程序执行结束时,批处理操作系统要加载运行下一个应用。
+
+这些处理都涉及到特权级切换,因此都需要硬件和操作系统协同提供的特权级切换机制。
+
+
+特权级切换相关的控制状态寄存器
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+本章中我们仅考虑当 CPU 在 U 特权级运行用户程序的时候触发 Trap,
+并切换到 S 特权级的批处理操作系统进行处理。
+
+.. list-table:: 进入 S 特权级 Trap 的相关 CSR
+ :header-rows: 1
+ :align: center
+ :widths: 30 100
+
+ * - CSR 名
+ - 该 CSR 与 Trap 相关的功能
+ * - sstatus
+ - ``SPP`` 等字段给出 Trap 发生之前 CPU 处在哪个特权级(S/U)等信息
+ * - sepc
+ - 当 Trap 是一个异常的时候,记录 Trap 发生之前执行的最后一条指令的地址
+ * - scause
+ - 描述 Trap 的原因
+ * - stval
+ - 给出 Trap 附加信息
+ * - stvec
+ - 控制 Trap 处理代码的入口地址
+
+特权级切换的具体过程一部分由硬件直接完成,另一部分则需要由操作系统来实现。
+
+.. _trap-hw-mechanism:
+
+特权级切换的硬件控制机制
+-------------------------------------
+
+当 CPU 执行完一条指令并准备从用户特权级 陷入( ``Trap`` )到 S 特权级的时候,硬件会自动完成如下这些事情:
+
+- ``sstatus`` 的 ``SPP`` 字段会被修改为 CPU 当前的特权级(U/S)。
+- ``sepc`` 会被修改为 Trap 处理完成后默认会执行的下一条指令的地址。
+- ``scause/stval`` 分别会被修改成这次 Trap 的原因以及相关的附加信息。
+- CPU 会跳转到 ``stvec`` 所设置的 Trap 处理入口地址,并将当前特权级设置为 S ,然后从Trap 处理入口地址处开始执行。
+
+.. note::
+
+ **stvec 相关细节**
+
+ 在 RV64 中, ``stvec`` 是一个 64 位的 CSR,在中断使能的情况下,保存了中断处理的入口地址。它有两个字段:
+
+ - MODE 位于 [1:0],长度为 2 bits;
+ - BASE 位于 [63:2],长度为 62 bits。
+
+ 当 MODE 字段为 0 的时候, ``stvec`` 被设置为 Direct 模式,此时进入 S 模式的 Trap 无论原因如何,处理 Trap 的入口地址都是 ``BASE<<2``
+ , CPU 会跳转到这个地方进行异常处理。本书中我们只会将 ``stvec`` 设置为 Direct 模式。而 ``stvec`` 还可以被设置为 Vectored 模式,
+
+而当 CPU 完成 Trap 处理准备返回的时候,需要通过一条 S 特权级的特权指令 ``sret`` 来完成,这一条指令具体完成以下功能:
+
+- CPU 会将当前的特权级按照 ``sstatus`` 的 ``SPP`` 字段设置为 U 或者 S ;
+- CPU 会跳转到 ``sepc`` 寄存器指向的那条指令,然后继续执行。
+
+用户栈与内核栈
+--------------------------------
+
+在 Trap 触发的一瞬间, CPU 会切换到 S 特权级并跳转到 ``stvec`` 所指示的位置。
+但是在正式进入 S 特权级的 Trap 处理之前,我们必须保存原控制流的寄存器状态,这一般通过栈来完成。
+但我们需要用专门为操作系统准备的内核栈,而不是应用程序运行时用到的用户栈。
+
+我们声明两个类型 ``KernelStack`` 和 ``UserStack`` 分别表示用户栈和内核栈,它们都只是字节数组的简单包装:
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/batch.rs
+
+ #[repr(align(4096))]
+ struct KernelStack {
+ data: [u8; KERNEL_STACK_SIZE],
+ }
+
+ #[repr(align(4096))]
+ struct UserStack {
+ data: [u8; USER_STACK_SIZE],
+ }
+
+ static KERNEL_STACK: KernelStack = KernelStack {
+ data: [0; KERNEL_STACK_SIZE],
+ };
+ static USER_STACK: UserStack = UserStack {
+ data: [0; USER_STACK_SIZE],
+ };
+
+
+两个栈以全局变量的形式实例化在批处理操作系统的 ``.bss`` 段中。
+
+我们为两个类型实现了 ``get_sp`` 方法来获取栈顶地址。由于在 RISC-V 中栈是向下增长的,
+我们只需返回包裹的数组的结尾地址,以用户栈类型 ``UserStack`` 为例:
+
+.. code-block:: rust
+ :linenos:
+
+ impl UserStack {
+ fn get_sp(&self) -> usize {
+ self.data.as_ptr() as usize + USER_STACK_SIZE
+ }
+ }
+
+换栈是非常简单的,只需将 ``sp`` 寄存器的值修改为 ``get_sp`` 的返回值即可。
+
+.. _term-trap-context:
+
+接下来是 Trap 上下文,即在 Trap 发生时需要保存的物理资源内容,定义如下:
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/trap/context.rs
+
+ #[repr(C)]
+ pub struct TrapContext {
+ pub x: [usize; 32],
+ pub sstatus: Sstatus,
+ pub sepc: usize,
+ }
+
+可以看到里面包含所有的通用寄存器 ``x0~x31`` ,还有 ``sstatus`` 和 ``sepc`` 。
+
+- 对于通用寄存器而言,两条控制流(应用程序控制流和内核控制流)运行在不同的特权级,所属的软件也可能由不同的编程语言编写,虽然在 Trap 控制流中只是会执行 Trap 处理
+ 相关的代码,但依然可能直接或间接调用很多模块,因此很难甚至不可能找出哪些寄存器无需保存。既然如此我们就只能全部保存了。但这里也有一些例外,
+ 如 ``x0`` 被硬编码为 0 ,它自然不会有变化;还有 ``tp(x4)`` 寄存器,除非我们手动出于一些特殊用途使用它,否则一般也不会被用到。虽然它们无需保存,
+ 但我们仍然在 ``TrapContext`` 中为它们预留空间,主要是为了后续的实现方便。
+- 对于 CSR 而言,我们知道进入 Trap 的时候,硬件会立即覆盖掉 ``scause/stval/sstatus/sepc`` 的全部或是其中一部分。``scause/stval``
+ 的情况是:它总是在 Trap 处理的第一时间就被使用或者是在其他地方保存下来了,因此它没有被修改并造成不良影响的风险。
+ 而对于 ``sstatus/sepc`` 而言,它们会在 Trap 处理的全程有意义(在 Trap 控制流最后 ``sret`` 的时候还用到了它们),而且确实会出现
+ Trap 嵌套的情况使得它们的值被覆盖掉。所以我们需要将它们也一起保存下来,并在 ``sret`` 之前恢复原样。
+
+
+Trap 管理
+-------------------------------
+
+Trap 上下文的保存与恢复
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+首先是具体实现 Trap 上下文保存和恢复的汇编代码。
+
+.. _trap-context-save-restore:
+
+
+在批处理操作系统初始化时,我们需要修改 ``stvec`` 寄存器来指向正确的 Trap 处理入口点。
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/trap/mod.rs
+
+ core::arch::global_asm!(include_str!("trap.S"));
+
+ pub fn init() {
+ extern "C" { fn __alltraps(); }
+ unsafe {
+ stvec::write(__alltraps as usize, TrapMode::Direct);
+ }
+ }
+
+这里我们引入了一个外部符号 ``__alltraps`` ,并将 ``stvec`` 设置为 Direct 模式指向它的地址。我们在 ``os/src/trap/trap.S``
+中实现 Trap 上下文保存/恢复的汇编代码,分别用外部符号 ``__alltraps`` 和 ``__restore`` 标记为函数,并通过 ``global_asm!`` 宏将 ``trap.S`` 这段汇编代码插入进来。
+
+Trap 处理的总体流程如下:首先通过 ``__alltraps`` 将 Trap 上下文保存在内核栈上,然后跳转到使用 Rust 编写的 ``trap_handler`` 函数
+完成 Trap 分发及处理。当 ``trap_handler`` 返回之后,使用 ``__restore`` 从保存在内核栈上的 Trap 上下文恢复寄存器。最后通过一条
+``sret`` 指令回到应用程序执行。
+
+首先是保存 Trap 上下文的 ``__alltraps`` 的实现:
+
+.. code-block:: riscv
+ :linenos:
+
+ # os/src/trap/trap.S
+
+ .macro SAVE_GP n
+ sd x\n, \n*8(sp)
+ .endm
+
+ .align 2
+ __alltraps:
+ csrrw sp, sscratch, sp
+ # now sp->kernel stack, sscratch->user stack
+ # allocate a TrapContext on kernel stack
+ addi sp, sp, -34*8
+ # save general-purpose registers
+ sd x1, 1*8(sp)
+ # skip sp(x2), we will save it later
+ sd x3, 3*8(sp)
+ # skip tp(x4), application does not use it
+ # save x5~x31
+ .set n, 5
+ .rept 27
+ SAVE_GP %n
+ .set n, n+1
+ .endr
+ # we can use t0/t1/t2 freely, because they were saved on kernel stack
+ csrr t0, sstatus
+ csrr t1, sepc
+ sd t0, 32*8(sp)
+ sd t1, 33*8(sp)
+ # read user stack from sscratch and save it on the kernel stack
+ csrr t2, sscratch
+ sd t2, 2*8(sp)
+ # set input argument of trap_handler(cx: &mut TrapContext)
+ mv a0, sp
+ call trap_handler
+
+- 第 7 行我们使用 ``.align`` 将 ``__alltraps`` 的地址 4 字节对齐,这是 RISC-V 特权级规范的要求;
+- 第 9 行的 ``csrrw`` 原型是 :math:`\text{csrrw rd, csr, rs}` 可以将 CSR 当前的值读到通用寄存器 :math:`\text{rd}` 中,然后将
+ 通用寄存器 :math:`\text{rs}` 的值写入该 CSR 。因此这里起到的是交换 sscratch 和 sp 的效果。在这一行之前 sp 指向用户栈, sscratch
+ 指向内核栈(原因稍后说明),现在 sp 指向内核栈, sscratch 指向用户栈。
+- 第 12 行,我们准备在内核栈上保存 Trap 上下文,于是预先分配 :math:`34\times 8` 字节的栈帧,这里改动的是 sp ,说明确实是在内核栈上。
+- 第 13~24 行,保存 Trap 上下文的通用寄存器 x0~x31,跳过 x0 和 tp(x4),原因之前已经说明。我们在这里也不保存 sp(x2),因为它在第 9 行
+ 后指向的是内核栈。用户栈的栈指针保存在 sscratch 中,必须通过 ``csrr`` 指令读到通用寄存器中后才能使用,因此我们先考虑保存其它通用寄存器,腾出空间。
+
+ 我们要基于 sp 来找到每个寄存器应该被保存到的正确的位置。实际上,在栈帧分配之后,我们可用于保存 Trap 上下文的地址区间为 :math:`[\text{sp},\text{sp}+8\times34)` ,
+ 按照 ``TrapContext`` 结构体的内存布局,基于内核栈的位置(sp所指地址)来从低地址到高地址分别按顺序放置 x0~x31这些通用寄存器,最后是 sstatus 和 sepc 。因此通用寄存器 xn
+ 应该被保存在地址区间 :math:`[\text{sp}+8n,\text{sp}+8(n+1))` 。
+
+ 为了简化代码,x5~x31 这 27 个通用寄存器我们通过类似循环的 ``.rept`` 每次使用 ``SAVE_GP`` 宏来保存,其实质是相同的。注意我们需要在
+ ``trap.S`` 开头加上 ``.altmacro`` 才能正常使用 ``.rept`` 命令。
+- 第 25~28 行,我们将 CSR sstatus 和 sepc 的值分别读到寄存器 t0 和 t1 中然后保存到内核栈对应的位置上。指令
+ :math:`\text{csrr rd, csr}` 的功能就是将 CSR 的值读到寄存器 :math:`\text{rd}` 中。这里我们不用担心 t0 和 t1 被覆盖,
+ 因为它们刚刚已经被保存了。
+- 第 30~31 行专门处理 sp 的问题。首先将 sscratch 的值读到寄存器 t2 并保存到内核栈上,注意: sscratch 的值是进入 Trap 之前的 sp 的值,指向
+ 用户栈。而现在的 sp 则指向内核栈。
+- 第 33 行令 :math:`\text{a}_0\leftarrow\text{sp}`,让寄存器 a0 指向内核栈的栈指针也就是我们刚刚保存的 Trap 上下文的地址,
+ 这是由于我们接下来要调用 ``trap_handler`` 进行 Trap 处理,它的第一个参数 ``cx`` 由调用规范要从 a0 中获取。而 Trap 处理函数
+ ``trap_handler`` 需要 Trap 上下文的原因在于:它需要知道其中某些寄存器的值,比如在系统调用的时候应用程序传过来的 syscall ID 和
+ 对应参数。我们不能直接使用这些寄存器现在的值,因为它们可能已经被修改了,因此要去内核栈上找已经被保存下来的值。
+
+
+.. _term-atomic-instruction:
+
+.. note::
+
+ **CSR 相关原子指令**
+
+ RISC-V 中读写 CSR 的指令是一类能不会被打断地完成多个读写操作的指令。这种不会被打断地完成多个操作的指令被称为 **原子指令** (Atomic Instruction)。这里的 **原子** 的含义是“不可分割的最小个体”,也就是说指令的多个操作要么都不完成,要么全部完成,而不会处于某种中间状态。
+
+当 ``trap_handler`` 返回之后会从调用 ``trap_handler`` 的下一条指令开始执行,也就是从栈上的 Trap 上下文恢复的 ``__restore`` :
+
+.. _code-restore:
+
+.. code-block:: riscv
+ :linenos:
+
+ .macro LOAD_GP n
+ ld x\n, \n*8(sp)
+ .endm
+
+ __restore:
+ # case1: start running app by __restore
+ # case2: back to U after handling trap
+ mv sp, a0
+ # now sp->kernel stack(after allocated), sscratch->user stack
+ # restore sstatus/sepc
+ ld t0, 32*8(sp)
+ ld t1, 33*8(sp)
+ ld t2, 2*8(sp)
+ csrw sstatus, t0
+ csrw sepc, t1
+ csrw sscratch, t2
+ # restore general-purpuse registers except sp/tp
+ ld x1, 1*8(sp)
+ ld x3, 3*8(sp)
+ .set n, 5
+ .rept 27
+ LOAD_GP %n
+ .set n, n+1
+ .endr
+ # release TrapContext on kernel stack
+ addi sp, sp, 34*8
+ # now sp->kernel stack, sscratch->user stack
+ csrrw sp, sscratch, sp
+ sret
+
+- 第 8 行比较奇怪,我们暂且不管,假设它从未发生,那么 sp 仍然指向内核栈的栈顶。
+- 第 11~24 行负责从内核栈顶的 Trap 上下文恢复通用寄存器和 CSR 。注意我们要先恢复 CSR 再恢复通用寄存器,这样我们使用的三个临时寄存器
+ 才能被正确恢复。
+- 在第 26 行之前,sp 指向保存了 Trap 上下文之后的内核栈栈顶, sscratch 指向用户栈栈顶。我们在第 26 行在内核栈上回收 Trap 上下文所
+ 占用的内存,回归进入 Trap 之前的内核栈栈顶。第 27 行,再次交换 sscratch 和 sp,现在 sp 重新指向用户栈栈顶,sscratch 也依然保存
+ 进入 Trap 之前的状态并指向内核栈栈顶。
+- 在应用程序控制流状态被还原之后,第 28 行我们使用 ``sret`` 指令回到 U 特权级继续运行应用程序控制流。
+
+Trap 分发与处理
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Trap 在使用 Rust 实现的 ``trap_handler`` 函数中完成分发和处理:
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/trap/mod.rs
+
+ #[no_mangle]
+ pub fn trap_handler(cx: &mut TrapContext) -> &mut TrapContext {
+ let scause = scause::read();
+ let stval = stval::read();
+ match scause.cause() {
+ Trap::Exception(Exception::UserEnvCall) => {
+ cx.sepc += 4;
+ cx.x[10] = syscall(cx.x[17], [cx.x[10], cx.x[11], cx.x[12]]) as usize;
+ }
+ Trap::Exception(Exception::StoreFault) |
+ Trap::Exception(Exception::StorePageFault) => {
+ println!("[kernel] PageFault in application, core dumped.");
+ run_next_app();
+ }
+ Trap::Exception(Exception::IllegalInstruction) => {
+ println!("[kernel] IllegalInstruction in application, core dumped.");
+ run_next_app();
+ }
+ _ => {
+ panic!("Unsupported trap {:?}, stval = {:#x}!", scause.cause(), stval);
+ }
+ }
+ cx
+ }
+
+- 第 4 行声明返回值为 ``&mut TrapContext`` 并在第 25 行实际将传入的 ``cx`` 原样返回,因此在 ``__restore`` 的时候 ``a0`` 寄存器在调用
+ ``trap_handler`` 前后并没有发生变化,仍然指向分配 Trap 上下文之后的内核栈栈顶,和此时 ``sp`` 的值相同,我们 :math:`\text{sp}\leftarrow\text{a}_0`
+ 并不会有问题;
+- 第 7 行根据 ``scause`` 寄存器所保存的 Trap 的原因进行分发处理。这里我们无需手动操作这些 CSR ,而是使用 Rust 第三方库 riscv 。
+- 第 8~11 行,发现触发 Trap 的原因是来自 U 特权级的 Environment Call,也就是系统调用。这里我们首先修改保存在内核栈上的 Trap 上下文里面
+ sepc,让其增加 4。这是因为我们知道这是一个由 ``ecall`` 指令触发的系统调用,在进入 Trap 的时候,硬件会将 sepc 设置为这条 ``ecall``
+ 指令所在的地址(因为它是进入 Trap 之前最后一条执行的指令)。而在 Trap 返回之后,我们希望应用程序控制流从 ``ecall`` 的下一条指令
+ 开始执行。因此我们只需修改 Trap 上下文里面的 sepc,让它增加 ``ecall`` 指令的码长,也即 4 字节。这样在 ``__restore`` 的时候 sepc
+ 在恢复之后就会指向 ``ecall`` 的下一条指令,并在 ``sret`` 之后从那里开始执行。
+
+ 用来保存系统调用返回值的 a0 寄存器也会同样发生变化。我们从 Trap 上下文取出作为 syscall ID 的 a7 和系统调用的三个参数 a0~a2 传给
+ ``syscall`` 函数并获取返回值。 ``syscall`` 函数是在 ``syscall`` 子模块中实现的。 这段代码是处理正常系统调用的控制逻辑。
+- 第 12~20 行,分别处理应用程序出现访存错误和非法指令错误的情形。此时需要打印错误信息并调用 ``run_next_app`` 直接切换并运行下一个
+ 应用程序。
+- 第 21 行开始,当遇到目前还不支持的 Trap 类型的时候,批处理操作系统整个 panic 报错退出。
+
+对于系统调用而言, ``syscall`` 函数并不会实际处理系统调用,而只是根据 syscall ID 分发到具体的处理函数:
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/syscall/mod.rs
+
+ pub fn syscall(syscall_id: usize, args: [usize; 3]) -> isize {
+ match syscall_id {
+ SYSCALL_WRITE => sys_write(args[0], args[1] as *const u8, args[2]),
+ SYSCALL_EXIT => sys_exit(args[0] as i32),
+ _ => panic!("Unsupported syscall_id: {}", syscall_id),
+ }
+ }
+
+这里我们会将传进来的参数 ``args`` 转化成能够被具体的系统调用处理函数接受的类型。它们的实现都非常简单:
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/syscall/fs.rs
+
+ const FD_STDOUT: usize = 1;
+
+ pub fn sys_write(fd: usize, buf: *const u8, len: usize) -> isize {
+ match fd {
+ FD_STDOUT => {
+ let slice = unsafe { core::slice::from_raw_parts(buf, len) };
+ let str = core::str::from_utf8(slice).unwrap();
+ print!("{}", str);
+ len as isize
+ },
+ _ => {
+ panic!("Unsupported fd in sys_write!");
+ }
+ }
+ }
+
+ // os/src/syscall/process.rs
+
+ pub fn sys_exit(xstate: i32) -> ! {
+ println!("[kernel] Application exited with code {}", xstate);
+ run_next_app()
+ }
+
+- ``sys_write`` 我们将传入的位于应用程序内的缓冲区的开始地址和长度转化为一个字符串 ``&str`` ,然后使用批处理操作系统已经实现的 ``print!``
+ 宏打印出来。这里我们并没有检查传入参数的安全性,存在安全隐患。
+- ``sys_exit`` 打印退出的应用程序的返回值并同样调用 ``run_next_app`` 切换到下一个应用程序。
+
+.. _ch2-app-execution:
+
+执行应用程序
+-------------------------------------
+
+当批处理操作系统初始化完成,或者是某个应用程序运行结束或出错的时候,我们要调用 ``run_next_app`` 函数切换到下一个应用程序。此时 CPU 运行在
+S 特权级,而它希望能够切换到 U 特权级。在 RISC-V 架构中,唯一一种能够使得 CPU 特权级下降的方法就是通过 Trap 返回系列指令,比如
+``sret`` 。事实上,在运行应用程序之前要完成如下这些工作:
+
+- 跳转到应用程序入口点 ``0x80400000``;
+- 将使用的栈切换到用户栈;
+- 在 ``__alltraps`` 时我们要求 ``sscratch`` 指向内核栈,这个也需要在此时完成;
+- 从 S 特权级切换到 U 特权级。
+
+它们可以通过复用 ``__restore`` 的代码来更容易的实现上述工作。我们只需要在内核栈上压入一个为启动应用程序而特殊构造的 Trap 上下文,再通过 ``__restore`` 函数,就能
+让这些寄存器到达启动应用程序所需要的上下文状态。
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/trap/context.rs
+
+ impl TrapContext {
+ pub fn set_sp(&mut self, sp: usize) { self.x[2] = sp; }
+ pub fn app_init_context(entry: usize, sp: usize) -> Self {
+ let mut sstatus = sstatus::read();
+ sstatus.set_spp(SPP::User);
+ let mut cx = Self {
+ x: [0; 32],
+ sstatus,
+ sepc: entry,
+ };
+ cx.set_sp(sp);
+ cx
+ }
+ }
+
+为 ``TrapContext`` 实现 ``app_init_context`` 方法,修改其中的 sepc 寄存器为应用程序入口点 ``entry``, sp 寄存器为我们设定的
+一个栈指针,并将 sstatus 寄存器的 ``SPP`` 字段设置为 User 。
+
+在 ``run_next_app`` 函数中我们能够看到:
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/batch.rs
+
+ pub fn run_next_app() -> ! {
+ let mut app_manager = APP_MANAGER.exclusive_access();
+ let current_app = app_manager.get_current_app();
+ unsafe {
+ app_manager.load_app(current_app);
+ }
+ app_manager.move_to_next_app();
+ drop(app_manager);
+ // before this we have to drop local variables related to resources manually
+ // and release the resources
+ extern "C" {
+ fn __restore(cx_addr: usize);
+ }
+ unsafe {
+ __restore(KERNEL_STACK.push_context(TrapContext::app_init_context(
+ APP_BASE_ADDRESS,
+ USER_STACK.get_sp(),
+ )) as *const _ as usize);
+ }
+ panic!("Unreachable in batch::run_current_app!");
+ }
+
+
+``__restore`` 所做的事情是在内核栈上压入一个 Trap 上下文,其 ``sepc`` 是应用程序入口地址 ``0x80400000`` ,其 ``sp`` 寄存器指向用户栈,其 ``sstatus``
+的 ``SPP`` 字段被设置为 User 。
+``push_context`` 的返回值是内核栈压入 Trap 上下文之后的栈顶,它会被作为 ``__restore`` 的参数(
+回看 :ref:`__restore 代码 ` ,这时我们可以理解为何 ``__restore`` 函数的起始部分会完成
+:math:`\text{sp}\leftarrow\text{a}_0` ),这使得在 ``__restore`` 函数中 ``sp`` 仍然可以指向内核栈的栈顶。这之后,就和执行一次普通的
+``__restore`` 函数调用一样了。
+
+.. note::
+
+ 有兴趣的读者可以思考: sscratch 是何时被设置为内核栈顶的?
+
+
+
+..
+ 马老师发生甚么事了?
+ --
+ 这里要说明目前只考虑从 U Trap 到 S ,而实际上 Trap 的要素就有:Trap 之前在哪个特权级,Trap 在哪个特权级处理。这个对于中断和异常
+ 都是如此,只不过中断可能跟特权级的关系稍微更紧密一点。毕竟中断的类型都是跟特权级挂钩的。但是对于 Trap 而言有一点是共同的,也就是触发
+ Trap 不会导致优先级下降。从中断/异常的代理就可以看出从定义上就不允许代理到更低的优先级。而且代理只能逐级代理,目前我们能操作的只有从
+ M 代理到 S,其他代理都基本只出现在指令集拓展或者硬件还不支持。中断的情况是,如果是属于某个特权级的中断,不能在更低的优先级处理。事实上
+ 这个中断只可能在 CPU 处于不会更高的优先级上收到(否则会被屏蔽),而 Trap 之后优先级不会下降(Trap 代理机制决定),这样就自洽了。
+ --
+ 之前提到异常是说需要执行环境功能的原因与某条指令的执行有关。而 Trap 的定义更加广泛一些,就是在执行某条指令之后发现需要执行环境的功能,
+ 如果是中断的话 Trap 回来之后默认直接执行下一条指令,如果是异常的话硬件会将 sepc 设置为 Trap 发生之前最后执行的那条指令,而异常发生
+ 的原因不一定和这条指令的执行有关。应该指出的是,在大多数情况下都是和最后这条指令的执行有关。但在缓存的作用下也会出现那种特别极端的情况。
+ --
+ 然后是 Trap 到 S,就有 S 模式的一些相关 CSR,以及从 U Trap 到 S,硬件会做哪些事情(包括触发异常的一瞬间,以及处理完成调用 sret
+ 之后)。然后指出从用户的视角来看,如果是 ecall 的话, Trap 回来之后应该从 ecall 的下一条指令开始执行,且执行现场不能发生变化。
+ 所以就需要将应用执行环境保存在内核栈上(还需要换栈!)。栈存在的原因可能是 Trap handler 是一条新的运行在 S 特权级的执行流,所以
+ 这个可以理解成跨特权级的执行流切换,确实就复杂一点,要保存的内容也相对多一点。而下一章多任务的任务切换是全程发生在 S 特权级的执行流
+ 切换,所以会简单一点,保存的通用寄存器大概率更少(少在调用者保存寄存器),从各种意义上都很像函数调用。从不同特权级的角度来解释换栈
+ 是出于安全性,应用不应该看到 Trap 执行流的栈,这样做完之后,虽然理论上可以访问,但应用不知道内核栈的位置应该也有点麻烦。
+ --
+ 然后是 rust_trap 的处理,尤其是奇妙的参数传递,内部处理逻辑倒是非常简单。
+ --
+ 最后是如何利用 __restore 初始化应用的执行环境,包括如何设置入口点、用户栈以及保证在 U 特权级执行。
+
+
+
+
+
diff --git a/guide/source/chapter2/5exercise.rst b/guide/source/chapter2/5exercise.rst
new file mode 100644
index 0000000..fbeaa2c
--- /dev/null
+++ b/guide/source/chapter2/5exercise.rst
@@ -0,0 +1,139 @@
+chapter2练习(已废弃)
+=====================================================
+
+.. toctree::
+ :hidden:
+ :maxdepth: 4
+
+编程练习
+-------------------------------
+
+简单安全检查
++++++++++++++++++++++++++++++++
+
+.. lab2 中,我们实现了第一个系统调用 ``sys_write``,这使得我们可以在用户态输出信息。但是 os 在提供服务的同时,还有保护 os 本身以及其他用户程序不受错误或者恶意程序破坏的功能。
+
+.. 由于还没有实现虚拟内存,我们可以在用户程序中指定一个属于其他程序字符串,并将它输出,这显然是不合理的,因此我们要对 sys_write 做检查:
+
+.. - sys_write 仅能输出位于程序本身内存空间内的数据,否则报错。
+
+实验要求
++++++++++++++++++++++++++++++++
+.. - 实现分支: ch2。
+.. - 完成实验指导书中的内容,能运行用户态程序并执行 sys_write,sys_exit 系统调用。
+.. - 为 sys_write 增加安全性检查,并通过 `Rust测例 `_ 中 chapter2 对应的所有测例,测例详情见对应仓库。
+
+.. challenge: 支持多核,实现多个核运行用户程序。
+
+实验约定
+++++++++++++++++++++++++++++++
+
+.. 在第二章的测试中,我们对于内核有如下仅仅为了测试方便的要求,请调整你的内核代码来符合这些要求。
+
+.. - 用户栈大小必须为 4096,且按照 4096 字节对齐。这一规定可以在实验4开始删除,仅仅为通过 lab2/3 测例设置。
+
+.. .. _inherit-last-ch-changes:
+
+.. .. note::
+
+.. **如何快速继承上一章练习题的修改**
+
+.. 从这一章开始,在完成本章习题之前,首先要做的就是将上一章框架的修改继承到本章的框架代码。出于各种原因,实际上通过 ``git merge`` 并不是很方便,这里给出一种打 patch 的方法,希望能够有所帮助。
+
+.. 1. 切换到上一章的分支,通过 ``git log`` 找到你在此分支上的第一次 commit 的前一个 commit 的 ID ,复制其前 8 位,记作 ``base-commit`` 。假设分支上最新的一次 commit ID 是 ``last-commit`` 。
+.. 2. 确保你位于项目根目录 ``rCore-Tutorial-v3`` 下。通过 ``git diff > `` 即可在 ``patch-path`` 路径位置(比如 ``~/Desktop/chx.patch`` )生成一个描述你对于上一章分支进行的全部修改的一个补丁文件。打开看一下,它给出了每个被修改的文件中涉及了哪些块的修改,还附加了块前后的若干行代码。如果想更加灵活进行合并的话,可以通过 ``git format-patch `` 命令在当前目录下生成一组补丁,它会对于 ``base-commit`` 后面的每一次 commit 均按照顺序生成一个补丁。
+.. 3. 切换到本章分支,通过 ``git apply --reject `` 来将一个补丁打到当前章节上。它的大概原理是对于补丁中的每个被修改文件中的每个修改块,尝试通过块的前后若干行代码来定位它在当前分支上的位置并进行替换。有一些块可能无法匹配,此时会生成与这些块所在的文件同名的 ``*.rej`` 文件,描述了哪些块替换失败了。在项目根目录 ``rCore-Tutorial-v3`` 下,可以通过 ``find . -name *.rej`` 来找到所有相关的 ``*.rej`` 文件并手动完成替换。
+.. 4. 在处理完所有 ``*.rej`` 之后,将它们删除并 commit 一下。现在就可以开始本章的实验了。
+
+
+实验检查
+++++++++++++++++++++++++++++++
+
+.. - 实验目录要求(Rust)
+
+.. .. code-block::
+
+.. ├── os(内核实现)
+.. │ ├── build.rs (在这里实现用户程序的打包)
+.. │ ├── Cargo.toml(配置文件)
+.. │ ├── Makefile (要求 make run 可以正确执行,尽量不输出调试信息)
+.. │ ├── src(所有内核的源代码放在 os/src 目录下)
+.. │ ├── main.rs(内核主函数)
+.. │ ├── ...
+.. ├── reports
+.. │ ├── lab2.md/pdf
+.. │ └── ...
+.. ├── README.md(其他必要的说明)
+.. ├── ...
+
+.. 参考示例目录结构。目标用户目录 ``../user/build/bin``。
+
+.. - 检查
+
+.. .. code-block:: console
+
+.. $ git checkout ch2
+.. $ cd os
+.. $ make run
+
+.. 可以正确执行正确执行目标用户测例,并得到预期输出(详见测例注释)。
+
+
+简答题
+-------------------------------
+
+.. 1. 正确进入 U 态后,程序的特征还应有:使用 S 态特权指令,访问 S 态寄存器后会报错。目前由于一些其他原因,这些问题不太好测试,请同学们可以自行测试这些内容(参考 `前三个测例 `_ ),描述程序出错行为,同时注意注明你使用的 sbi 及其版本。
+
+.. 2. 请结合用例理解 `trap.S `_ 中两个函数 ``__alltraps`` 和 ``__restore`` 的作用,并回答如下几个问题:
+
+.. 1. L40:刚进入 ``__restore`` 时,``a0`` 代表了什么值。请指出 ``__restore`` 的两种使用情景。
+
+.. 2. L46-L51:这几行汇编代码特殊处理了哪些寄存器?这些寄存器的的值对于进入用户态有何意义?请分别解释。
+
+.. .. code-block:: riscv
+
+.. ld t0, 32*8(sp)
+.. ld t1, 33*8(sp)
+.. ld t2, 2*8(sp)
+.. csrw sstatus, t0
+.. csrw sepc, t1
+.. csrw sscratch, t2
+
+.. 3. L53-L59:为何跳过了 ``x2`` 和 ``x4``?
+
+.. .. code-block:: riscv
+
+.. ld x1, 1*8(sp)
+.. ld x3, 3*8(sp)
+.. .set n, 5
+.. .rept 27
+.. LOAD_GP %n
+.. .set n, n+1
+.. .endr
+
+.. 4. L63:该指令之后,``sp`` 和 ``sscratch`` 中的值分别有什么意义?
+
+.. .. code-block:: riscv
+
+.. csrrw sp, sscratch, sp
+
+.. 5. ``__restore``:中发生状态切换在哪一条指令?为何该指令执行之后会进入用户态?
+
+.. 6. L13:该指令之后,``sp`` 和 ``sscratch`` 中的值分别有什么意义?
+
+.. .. code-block:: riscv
+
+.. csrrw sp, sscratch, sp
+
+.. 7. 从 U 态进入 S 态是哪一条指令发生的?
+
+.. 3. 程序陷入内核的原因有中断和异常(系统调用),请问 riscv64 支持哪些中断 / 异常?如何判断进入内核是由于中断还是异常?描述陷入内核时的几个重要寄存器及其值。
+
+.. 4. 对于任何中断,``__alltraps`` 中都需要保存所有寄存器吗?你有没有想到一些加速 ``__alltraps`` 的方法?简单描述你的想法。
+
+报告要求
+-------------------------------
+
+- 简单总结你实现的功能(200字以内,不要贴代码)。
+- 完成问答题。
+- (optional) 你对本次实验设计及难度/工作量的看法,以及有哪些需要改进的地方,欢迎畅所欲言。
diff --git a/guide/source/chapter2/index.rst b/guide/source/chapter2/index.rst
new file mode 100644
index 0000000..5f1083d
--- /dev/null
+++ b/guide/source/chapter2/index.rst
@@ -0,0 +1,13 @@
+.. _link-chapter2:
+
+第二章:批处理系统
+==============================================
+
+.. toctree::
+ :maxdepth: 4
+
+ 0intro
+ 2application
+ 3batch-system
+ 4trap-handling
+
diff --git a/guide/source/chapter3/0intro.rst b/guide/source/chapter3/0intro.rst
new file mode 100644
index 0000000..84d6440
--- /dev/null
+++ b/guide/source/chapter3/0intro.rst
@@ -0,0 +1,209 @@
+引言
+========================================
+
+本章导读
+--------------------------
+
+
+本章的目标是实现分时多任务系统,它能并发地执行多个用户程序,并调度这些程序。为此需要实现
+
+- 一次性加载所有用户程序,减少任务切换开销;
+- 支持任务切换机制,保存切换前后程序上下文;
+- 支持程序主动放弃处理器,实现 yield 系统调用;
+- 以时间片轮转算法调度用户程序,实现资源的时分复用。
+
+
+实践体验
+-------------------------------------
+
+.. code-block:: console
+
+ $ git clone https://github.com/LearningOS/rCore-Tutorial-Code-2022S.git
+ $ cd rCore-Tutorial-Code-2022S
+ $ git checkout ch3
+ $ git clone https://github.com/LearningOS/rCore-Tutorial-Test-2022S.git user
+
+在 qemu 模拟器上运行本章代码:
+
+.. code-block:: console
+
+ $ cd os
+ $ make run
+
+运行代码,看到用户程序交替输出信息:
+
+.. code-block::
+
+ [rustsbi] RustSBI version 0.2.0-alpha.4
+ .______ __ __ _______.___________. _______..______ __
+ | _ \ | | | | / | | / || _ \ | |
+ | |_) | | | | | | (----`---| |----`| (----`| |_) || |
+ | / | | | | \ \ | | \ \ | _ < | |
+ | |\ \----.| `--' |.----) | | | .----) | | |_) || |
+ | _| `._____| \______/ |_______/ |__| |_______/ |______/ |__|
+
+ [rustsbi] Implementation: RustSBI-QEMU Version 0.0.1
+ [rustsbi-dtb] Hart count: cluster0 with 1 cores
+ [rustsbi] misa: RV64ACDFIMSU
+ [rustsbi] mideleg: ssoft, stimer, sext (0x222)
+ [rustsbi] medeleg: ima, ia, bkpt, la, sa, uecall, ipage, lpage, spage (0xb1ab)
+ [rustsbi] pmp0: 0x80000000 ..= 0x800fffff (rwx)
+ [rustsbi] pmp1: 0x80000000 ..= 0x807fffff (rwx)
+ [rustsbi] pmp2: 0x0 ..= 0xffffffffffffff (---)
+ [rustsbi] enter supervisor 0x80200000
+ [kernel] Hello, world!
+ power_3 [10000/200000]
+ power_3 [20000/200000]
+ power_3 [30000/200000]
+ power_3 [40000/200000]
+ power_3 [50000/200000]
+ power_3 [60000/200000]
+ power_3 [70000/200000]
+ power_3 [80000/200000]
+ power_3 [90000/200000]
+ power_3 [100000/200000]
+ power_3 [110000/200000]
+ power_3 [120000/200000]
+ power_3 [130000/200000]
+ power_3 [140000/200000]
+ power_3 [150000/200000]
+ power_3 [160000/200000]
+ power_3 [170000/200000]
+ power_3 [180000/200000]
+ power_3 [190000/200000]
+ power_3 [200000/200000]
+ 3^200000 = 871008973(MOD 998244353)
+ Test power_3 OK!
+ power_5 [10000/140000]
+ power_5 [20000/140000]
+ power_5 [30000/140000]
+ power_5 [40000/140000]
+ power_5 [50000/140000]
+ power_5 [60000/140000]
+ power_7 [10000/160000]
+ power_7 [20000/160000]
+ power_7 [30000/160000]
+ power_7 [40000/160000]
+ power_7 [50000/160000]
+ power_7 [60000/160000]
+ power_7 [70000/160000]
+ power_7 [80000/160000]
+ power_7 [90000/160000]
+ power_7 [100000/160000]
+ power_7 [110000/160000]
+ power_7 [120000/160000]
+ power_7 [130000/160000]
+ power_7 [140000/160000]
+ power_7 [150000/160000]
+ power_7 [160000/160000]
+ 7^160000 = 667897727(MOD 998244353)
+ Test power_7 OK!
+ get_time OK! 42
+ current time_msec = 42
+ AAAAAAAAAA [1/5]
+ BBBBBBBBBB [1/5]
+ CCCCCCCCCC [1/5]
+ power_5 [70000/140000]
+ AAAAAAAAAA [2/5]
+ BBBBBBBBBB [2/5]
+ CCCCCCCCCC [2/5]
+ power_5 [80000/140000]
+ power_5 [90000/140000]
+ power_5 [100000/140000]
+ power_5 [110000/140000]
+ power_5 [120000/140000]
+ power_5 [130000/140000]
+ power_5 [140000/140000]
+ 5^140000 = 386471875(MOD 998244353)
+ Test power_5 OK!
+ AAAAAAAAAA [3/5]
+ BBBBBBBBBB [3/5]
+ CCCCCCCCCC [3/5]
+ AAAAAAAAAA [4/5]
+ BBBBBBBBBB [4/5]
+ CCCCCCCCCC [4/5]
+ AAAAAAAAAA [5/5]
+ BBBBBBBBBB [5/5]
+ CCCCCCCCCC [5/5]
+ Test write A OK!
+ Test write B OK!
+ Test write C OK!
+ time_msec = 143 after sleeping 100 ticks, delta = 101ms!
+ Test sleep1 passed!
+ Test sleep OK!
+ Panicked at src/task/mod.rs:98 All applications completed!
+
+
+本章代码树
+---------------------------------------------
+
+.. code-block::
+
+ ── os
+ ├── build.rs
+ ├── Cargo.toml
+ ├── Makefile
+ └── src
+ ├── batch.rs(移除:功能分别拆分到 loader 和 task 两个子模块)
+ ├── config.rs(新增:保存内核的一些配置)
+ ├── console.rs
+ ├── logging.rs
+ ├── sync
+ ├── entry.asm
+ ├── lang_items.rs
+ ├── link_app.S
+ ├── linker.ld
+ ├── loader.rs(新增:将应用加载到内存并进行管理)
+ ├── main.rs(修改:主函数进行了修改)
+ ├── sbi.rs(修改:引入新的 sbi call set_timer)
+ ├── syscall(修改:新增若干 syscall)
+ │ ├── fs.rs
+ │ ├── mod.rs
+ │ └── process.rs
+ ├── task(新增:task 子模块,主要负责任务管理)
+ │ ├── context.rs(引入 Task 上下文 TaskContext)
+ │ ├── mod.rs(全局任务管理器和提供给其他模块的接口)
+ │ ├── switch.rs(将任务切换的汇编代码解释为 Rust 接口 __switch)
+ │ ├── switch.S(任务切换的汇编代码)
+ │ └── task.rs(任务控制块 TaskControlBlock 和任务状态 TaskStatus 的定义)
+ ├── timer.rs(新增:计时器相关)
+ └── trap
+ ├── context.rs
+ ├── mod.rs(修改:时钟中断相应处理)
+ └── trap.S
+
+ cloc os
+ -------------------------------------------------------------------------------
+ Language files blank comment code
+ -------------------------------------------------------------------------------
+ Rust 21 87 20 627
+ Assembly 4 12 22 144
+ make 1 11 4 36
+ TOML 1 2 1 10
+ -------------------------------------------------------------------------------
+ SUM: 27 112 47 817
+ -------------------------------------------------------------------------------
+
+
+.. 本章代码导读
+.. -----------------------------------------------------
+
+.. 本章的重点是实现对应用之间的协作式和抢占式任务切换的操作系统支持。与上一章的操作系统实现相比,有如下一些不同的情况导致实现上也有差异:
+
+.. - 多个应用同时放在内存中,所以他们的起始地址是不同的,且地址范围不能重叠
+.. - 应用在整个执行过程中会暂停或被抢占,即会有主动或被动的任务切换
+
+.. 这些实现上差异主要集中在对应用程序执行过程的管理、支持应用程序暂停的系统调用和主动切换应用程序所需的时钟中断机制的管理。
+
+.. 对于第一个不同情况,需要对应用程序的地址空间布局进行调整,每个应用的地址空间都不相同,且不能重叠。这并不要修改应用程序本身,而是通过一个脚本 ``build.py`` 来针对每个应用程序修改链接脚本 ``linker.ld`` 中的 ``BASE_ADDRESS`` ,让编译器在编译不同应用时用到的 ``BASE_ADDRESS`` 都不同,且有足够大的地址间隔。这样就可以让每个应用所在的内存空间是不同的。
+
+.. 对于第二个不同情况,需要实现任务切换,这就需要在上一章的 ``trap`` 上下文切换的基础上,再加上一个 ``task`` 上下文切换,才能完成完整的任务切换。这里面的关键数据结构是表示应用执行上下文的 ``TaskContext`` 数据结构和具体完成上下文切换的汇编语言编写的 ``__switch`` 函数。一个应用的执行需要被操作系统管理起来,这是通过 ``TaskControlBlock`` 数据结构来表示应用执行上下文的动态过程和动态状态(运行态、就绪态等)。而为了做好应用程序第一次执行的前期初始化准备, ``TaskManager`` 数据结构的全局变量实例 ``TASK_MANAGER`` 描述了应用程序初始化所需的数据, 而 ``TASK_MANAGER`` 的初始化赋值过程是实现这个准备的关键步骤。
+
+.. 应用程序可以在用户态执行后,还需要有新的系统调用 ``sys_yield`` 的实现来支持应用自己的主动暂停;还要添加对时钟中断的处理,来支持抢占应用执行的抢占式切换。有了时钟中断,就可以在一定时间内打断应用的执行,并主动切换到另外一个应用,这部分主要是通过对 ``trap_handler`` 函数中进行扩展,来完成在时钟中断产生时可能进行的任务切换。 ``TaskManager`` 数据结构的成员函数 ``run_next_task`` 来实现基于任务控制块的切换,并会具体调用 ``__switch`` 函数完成硬件相关部分的任务上下文切换。
+
+.. 如果理解了上面的数据结构和相关函数的关系和相互调用的情况,那么就比较容易理解本章改进后的操作系统了。
+
+
+.. .. [#prionosuchus] 锯齿螈身长可达9米,是迄今出现过的最大的两栖动物,是二叠纪时期江河湖泊和沼泽中的顶级掠食者。
+.. .. [#eoraptor] 始初龙(也称始盗龙)是后三叠纪时期的两足食肉动物,也是目前所知最早的恐龙,它们只有一米长,却代表着恐龙的黎明。
+.. .. [#coelophysis] 腔骨龙(也称虚形龙)最早出现于三叠纪晚期,它体形纤细,善于奔跑,以小型动物为食。
diff --git a/guide/source/chapter3/1multi-loader.rst b/guide/source/chapter3/1multi-loader.rst
new file mode 100644
index 0000000..7ebe569
--- /dev/null
+++ b/guide/source/chapter3/1multi-loader.rst
@@ -0,0 +1,71 @@
+多道程序放置与加载
+=====================================
+
+多道程序放置
+----------------------------
+
+
+在第二章中,内核让所有应用都共享同一个固定的起始地址。
+正因如此,内存中同时最多只能驻留一个应用,
+
+要一次加载运行多个程序,就要求每个用户程序被内核加载到内存中的起始地址都不同。
+为此,我们编写脚本 ``user/build.py`` 为每个应用定制各自的起始地址。
+它的思路很简单,对于每一个应用程序,使用 ``cargo rustc`` 单独编译,
+用 ``-Clink-args=-Ttext=xxxx`` 选项指定链接时 .text 段的地址为 ``0x80400000 + app_id * 0x20000`` 。
+
+.. note::
+
+ qemu 预留的内存空间是有限的,如果加载的程序过多,程序地址超出内存空间,可能出现 ``core dumped``.
+
+多道程序加载
+----------------------------
+
+在第二章中负责应用加载和执行的子模块 ``batch`` 被拆分为 ``loader`` 和 ``task`` ,
+前者负责启动时加载应用程序,后者负责切换和调度。
+
+其中, ``loader`` 模块的 ``load_apps`` 函数负责将所有用户程序在内核初始化的时一并加载进内存。
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/loader.rs
+
+ pub fn load_apps() {
+ extern "C" {
+ fn _num_app();
+ }
+ let num_app_ptr = _num_app as usize as *const usize;
+ let num_app = get_num_app();
+ let app_start = unsafe { core::slice::from_raw_parts(num_app_ptr.add(1), num_app + 1) };
+ // clear i-cache first
+ unsafe {
+ core::arch::asm!("fence.i");
+ }
+ // load apps
+ for i in 0..num_app {
+ let base_i = get_base_i(i);
+ // clear region
+ (base_i..base_i + APP_SIZE_LIMIT)
+ .for_each(|addr| unsafe { (addr as *mut u8).write_volatile(0) });
+ // load app from data section to memory
+ let src = unsafe {
+ core::slice::from_raw_parts(app_start[i] as *const u8, app_start[i + 1] - app_start[i])
+ };
+ let dst = unsafe { core::slice::from_raw_parts_mut(base_i as *mut u8, src.len()) };
+ dst.copy_from_slice(src);
+ }
+ }
+
+第 :math:`i` 个应用被加载到以物理地址 ``base_i`` 开头的一段物理内存上,而 ``base_i`` 的计算方式如下:
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/loader.rs
+
+ fn get_base_i(app_id: usize) -> usize {
+ APP_BASE_ADDRESS + app_id * APP_SIZE_LIMIT
+ }
+
+我们可以在 ``config`` 子模块中找到这两个常数, ``APP_BASE_ADDRESS`` 被设置为 ``0x80400000`` ,
+而 ``APP_SIZE_LIMIT`` 和上一章一样被设置为 ``0x20000`` 。这种放置方式与 ``user/build.py`` 的实现一致。
diff --git a/guide/source/chapter3/2task-switching.rst b/guide/source/chapter3/2task-switching.rst
new file mode 100644
index 0000000..b32046b
--- /dev/null
+++ b/guide/source/chapter3/2task-switching.rst
@@ -0,0 +1,104 @@
+任务切换
+================================
+
+
+本节我们将见识操作系统的核心机制—— **任务切换** ,
+即应用在运行中主动或被动地交出 CPU 的使用权,内核可以选择另一个程序继续执行。
+内核需要保证用户程序两次运行期间,任务上下文(如寄存器、栈等)保持一致。
+
+任务切换的设计与实现
+---------------------------------
+
+任务切换与上一章提及的 Trap 控制流切换相比,有如下异同:
+
+- 与 Trap 切换不同,它不涉及特权级切换,部分由编译器完成;
+- 与 Trap 切换相同,它对应用是透明的。
+
+事实上,任务切换是来自两个不同应用在内核中的 Trap 控制流之间的切换。
+当一个应用 Trap 到 S 态 OS 内核中进行进一步处理时,
+其 Trap 控制流可以调用一个特殊的 ``__switch`` 函数。
+在 ``__switch`` 返回之后,Trap 控制流将继续从调用该函数的位置继续向下执行。
+而在调用 ``__switch`` 之后到返回前的这段时间里,
+原 Trap 控制流 ``A`` 会先被暂停并被切换出去, CPU 转而运行另一个应用的 Trap 控制流 ``B`` 。
+``__switch`` 返回之后,原 Trap 控制流 ``A`` 才会从某一条 Trap 控制流 ``C`` 切换回来继续执行。
+
+我们需要在 ``__switch`` 中保存 CPU 的某些寄存器,它们就是 **任务上下文** (Task Context)。
+
+下面我们给出 ``__switch`` 的实现:
+
+.. code-block:: riscv
+ :linenos:
+
+ # os/src/task/switch.S
+
+ .altmacro
+ .macro SAVE_SN n
+ sd s\n, (\n+2)*8(a0)
+ .endm
+ .macro LOAD_SN n
+ ld s\n, (\n+2)*8(a1)
+ .endm
+ .section .text
+ .globl __switch
+ __switch:
+ # __switch(
+ # current_task_cx_ptr: *mut TaskContext,
+ # next_task_cx_ptr: *const TaskContext
+ # )
+ # save kernel stack of current task
+ sd sp, 8(a0)
+ # save ra & s0~s11 of current execution
+ sd ra, 0(a0)
+ .set n, 0
+ .rept 12
+ SAVE_SN %n
+ .set n, n + 1
+ .endr
+ # restore ra & s0~s11 of next execution
+ ld ra, 0(a1)
+ .set n, 0
+ .rept 12
+ LOAD_SN %n
+ .set n, n + 1
+ .endr
+ # restore kernel stack of next task
+ ld sp, 8(a1)
+ ret
+
+它的两个参数分别是当前和即将被切换到的 Trap 控制流的 ``task_cx_ptr`` ,从 RISC-V 调用规范可知,它们分别通过寄存器 ``a0/a1`` 传入。
+
+内核先把 ``current_task_cx_ptr`` 中包含的寄存器值逐个保存,再把 ``next_task_cx_ptr`` 中包含的寄存器值逐个恢复。
+
+``TaskContext`` 里包含的寄存器有:
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/task/context.rs
+ #[repr(C)]
+ pub struct TaskContext {
+ ra: usize,
+ sp: usize,
+ s: [usize; 12],
+ }
+
+``s0~s11`` 是被调用者保存寄存器, ``__switch`` 是用汇编编写的,编译器不会帮我们处理这些寄存器。
+保存 ``ra`` 很重要,它记录了 ``__switch`` 函数返回之后应该跳转到哪里继续执行。
+
+我们将这段汇编代码 ``__switch`` 解释为一个 Rust 函数:
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/task/switch.rs
+
+ core::arch::global_asm!(include_str!("switch.S"));
+
+ extern "C" {
+ pub fn __switch(
+ current_task_cx_ptr: *mut TaskContext,
+ next_task_cx_ptr: *const TaskContext);
+ }
+
+我们会调用该函数来完成切换功能,而不是直接跳转到符号 ``__switch`` 的地址。
+因此在调用前后,编译器会帮我们保存和恢复调用者保存寄存器。
diff --git a/guide/source/chapter3/3multiprogramming.rst b/guide/source/chapter3/3multiprogramming.rst
new file mode 100644
index 0000000..5132c3a
--- /dev/null
+++ b/guide/source/chapter3/3multiprogramming.rst
@@ -0,0 +1,317 @@
+管理多道程序
+=========================================
+
+
+而内核为了管理任务,需要维护任务信息,相关内容包括:
+
+- 任务运行状态:未初始化、准备执行、正在执行、已退出
+- 任务控制块:维护任务状态和任务上下文
+- 任务相关系统调用:程序主动暂停 ``sys_yield`` 和主动退出 ``sys_exit``
+
+yield 系统调用
+-------------------------------------------------------------------------
+
+
+.. image:: multiprogramming.png
+
+上图描述了一种多道程序执行的典型情况。其中横轴为时间线,纵轴为正在执行的实体。
+开始时,蓝色应用向外设提交了一个请求,外设随即开始工作,
+但是它要一段时间后才能返回结果。蓝色应用于是调用 ``sys_yield`` 交出 CPU 使用权,
+内核让绿色应用继续执行。一段时间后 CPU 切换回蓝色应用,发现外设仍未返回结果,
+于是再次 ``sys_yield`` 。直到第二次切换回蓝色应用,外设才处理完请求,于是蓝色应用终于可以向下执行了。
+
+我们还会遇到很多其他需要等待其完成才能继续向下执行的事件,调用 ``sys_yield`` 可以避免等待过程造成的资源浪费。
+
+.. code-block:: rust
+ :caption: 第三章新增系统调用(一)
+
+ /// 功能:应用主动交出 CPU 所有权并切换到其他应用。
+ /// 返回值:总是返回 0。
+ /// syscall ID:124
+ fn sys_yield() -> isize;
+
+用户库对应的实现和封装:
+
+.. code-block:: rust
+
+ // user/src/syscall.rs
+
+ pub fn sys_yield() -> isize {
+ syscall(SYSCALL_YIELD, [0, 0, 0])
+ }
+
+ // user/src/lib.rs
+ // yield 是 Rust 的关键字
+ pub fn yield_() -> isize { sys_yield() }
+
+下文介绍内核应如何实现该系统调用。
+
+任务控制块与任务运行状态
+---------------------------------------------------------
+
+任务运行状态暂包括如下几种:
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/task/task.rs
+
+ #[derive(Copy, Clone, PartialEq)]
+ pub enum TaskStatus {
+ UnInit, // 未初始化
+ Ready, // 准备运行
+ Running, // 正在运行
+ Exited, // 已退出
+ }
+
+任务状态外和任务上下文一并保存在名为 **任务控制块** (Task Control Block) 的数据结构中:
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/task/task.rs
+
+ #[derive(Copy, Clone)]
+ pub struct TaskControlBlock {
+ pub task_status: TaskStatus,
+ pub task_cx: TaskContext,
+ }
+
+
+任务控制块非常重要。在内核中,它就是应用的管理单位。后面的章节我们还会不断向里面添加更多内容。
+
+任务管理器
+--------------------------------------
+
+内核需要一个全局的任务管理器来管理这些任务控制块:
+
+.. code-block:: rust
+
+ // os/src/task/mod.rs
+
+ pub struct TaskManager {
+ num_app: usize,
+ inner: UPSafeCell,
+ }
+
+ struct TaskManagerInner {
+ tasks: [TaskControlBlock; MAX_APP_NUM],
+ current_task: usize,
+ }
+
+这里用到了变量与常量分离的编程风格:字段 ``num_app`` 表示应用数目,它在 ``TaskManager`` 初始化后将保持不变;
+而包裹在 ``TaskManagerInner`` 内的任务控制块数组 ``tasks``,以及正在执行的应用编号 ``current_task`` 会在执行过程中变化。
+
+初始化 ``TaskManager`` 的全局实例 ``TASK_MANAGER``:
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/task/mod.rs
+
+ lazy_static! {
+ pub static ref TASK_MANAGER: TaskManager = {
+ let num_app = get_num_app();
+ let mut tasks = [TaskControlBlock {
+ task_cx: TaskContext::zero_init(),
+ task_status: TaskStatus::UnInit,
+ }; MAX_APP_NUM];
+ for (i, t) in tasks.iter_mut().enumerate().take(num_app) {
+ t.task_cx = TaskContext::goto_restore(init_app_cx(i));
+ t.task_status = TaskStatus::Ready;
+ }
+ TaskManager {
+ num_app,
+ inner: unsafe {
+ UPSafeCell::new(TaskManagerInner {
+ tasks,
+ current_task: 0,
+ })
+ },
+ }
+ };
+ }
+
+- 第 5 行:调用 ``loader`` 子模块提供的 ``get_num_app`` 接口获取链接到内核的应用总数;
+- 第 10~12 行:依次对每个任务控制块进行初始化,将其运行状态设置为 ``Ready`` ,并在它的内核栈栈顶压入一些初始化
+ 上下文,然后更新它的 ``task_cx`` 。一些细节我们会稍后介绍。
+- 从第 14 行开始:创建 ``TaskManager`` 实例并返回。
+
+.. note::
+
+ 关于 Rust 迭代器语法如 ``iter_mut/(a..b)`` ,及其方法如 ``enumerate/map/find/take``,请参考 Rust 官方文档。
+
+实现 sys_yield 和 sys_exit
+----------------------------------------------------------------------------
+
+``sys_yield`` 的实现用到了 ``task`` 子模块提供的 ``suspend_current_and_run_next`` 接口,这个接口如字面含义,就是暂停当前的应用并切换到下个应用。
+
+.. code-block:: rust
+
+ // os/src/syscall/process.rs
+
+ use crate::task::suspend_current_and_run_next;
+
+ pub fn sys_yield() -> isize {
+ suspend_current_and_run_next();
+ 0
+ }
+
+``sys_exit`` 基于 ``task`` 子模块提供的 ``exit_current_and_run_next`` 接口,它的含义是退出当前的应用并切换到下个应用:
+
+.. code-block:: rust
+
+ // os/src/syscall/process.rs
+
+ use crate::task::exit_current_and_run_next;
+
+ pub fn sys_exit(exit_code: i32) -> ! {
+ println!("[kernel] Application exited with code {}", exit_code);
+ exit_current_and_run_next();
+ panic!("Unreachable in sys_exit!");
+ }
+
+那么 ``suspend_current_and_run_next`` 和 ``exit_current_and_run_next`` 各是如何实现的呢?
+
+.. code-block:: rust
+
+ // os/src/task/mod.rs
+
+ pub fn suspend_current_and_run_next() {
+ TASK_MANAGER.mark_current_suspended();
+ TASK_MANAGER.run_next_task();
+ }
+
+ pub fn exit_current_and_run_next() {
+ TASK_MANAGER.mark_current_exited();
+ TASK_MANAGER.run_next_task();
+ }
+
+
+它们都是先修改当前应用的运行状态,然后尝试切换到下一个应用。修改运行状态比较简单,实现如下:
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/task/mod.rs
+
+ impl TaskManager {
+ fn mark_current_suspended(&self) {
+ let mut inner = self.inner.exclusive_access();
+ let current = inner.current_task;
+ inner.tasks[current].task_status = TaskStatus::Ready;
+ }
+ }
+
+以 ``mark_current_suspended`` 为例。首先获得里层 ``TaskManagerInner`` 的可变引用,然后修改任务控制块数组 ``tasks`` 中当前任务的状态。
+
+再看 ``run_next_task`` 的实现:
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/task/mod.rs
+
+ impl TaskManager {
+ fn run_next_task(&self) {
+ if let Some(next) = self.find_next_task() {
+ let mut inner = self.inner.exclusive_access();
+ let current = inner.current_task;
+ inner.tasks[next].task_status = TaskStatus::Running;
+ inner.current_task = next;
+ let current_task_cx_ptr = &mut inner.tasks[current].task_cx as *mut TaskContext;
+ let next_task_cx_ptr = &inner.tasks[next].task_cx as *const TaskContext;
+ drop(inner);
+ // before this, we should drop local variables that must be dropped manually
+ unsafe {
+ __switch(current_task_cx_ptr, next_task_cx_ptr);
+ }
+ // go back to user mode
+ } else {
+ panic!("All applications completed!");
+ }
+ }
+
+ fn find_next_task(&self) -> Option {
+ let inner = self.inner.exclusive_access();
+ let current = inner.current_task;
+ (current + 1..current + self.num_app + 1)
+ .map(|id| id % self.num_app)
+ .find(|id| inner.tasks[*id].task_status == TaskStatus::Ready)
+ }
+ }
+
+``run_next_task`` 会调用 ``find_next_task`` 方法尝试寻找一个运行状态为 ``Ready`` 的应用并获得其 ID 。
+如果找不到, 说明所有应用都执行完了, ``find_next_task`` 将返回 ``None`` ,内核 panic 退出。
+如果能够找到下一个可运行应用,我们就调用 ``__switch`` 切换任务。
+
+切换任务之前,我们要手动 drop 掉我们获取到的 ``TaskManagerInner`` 可变引用。
+因为函数还没有返回, ``inner`` 不会自动销毁。我们只有令 ``TASK_MANAGER`` 的 ``inner`` 字段回到未被借用的状态,下次任务切换时才能再借用。
+
+我们可以总结一下应用的运行状态变化图:
+
+.. image:: fsm-coop.png
+
+第一次进入用户态
+------------------------------------------
+
+我们在第二章中介绍过 CPU 第一次从内核态进入用户态的方法,只需在内核栈上压入构造好的 Trap 上下文,
+然后 ``__restore`` 即可。本章要在此基础上做一些扩展。
+
+在初始化任务控制块时,我们是这样做的:
+
+.. code-block:: rust
+
+ // os/src/task/mod.rs
+
+ for (i, t) in tasks.iter_mut().enumerate().take(num_app) {
+ t.task_cx = TaskContext::goto_restore(init_app_cx(i));
+ t.task_status = TaskStatus::Ready;
+ }
+
+``init_app_cx`` 在 ``loader`` 子模块中定义,它向内核栈压入了一个 Trap 上下文,并返回压入 Trap 上下文后 ``sp`` 的值。
+这个 Trap 上下文的构造方式与第二章相同。
+
+``goto_restore`` 保存传入的 ``sp``,并将 ``ra`` 设置为 ``__restore`` 的入口地址,构造任务上下文后返回。这样,任务管理器中各个应用的任务上下文就得到了初始化。
+
+.. code-block:: rust
+
+ // os/src/task/context.rs
+
+ impl TaskContext {
+ pub fn goto_restore(kstack_ptr: usize) -> Self {
+ extern "C" { fn __restore(); }
+ Self {
+ ra: __restore as usize,
+ sp: kstack_ptr,
+ s: [0; 12],
+ }
+ }
+ }
+
+在 ``rust_main`` 中我们调用 ``task::run_first_task`` 来执行第一个应用:
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/task/mod.rs
+
+ fn run_first_task(&self) -> ! {
+ let mut inner = self.inner.exclusive_access();
+ let task0 = &mut inner.tasks[0];
+ task0.task_status = TaskStatus::Running;
+ let next_task_cx_ptr = &task0.task_cx as *const TaskContext;
+ drop(inner);
+ let mut _unused = TaskContext::zero_init();
+ // before this, we should drop local variables that must be dropped manually
+ unsafe {
+ __switch(&mut _unused as *mut TaskContext, next_task_cx_ptr);
+ }
+ panic!("unreachable in run_first_task!");
+ }
+
+我们显式声明了一个 ``_unused`` 变量,并将它的地址作为第一个参数传给 ``__switch`` ,
+声明此变量的意义仅仅是为了避免其他数据被覆盖。
+
+在 ``__switch`` 中恢复 ``sp`` 后, ``sp`` 将指向 ``init_app_cx`` 构造的 Trap 上下文,后面就回到第二章的情况了。
+此外, ``__restore`` 的实现需要做出变化:它 **不再需要** 在开头 ``mv sp, a0`` 了。因为在 ``__switch`` 之后,``sp`` 就已经正确指向了我们需要的 Trap 上下文地址。
\ No newline at end of file
diff --git a/guide/source/chapter3/4time-sharing-system.rst b/guide/source/chapter3/4time-sharing-system.rst
new file mode 100644
index 0000000..9038da2
--- /dev/null
+++ b/guide/source/chapter3/4time-sharing-system.rst
@@ -0,0 +1,161 @@
+分时多任务系统
+===========================================================
+
+
+现代的任务调度算法基本都是抢占式的,它要求每个应用只能连续执行一段时间,然后内核就会将它强制性切换出去。
+一般将 **时间片** (Time Slice) 作为应用连续执行时长的度量单位,每个时间片可能在毫秒量级。
+简单起见,我们使用 **时间片轮转算法** (RR, Round-Robin) 来对应用进行调度。
+
+
+时钟中断与计时器
+------------------------------------------------------------------
+
+实现调度算法需要计时。RISC-V 要求处理器维护时钟计数器 ``mtime``,还有另外一个 CSR ``mtimecmp`` 。
+一旦计数器 ``mtime`` 的值超过了 ``mtimecmp``,就会触发一次时钟中断。
+
+运行在 M 特权级的 SEE 已经预留了相应的接口,基于此编写的 ``get_time`` 函数可以取得当前 ``mtime`` 计数器的值;
+
+.. code-block:: rust
+
+ // os/src/timer.rs
+
+ use riscv::register::time;
+
+ pub fn get_time() -> usize {
+ time::read()
+ }
+
+在 10 ms 后设置时钟中断的代码如下:
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/sbi.rs
+
+ const SBI_SET_TIMER: usize = 0;
+
+ pub fn set_timer(timer: usize) {
+ sbi_call(SBI_SET_TIMER, timer, 0, 0);
+ }
+
+ // os/src/timer.rs
+
+ use crate::config::CLOCK_FREQ;
+ const TICKS_PER_SEC: usize = 100;
+
+ pub fn set_next_trigger() {
+ set_timer(get_time() + CLOCK_FREQ / TICKS_PER_SEC);
+ }
+
+- 第 5 行, ``sbi`` 子模块有一个 ``set_timer`` 调用,用来设置 ``mtimecmp`` 的值。
+- 第 14 行, ``timer`` 子模块的 ``set_next_trigger`` 函数对 ``set_timer`` 进行了封装,
+ 它首先读取当前 ``mtime`` 的值,然后计算出 10ms 之内计数器的增量,再将 ``mtimecmp`` 设置为二者的和。
+ 这样,10ms 之后一个 S 特权级时钟中断就会被触发。
+
+ 至于增量的计算方式, ``CLOCK_FREQ`` 是一个预先获取到的各平台不同的时钟频率,单位为赫兹,也就是一秒钟之内计数器的增量。
+ 它可以在 ``config`` 子模块中找到。10ms 的话只需除以常数 ``TICKS_PER_SEC`` 也就是 100 即可。
+
+后面可能还有一些计时的需求,我们再设计一个函数:
+
+.. code-block:: rust
+
+ // os/src/timer.rs
+
+ const MICRO_PER_SEC: usize = 1_000_000;
+
+ pub fn get_time_us() -> usize {
+ time::read() / (CLOCK_FREQ / MICRO_PER_SEC)
+ }
+
+
+``timer`` 子模块的 ``get_time_us`` 可以以微秒为单位返回当前计数器的值。
+
+新增一个系统调用,使应用能获取当前的时间:
+
+.. code-block:: rust
+ :caption: 第三章新增系统调用(二)
+
+ /// 功能:获取当前的时间,保存在 TimeVal 结构体 ts 中,_tz 在我们的实现中忽略
+ /// 返回值:返回是否执行成功,成功则返回 0
+ /// syscall ID:169
+ fn sys_get_time(ts: *mut TimeVal, _tz: usize) -> isize;
+
+结构体 ``TimeVal`` 的定义如下,内核只需调用 ``get_time_us`` 即可实现该系统调用。
+
+.. code-block:: rust
+
+ // os/src/syscall/process.rs
+
+ #[repr(C)]
+ pub struct TimeVal {
+ pub sec: usize,
+ pub usec: usize,
+ }
+
+RISC-V 架构中的嵌套中断问题
+-----------------------------------
+
+默认情况下,当 Trap 进入某个特权级之后,在 Trap 处理的过程中同特权级的中断都会被屏蔽。
+
+- 当 Trap 发生时,``sstatus.sie`` 会被保存在 ``sstatus.spie`` 字段中,同时 ``sstatus.sie`` 置零,
+ 这也就在 Trap 处理的过程中屏蔽了所有 S 特权级的中断;
+- 当 Trap 处理完毕 ``sret`` 的时候, ``sstatus.sie`` 会恢复到 ``sstatus.spie`` 内的值。
+
+也就是说,如果不去手动设置 ``sstatus`` CSR ,在只考虑 S 特权级中断的情况下,是不会出现 **嵌套中断** (Nested Interrupt) 的。
+
+.. note::
+
+ **嵌套中断与嵌套 Trap**
+
+ 嵌套中断可以分为两部分:在处理一个中断的过程中又被同特权级/高特权级中断所打断。默认情况下硬件会避免前一部分,
+ 也可以通过手动设置来允许前一部分的存在;而从上面介绍的规则可以知道,后一部分则是无论如何设置都不可避免的。
+
+ 嵌套 Trap 则是指处理一个 Trap 过程中又再次发生 Trap ,嵌套中断算是嵌套 Trap 的一种。
+
+
+抢占式调度
+-----------------------------------
+
+有了时钟中断和计时器,抢占式调度就很容易实现了:
+
+.. code-block:: rust
+
+ // os/src/trap/mod.rs
+
+ match scause.cause() {
+ Trap::Interrupt(Interrupt::SupervisorTimer) => {
+ set_next_trigger();
+ suspend_current_and_run_next();
+ }
+ }
+
+我们只需在 ``trap_handler`` 函数下新增一个分支,触发了 S 特权级时钟中断时,重新设置计时器,
+调用 ``suspend_current_and_run_next`` 函数暂停当前应用并切换到下一个。
+
+为了避免 S 特权级时钟中断被屏蔽,我们需要在执行第一个应用前调用 ``enable_timer_interrupt()`` 设置 ``sie.stie``,
+使得 S 特权级时钟中断不会被屏蔽;再设置第一个 10ms 的计时器。
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/main.rs
+
+ #[no_mangle]
+ pub fn rust_main() -> ! {
+ // ...
+ trap::enable_timer_interrupt();
+ timer::set_next_trigger();
+ // ...
+ }
+
+ // os/src/trap/mod.rs
+
+ use riscv::register::sie;
+
+ pub fn enable_timer_interrupt() {
+ unsafe { sie::set_stimer(); }
+ }
+
+就这样,我们实现了时间片轮转任务调度算法。 ``power`` 系列用户程序可以验证我们取得的成果:这些应用并没有主动 yield,
+内核仍能公平地把时间片分配给它们。
+
diff --git a/guide/source/chapter3/5exercise.rst b/guide/source/chapter3/5exercise.rst
new file mode 100644
index 0000000..422fbbd
--- /dev/null
+++ b/guide/source/chapter3/5exercise.rst
@@ -0,0 +1,133 @@
+chapter3练习
+=======================================
+
+编程作业
+--------------------------------------
+
+获取任务信息
+++++++++++++++++++++++++++
+
+ch3 中,我们的系统已经能够支持多个任务分时轮流运行,我们希望引入一个新的系统调用 ``sys_task_info`` 以获取当前任务的信息,定义如下:
+
+.. code-block:: rust
+
+ fn sys_task_info(ti: *mut TaskInfo) -> isize
+
+- syscall ID: 410
+- 查询当前正在执行的任务信息,任务信息包括任务控制块相关信息(任务状态)、任务使用的系统调用及调用次数、任务总运行时长(单位ms)。
+
+.. code-block:: rust
+
+ struct TaskInfo {
+ status: TaskStatus,
+ syscall_times: [u32; MAX_SYSCALL_NUM],
+ time: usize
+ }
+
+- 参数:
+ - ti: 待查询任务信息
+- 返回值:执行成功返回0,错误返回-1
+- 说明:
+ - 相关结构已在框架中给出,只需添加逻辑实现功能需求即可。
+ - 在我们的实验中,系统调用号一定小于 500,所以直接使用一个长为 ``MAX_SYSCALL_NUM=500`` 的数组做桶计数。
+ - 运行时间 time 返回系统调用时刻距离任务第一次被调度时刻的时长,也就是说这个时长可能包含该任务被其他任务抢占后的等待重新调度的时间。
+ - 由于查询的是当前任务的状态,因此 TaskStatus 一定是 Running。(助教起初想设计根据任务 id 查询,但是既不好定义任务 id 也不好写测例,遂放弃 QAQ)
+ - 调用 ``sys_task_info`` 也会对本次调用计数。
+- 提示:
+ - 大胆修改已有框架!除了配置文件,你几乎可以随意修改已有框架的内容。
+ - 程序运行时间可以通过调用 ``get_time()`` 获取,注意任务运行总时长的单位是 ms。
+ - 系统调用次数可以考虑在进入内核态系统调用异常处理函数之后,进入具体系统调用函数之前维护。
+ - 阅读 TaskManager 的实现,思考如何维护内核控制块信息(可以在控制块可变部分加入需要的信息)。
+
+
+实验要求
++++++++++++++++++++++++++++++++++++++++++
+
+- 完成分支: ch3。
+
+- 实验目录要求
+
+.. code-block::
+
+ ├── os(内核实现)
+ │ ├── Cargo.toml(配置文件)
+ │ └── src(所有内核的源代码放在 os/src 目录下)
+ │ ├── main.rs(内核主函数)
+ │ └── ...
+ ├── reports (不是 report)
+ │ ├── lab1.md/pdf
+ │ └── ...
+ ├── ...
+
+
+- 通过所有测例:
+
+ CI 使用的测例与本地相同,测试中,user 文件夹及其它与构建相关的文件将被替换,请不要试图依靠硬编码通过测试。
+
+ 默认情况下,makefile 仅编译基础测例 (``BASE=1``),即无需修改框架即可正常运行的测例。
+ 你需要在编译时指定 ``BASE=0`` 控制框架仅编译实验测例(在 os 目录执行 ``make run BASE=0``),
+ 或指定 ``BASE=2`` 控制框架同时编译基础测例和实验测例。
+
+.. note::
+
+ 你的实现只需且必须通过测例,建议读者感到困惑时先检查测例。
+
+
+简答作业
+--------------------------------------------
+
+1. 正确进入 U 态后,程序的特征还应有:使用 S 态特权指令,访问 S 态寄存器后会报错。
+ 请同学们可以自行测试这些内容 (运行 `Rust 三个 bad 测例 (ch2b_bad_*.rs) `_ ,
+ 注意在编译时至少需要指定 ``LOG=ERROR`` 才能观察到内核的报错信息) ,
+ 描述程序出错行为,同时注意注明你使用的 sbi 及其版本。
+
+2. 深入理解 `trap.S `_
+ 中两个函数 ``__alltraps`` 和 ``__restore`` 的作用,并回答如下问题:
+
+ 1. L40:刚进入 ``__restore`` 时,``a0`` 代表了什么值。请指出 ``__restore`` 的两种使用情景。
+
+ 2. L46-L51:这几行汇编代码特殊处理了哪些寄存器?这些寄存器的的值对于进入用户态有何意义?请分别解释。
+
+ .. code-block:: riscv
+
+ ld t0, 32*8(sp)
+ ld t1, 33*8(sp)
+ ld t2, 2*8(sp)
+ csrw sstatus, t0
+ csrw sepc, t1
+ csrw sscratch, t2
+
+ 3. L53-L59:为何跳过了 ``x2`` 和 ``x4``?
+
+ .. code-block:: riscv
+
+ ld x1, 1*8(sp)
+ ld x3, 3*8(sp)
+ .set n, 5
+ .rept 27
+ LOAD_GP %n
+ .set n, n+1
+ .endr
+
+ 4. L63:该指令之后,``sp`` 和 ``sscratch`` 中的值分别有什么意义?
+
+ .. code-block:: riscv
+
+ csrrw sp, sscratch, sp
+
+ 5. ``__restore``:中发生状态切换在哪一条指令?为何该指令执行之后会进入用户态?
+
+ 6. L13:该指令之后,``sp`` 和 ``sscratch`` 中的值分别有什么意义?
+
+ .. code-block:: riscv
+
+ csrrw sp, sscratch, sp
+
+ 7. 从 U 态进入 S 态是哪一条指令发生的?
+
+报告要求
+-------------------------------
+
+- 简单总结你实现的功能(200字以内,不要贴代码)。
+- 完成问答题。
+- (optional) 你对本次实验设计及难度/工作量的看法,以及有哪些需要改进的地方,欢迎畅所欲言。
diff --git a/guide/source/chapter3/fsm-coop.png b/guide/source/chapter3/fsm-coop.png
new file mode 100644
index 0000000..bc633bc
Binary files /dev/null and b/guide/source/chapter3/fsm-coop.png differ
diff --git a/guide/source/chapter3/index.rst b/guide/source/chapter3/index.rst
new file mode 100644
index 0000000..d71d5a5
--- /dev/null
+++ b/guide/source/chapter3/index.rst
@@ -0,0 +1,14 @@
+.. _link-chapter3:
+
+第三章:多道程序与分时多任务
+==============================================
+
+.. toctree::
+ :maxdepth: 4
+
+ 0intro
+ 1multi-loader
+ 2task-switching
+ 3multiprogramming
+ 4time-sharing-system
+ 5exercise
diff --git a/guide/source/chapter3/multiprogramming.png b/guide/source/chapter3/multiprogramming.png
new file mode 100644
index 0000000..7e4df49
Binary files /dev/null and b/guide/source/chapter3/multiprogramming.png differ
diff --git a/guide/source/chapter4/0intro.rst b/guide/source/chapter4/0intro.rst
new file mode 100644
index 0000000..9334ff2
--- /dev/null
+++ b/guide/source/chapter4/0intro.rst
@@ -0,0 +1,143 @@
+引言
+==============================
+
+本章导读
+-------------------------------
+
+本章中内核将实现虚拟内存机制,这注定是一趟艰难的旅程。
+
+
+实践体验
+-----------------------
+
+本章应用运行起来效果与上一章基本一致。
+
+获取本章代码:
+
+.. code-block:: console
+
+ $ git clone https://github.com/LearningOS/rCore-Tutorial-Code-2022S.git
+ $ cd rCore-Tutorial-Code-2022S
+ $ git checkout ch4
+ $ git clone https://github.com/LearningOS/rCore-Tutorial-Test-2022S.git user
+
+或许你之前已经克隆过了仓库,只希望从远程仓库更新,而非再克隆一次:
+
+.. code-block:: console
+
+ $ cd rCore-Tutorial-Code-2022S
+ # 你可以将 upstream 改为你喜欢的名字
+ $ git remote add upstream https://github.com/LearningOS/rCore-Tutorial-Code-2022S.git
+ # 更新仓库信息
+ $ git fetch upstream
+ # 查看已添加的远程仓库;应该能看到已有一个 origin 和新添加的 upstream 仓库
+ $ git remote -v
+ # 根据需求选择以下一种操作即可
+ # 在本地新建一个与远程仓库对应的分支:
+ $ git checkout -b ch4 upstream/ch4
+ # 本地已有分支,从远程仓库更新:
+ $ git checkout ch4
+ $ git merge upstream/ch4
+ # 将更新推送到自己的远程仓库
+ $ git push origin ch4
+
+在 qemu 模拟器上运行本章代码:
+
+.. code-block:: console
+
+ $ cd os
+ $ make run
+
+
+本章代码树
+-----------------------------------------------------
+
+.. code-block::
+ :linenos:
+
+ ├── os
+ │ ├── ...
+ │ └── src
+ │ ├── ...
+ │ ├── config.rs(修改:新增一些内存管理的相关配置)
+ │ ├── linker.ld(修改:将跳板页引入内存布局)
+ │ ├── loader.rs(修改:仅保留获取应用数量和数据的功能)
+ │ ├── main.rs(修改)
+ │ ├── mm(新增:内存管理的 mm 子模块)
+ │ │ ├── address.rs(物理/虚拟 地址/页号的 Rust 抽象)
+ │ │ ├── frame_allocator.rs(物理页帧分配器)
+ │ │ ├── heap_allocator.rs(内核动态内存分配器)
+ │ │ ├── memory_set.rs(引入地址空间 MemorySet 及逻辑段 MemoryArea 等)
+ │ │ ├── mod.rs(定义了 mm 模块初始化方法 init)
+ │ │ └── page_table.rs(多级页表抽象 PageTable 以及其他内容)
+ │ ├── syscall
+ │ │ ├── fs.rs(修改:基于地址空间的 sys_write 实现)
+ │ │ ├── mod.rs
+ │ │ └── process.rs
+ │ ├── task
+ │ │ ├── context.rs(修改:构造一个跳转到不同位置的初始任务上下文)
+ │ │ ├── mod.rs(修改,详见文档)
+ │ │ ├── switch.rs
+ │ │ ├── switch.S
+ │ │ └── task.rs(修改,详见文档)
+ │ └── trap
+ │ ├── context.rs(修改:在 Trap 上下文中加入了更多内容)
+ │ ├── mod.rs(修改:基于地址空间修改了 Trap 机制,详见文档)
+ │ └── trap.S(修改:基于地址空间修改了 Trap 上下文保存与恢复汇编代码)
+ └── user
+ ├── build.py(编译时不再使用)
+ ├── ...
+ └── src
+ ├── linker.ld(修改:将所有应用放在各自地址空间中固定的位置)
+ └── ...
+
+ cloc os
+ -------------------------------------------------------------------------------
+ Language files blank comment code
+ -------------------------------------------------------------------------------
+ Rust 26 138 56 1526
+ Assembly 3 3 26 86
+ make 1 11 4 36
+ TOML 1 2 1 13
+ -------------------------------------------------------------------------------
+ SUM: 31 154 87 1661
+ -------------------------------------------------------------------------------
+
+
+.. 本章代码导读
+.. -----------------------------------------------------
+
+.. 本章涉及的代码量相对多了起来,也许同学们不知如何从哪里看起或从哪里开始尝试实验。这里简要介绍一下“头甲龙”操作系统的大致开发过程。
+
+.. 我们先从简单的地方入手,那当然就是先改进应用程序了。具体而言,主要就是把 ``linker.ld`` 中应用程序的起始地址都改为 ``0x0`` ,这是假定我们操作系统能够通过分页机制把不同应用的相同虚地址映射到不同的物理地址中。这样我们写应用就不用考虑物理地址布局的问题,能够以一种更加统一的方式编写应用程序,可以忽略掉一些不必要的细节。
+
+.. 为了能够在内核中动态分配内存,我们的第二步需要在内核增加连续内存分配的功能,具体实现主要集中在 ``os/src/mm/heap_allocator.rs`` 中。完成这一步后,我们就可以在内核中用到Rust的堆数据结构了,如 ``Vec`` 、 ``Box`` 等,这样内核编程就更加灵活了。
+
+.. 操作系统如果要建立页表,首先要能管理整个系统的物理内存,这就需要知道物理内存哪些区域放置内核的代码、数据,哪些区域则是空闲的等信息。所以需要了解整个系统的物理内存空间的范围,并以物理页帧为单位分配和回收物理内存,具体实现主要集中在 ``os/src/mm/frame_allocator.rs`` 中。
+
+.. 页表中的页表项的索引其实是虚拟地址中的虚拟页号,页表项的重要内容是物理地址的物理页帧号。为了能够灵活地在虚拟地址、物理地址、虚拟页号、物理页号之间进行各种转换,在 ``os/src/mm/address.rs`` 中实现了各种转换函数。
+
+.. 完成上述工作后,基本上就做好了建立页表的前期准备。我们就可以开始建立页表,这主要涉及到页表项的数据结构表示,以及多级页表的起始物理页帧位置和整个所占用的物理页帧的记录。具体实现主要集中在 ``os/src/mm/page_table.rs`` 中。
+
+.. 一旦使能分页机制,那么内核中也将基于虚地址进行虚存访问,所以在给应用添加虚拟地址空间前,内核自己也会建立一个页表,把整个物理地址空间通过简单的恒等映射对应到一个虚拟地址空间中。后续的应用在执行前,也需要建立一个虚拟地址空间,这意味着第三章的 ``task`` 将进化到第五章的拥有独立页表的进程 。虚拟地址空间需要有一个数据结构管理起来,这就是 ``MemorySet`` ,即地址空间这个抽象概念所对应的具象体现。在一个虚拟地址空间中,有代码段,数据段等不同属性且不一定连续的子空间,它们通过一个重要的数据结构 ``MapArea`` 来表示和管理。围绕 ``MemorySet`` 等一系列的数据结构和相关操作的实现,主要集中在 ``os/src/mm/memory_set.rs`` 中。比如内核的页表和虚拟空间的建立在如下代码中:
+
+.. .. code-block:: rust
+.. :linenos:
+
+.. // os/src/mm/memory_set.rs
+
+.. lazy_static! {
+.. pub static ref KERNEL_SPACE: Arc> = Arc::new(Mutex::new(
+.. MemorySet::new_kernel()
+.. ));
+.. }
+
+.. 完成到这里,我们就可以使能分页机制了。且我们应该有更加方便的机制来给支持应用运行。在本章之前,都是把应用程序的所有元数据丢弃从而转换成二进制格式来执行,这其实把编译器生成的 ELF 执行文件中大量有用的信息给去掉了,比如代码段、数据段的各种属性,程序的入口地址等。既然有了给应用运行提供虚拟地址空间的能力,我们就可以利用 ELF 执行文件中的各种信息来灵活构建应用运行所需要的虚拟地址空间。在 ``os/src/loader.rs`` 中可以看到如何获取一个应用的 ELF 执行文件数据,而在 ``os/src/mm/memory_set`` 中的 ``MemorySet::from_elf`` 可以看到如何通过解析 ELF 来创建一个应用地址空间。
+
+.. 对于有了虚拟地址空间的 *任务* ,我们可以把它叫做 *进程* 了。操作系统为此需要扩展任务控制块 ``TaskControlBlock`` 的管理范围,使得操作系统能管理拥有独立页表和虚拟地址空间的应用程序的运行。相关主要的改动集中在 ``os/src/task/task.rs`` 中。
+
+.. 由于代表应用程序运行的进程和管理应用的操作系统各自有独立的页表和虚拟地址空间,所以这就出现了两个比较挑战的事情。一个是由于系统调用、中断或异常导致的应用程序和操作系统之间的 Trap 上下文切换不像以前那么简单了,因为需要切换页表,这需要看看 ``os/src/trap/trap.S`` ;还有就是需要对来自用户态和内核态的 Trap 分别进行处理,这需要看看 ``os/src/trap/mod.rs`` 和 :ref:`跳板的实现 ` 中的讲解。
+
+.. 另外一个挑战是,在内核地址空间中执行的内核代码常常需要读写应用地址空间的数据,这无法简单的通过一次访存交给 MMU 来解决,而是需要手动查应用地址空间的页表。在访问应用地址空间中的一块跨多个页数据的时候还需要注意处理边界条件。可以参考 ``os/src/syscall/fs.rs``、 ``os/src/mm/page_table.rs`` 中的 ``translated_byte_buffer`` 函数的实现。
+
+.. 实现到这,应该就可以给应用程序运行提供一个方便且安全的虚拟地址空间了。
\ No newline at end of file
diff --git a/guide/source/chapter4/3sv39-implementation-1.rst b/guide/source/chapter4/3sv39-implementation-1.rst
new file mode 100644
index 0000000..79f6965
--- /dev/null
+++ b/guide/source/chapter4/3sv39-implementation-1.rst
@@ -0,0 +1,216 @@
+实现 SV39 多级页表机制(上)
+========================================================
+
+.. note::
+
+ 背景知识: `地址空间 `_
+
+ 背景知识: `SV39 多级页表原理 `_
+
+
+我们将在内核实现 RV64 架构 SV39 分页机制。由于内容过多,分成两个小节。
+
+虚拟地址和物理地址
+------------------------------------------------------
+
+内存控制相关的CSR寄存器
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+默认情况下 MMU 未被使能,此时无论 CPU 处于哪个特权级,访存的地址都将直接被视作物理地址。
+可以通过修改 S 特权级的 ``satp`` CSR 来启用分页模式,此后 S 和 U 特权级的访存地址会被视为虚拟地址,经过 MMU 的地址转换获得对应物理地址,再通过它来访问物理内存。
+
+.. image:: satp.png
+ :name: satp-layout
+
+上图是 RV64 架构下 ``satp`` 的字段分布。当 ``MODE`` 设置为 0 的时候,所有访存都被视为物理地址;而设置为 8
+时,SV39 分页机制被启用,所有 S/U 特权级的访存被视为一个 39 位的虚拟地址,MMU 会将其转换成 56 位的物理地址;如果转换失败,则会触发异常。
+
+
+地址格式与组成
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. image:: sv39-va-pa.png
+
+我们采用分页管理,单个页面的大小设置为 :math:`4\text{KiB}` ,每个虚拟页面和物理页帧都按 4 KB 对齐。
+:math:`4\text{KiB}` 需要用 12 位字节地址来表示,因此虚拟地址和物理地址都被分成两部分:
+它们的低 12 位被称为 **页内偏移** (Page Offset) 。虚拟地址的高 27 位,即 :math:`[38:12]` 为它的虚拟页号 VPN;
+物理地址的高 44 位,即 :math:`[55:12]` 为它的物理页号 PPN。页号可以用来定位一个虚拟/物理地址属于哪一个虚拟页面/物理页帧。
+
+地址转换是以页为单位进行的,转换前后地址页内偏移部分不变。MMU 只是从虚拟地址中取出 27 位虚拟页号,
+在页表中查到其对应的物理页号,如果找到,就将得到的 44 位的物理页号与 12 位页内偏移拼接到一起,形成 56 位物理地址。
+
+.. note::
+
+ **RV64 架构中虚拟地址为何只有 39 位?**
+
+ 虚拟地址长度确实应该和位宽一致为 64 位,但是在启用 SV39 分页模式下,只有低 39 位是真正有意义的。
+ SV39 分页模式规定 64 位虚拟地址的 :math:`[63:39]` 这 25 位必须和第 38 位相同,否则 MMU 会直接认定它是一个
+ 不合法的虚拟地址。。
+
+ 也就是说,所有 :math:`2^{64}` 个虚拟地址中,只有最低的 :math:`256\text{GiB}` (当第 38 位为 0 时)
+ 以及最高的 :math:`256\text{GiB}` (当第 38 位为 1 时)是可能通过 MMU 检查的。
+
+地址相关的数据结构抽象与类型定义
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+实现页表之前,先将地址和页号的概念抽象为 Rust 中的类型。
+
+首先是这些类型的定义:
+
+.. code-block:: rust
+
+ // os/src/mm/address.rs
+
+ #[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
+ pub struct PhysAddr(pub usize);
+
+ #[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
+ pub struct VirtAddr(pub usize);
+
+ #[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
+ pub struct PhysPageNum(pub usize);
+
+ #[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
+ pub struct VirtPageNum(pub usize);
+
+.. _term-type-convertion:
+
+上面分别给出了物理地址、虚拟地址、物理页号、虚拟页号的 Rust 类型声明,它们都是 usize 的一种简单包装。
+将它们各自抽象出来而不是直接使用 usize,是为了在 Rust 编译器的帮助下进行多种方便且安全的 **类型转换** (Type Convertion) 。
+
+这些类型本身可以和 usize 之间互相转换,地址和页号之间也可以相互转换。以物理地址和物理页号之间的转换为例:
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/mm/address.rs
+
+ impl PhysAddr {
+ pub fn page_offset(&self) -> usize { self.0 & (PAGE_SIZE - 1) }
+ }
+
+ impl From for PhysPageNum {
+ fn from(v: PhysAddr) -> Self {
+ assert_eq!(v.page_offset(), 0);
+ v.floor()
+ }
+ }
+
+ impl From for PhysAddr {
+ fn from(v: PhysPageNum) -> Self { Self(v.0 << PAGE_SIZE_BITS) }
+ }
+
+其中 ``PAGE_SIZE`` 为 :math:`4096` , ``PAGE_SIZE_BITS`` 为 :math:`12` ,它们均定义在 ``config`` 子模块
+中,分别表示每个页面的大小和页内偏移的位宽。从物理页号到物理地址的转换只需左移 :math:`12` 位即可,但是物理地址需要
+保证它与页面大小对齐才能通过右移转换为物理页号。
+
+对于不对齐的情况,物理地址不能通过 ``From/Into`` 转换为物理页号,而是需要通过它自己的 ``floor`` 或 ``ceil`` 方法来
+进行下取整或上取整的转换。
+
+.. code-block:: rust
+
+ // os/src/mm/address.rs
+
+ impl PhysAddr {
+ pub fn floor(&self) -> PhysPageNum { PhysPageNum(self.0 / PAGE_SIZE) }
+ pub fn ceil(&self) -> PhysPageNum { PhysPageNum((self.0 + PAGE_SIZE - 1) / PAGE_SIZE) }
+ }
+
+页表项的数据结构抽象与类型定义
+-----------------------------------------
+
+.. image:: sv39-pte.png
+
+上图为 SV39 分页模式下的页表项,其中 :math:`[53:10]` 这 :math:`44` 位是物理页号,最低的 :math:`8` 位
+:math:`[7:0]` 则是标志位,它们的含义如下:
+
+- 仅当 V(Valid) 位为 1 时,页表项才是合法的;
+- R/W/X 分别控制索引到这个页表项的对应虚拟页面是否允许读/写/取指;
+- U 控制索引到这个页表项的对应虚拟页面是否在 CPU 处于 U 特权级的情况下是否被允许访问;
+- G 我们不理会;
+- A(Accessed) 记录自从页表项上的这一位被清零之后,页表项的对应虚拟页面是否被访问过;
+- D(Dirty) 则记录自从页表项上的这一位被清零之后,页表项的对应虚拟页表是否被修改过。
+
+先来实现页表项中的标志位 ``PTEFlags`` :
+
+.. code-block:: rust
+
+ // os/src/main.rs
+
+ #[macro_use]
+ extern crate bitflags;
+
+ // os/src/mm/page_table.rs
+
+ use bitflags::*;
+
+ bitflags! {
+ pub struct PTEFlags: u8 {
+ const V = 1 << 0;
+ const R = 1 << 1;
+ const W = 1 << 2;
+ const X = 1 << 3;
+ const U = 1 << 4;
+ const G = 1 << 5;
+ const A = 1 << 6;
+ const D = 1 << 7;
+ }
+ }
+
+`bitflags `_ 是一个 Rust 中常用来比特标志位的 crate 。它提供了
+一个 ``bitflags!`` 宏,如上面的代码段所展示的那样,可以将一个 ``u8`` 封装成一个标志位的集合类型,支持一些常见的集合
+运算。
+
+接下来我们实现页表项 ``PageTableEntry`` :
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/mm/page_table.rs
+
+ #[derive(Copy, Clone)]
+ #[repr(C)]
+ pub struct PageTableEntry {
+ pub bits: usize,
+ }
+
+ impl PageTableEntry {
+ pub fn new(ppn: PhysPageNum, flags: PTEFlags) -> Self {
+ PageTableEntry {
+ bits: ppn.0 << 10 | flags.bits as usize,
+ }
+ }
+ pub fn empty() -> Self {
+ PageTableEntry {
+ bits: 0,
+ }
+ }
+ pub fn ppn(&self) -> PhysPageNum {
+ (self.bits >> 10 & ((1usize << 44) - 1)).into()
+ }
+ pub fn flags(&self) -> PTEFlags {
+ PTEFlags::from_bits(self.bits as u8).unwrap()
+ }
+ }
+
+- 第 3 行我们让编译器自动为 ``PageTableEntry`` 实现 ``Copy/Clone`` Trait,来让这个类型以值语义赋值/传参的时候
+ 不会发生所有权转移,而是拷贝一份新的副本。
+- 第 10 行使得我们可以从一个物理页号 ``PhysPageNum`` 和一个页表项标志位 ``PTEFlags`` 生成一个页表项
+ ``PageTableEntry`` 实例;而第 20 行和第 23 行则分别可以从一个页表项将它们两个取出。
+- 第 15 行中,我们也可以通过 ``empty`` 方法生成一个全零的页表项,注意这隐含着该页表项的 V 标志位为 0 ,
+ 因此它是不合法的。
+
+后面我们还为 ``PageTableEntry`` 实现了一些辅助函数(Helper Function),可以快速判断一个页表项的 V/R/W/X 标志位是否为 1,以 V
+标志位的判断为例:
+
+.. code-block:: rust
+
+ // os/src/mm/page_table.rs
+
+ impl PageTableEntry {
+ pub fn is_valid(&self) -> bool {
+ (self.flags() & PTEFlags::V) != PTEFlags::empty()
+ }
+ }
+
+这里相当于判断两个集合的交集是否为空。
diff --git a/guide/source/chapter4/4sv39-implementation-2.rst b/guide/source/chapter4/4sv39-implementation-2.rst
new file mode 100644
index 0000000..ce6df02
--- /dev/null
+++ b/guide/source/chapter4/4sv39-implementation-2.rst
@@ -0,0 +1,447 @@
+实现 SV39 多级页表机制(下)
+========================================================
+
+物理页帧管理
+-----------------------------------
+
+可用物理页的分配与回收
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+首先,我们需要知道物理内存的哪一部分是可用的。在 ``os/src/linker.ld`` 中,我们用符号 ``ekernel`` 指明了
+内核数据的终止物理地址,在它之后的物理内存都是可用的。而在 ``config`` 子模块中:
+
+.. code-block:: rust
+
+ // os/src/config.rs
+
+ pub const MEMORY_END: usize = 0x80800000;
+
+我们硬编码整块物理内存的终止物理地址为 ``0x80800000`` 。 而物理内存的起始物理地址为 ``0x80000000`` ,
+意味着我们将可用内存大小设置为 :math:`8\text{MiB}` ,当然也可以设置的更大一点。
+
+用一个左闭右开的物理页号区间来表示可用的物理内存,则:
+
+- 区间的左端点应该是 ``ekernel`` 的物理地址以上取整方式转化成的物理页号;
+- 区间的右端点应该是 ``MEMORY_END`` 以下取整方式转化成的物理页号。
+
+这个区间将被传给我们后面实现的物理页帧管理器用于初始化。
+
+我们声明一个 ``FrameAllocator`` Trait 来描述一个物理页帧管理器需要提供哪些功能:
+
+.. code-block:: rust
+
+ // os/src/mm/frame_allocator.rs
+
+ trait FrameAllocator {
+ fn new() -> Self;
+ fn alloc(&mut self) -> Option;
+ fn dealloc(&mut self, ppn: PhysPageNum);
+ }
+
+我们实现一种最简单的栈式物理页帧管理策略 ``StackFrameAllocator`` :
+
+.. code-block:: rust
+
+ // os/src/mm/frame_allocator.rs
+
+ pub struct StackFrameAllocator {
+ current: usize,
+ end: usize,
+ recycled: Vec,
+ }
+
+其中各字段的含义是:物理页号区间 :math:`[\text{current},\text{end})` 此前均 *从未* 被分配出去过,而向量
+``recycled`` 以后入先出的方式保存了被回收的物理页号(我们已经实现了堆分配器,参见第三章实验)。
+
+初始化非常简单。在通过 ``FrameAllocator`` 的 ``new`` 方法创建实例的时候,只需将区间两端均设为 :math:`0` ,
+然后创建一个新的向量;而在它真正被使用起来之前,需要调用 ``init`` 方法将自身的 :math:`[\text{current},\text{end})`
+初始化为可用物理页号区间:
+
+.. code-block:: rust
+
+ // os/src/mm/frame_allocator.rs
+
+ impl FrameAllocator for StackFrameAllocator {
+ fn new() -> Self {
+ Self {
+ current: 0,
+ end: 0,
+ recycled: Vec::new(),
+ }
+ }
+ }
+
+ impl StackFrameAllocator {
+ pub fn init(&mut self, l: PhysPageNum, r: PhysPageNum) {
+ self.current = l.0;
+ self.end = r.0;
+ }
+ }
+
+接下来我们来看核心的物理页帧分配和回收如何实现:
+
+.. code-block:: rust
+
+ // os/src/mm/frame_allocator.rs
+
+ impl FrameAllocator for StackFrameAllocator {
+ fn alloc(&mut self) -> Option {
+ if let Some(ppn) = self.recycled.pop() {
+ Some(ppn.into())
+ } else {
+ if self.current == self.end {
+ None
+ } else {
+ self.current += 1;
+ Some((self.current - 1).into())
+ }
+ }
+ }
+ fn dealloc(&mut self, ppn: PhysPageNum) {
+ let ppn = ppn.0;
+ // validity check
+ if ppn >= self.current || self.recycled
+ .iter()
+ .find(|&v| {*v == ppn})
+ .is_some() {
+ panic!("Frame ppn={:#x} has not been allocated!", ppn);
+ }
+ // recycle
+ self.recycled.push(ppn);
+ }
+ }
+
+- 在分配 ``alloc`` 的时候,首先会检查栈 ``recycled`` 内有没有之前回收的物理页号,如果有的话直接弹出栈顶并返回;
+ 否则的话我们只能从之前从未分配过的物理页号区间 :math:`[\text{current},\text{end})` 上进行分配,我们分配它的
+ 左端点 ``current`` ,同时将管理器内部维护的 ``current`` 加一代表 ``current`` 此前已经被分配过了。在即将返回
+ 的时候,我们使用 ``into`` 方法将 usize 转换成了物理页号 ``PhysPageNum`` 。
+
+ 注意极端情况下可能出现内存耗尽分配失败的情况:即 ``recycled`` 为空且 :math:`\text{current}==\text{end}` 。
+ 为了涵盖这种情况, ``alloc`` 的返回值被 ``Option`` 包裹,我们返回 ``None`` 即可。
+- 在回收 ``dealloc`` 的时候,我们需要检查回收页面的合法性,然后将其压入 ``recycled`` 栈中。回收页面合法有两个
+ 条件:
+
+ - 该页面之前一定被分配出去过,因此它的物理页号一定 :math:`<\text{current}` ;
+ - 该页面没有正处在回收状态,即它的物理页号不能在栈 ``recycled`` 中找到。
+
+ 我们通过 ``recycled.iter()`` 获取栈上内容的迭代器,然后通过迭代器的 ``find`` 方法试图
+ 寻找一个与输入物理页号相同的元素。其返回值是一个 ``Option`` ,如果找到了就会是一个 ``Option::Some`` ,
+ 这种情况说明我们内核其他部分实现有误,直接报错退出。
+
+之后创建 ``StackFrameAllocator`` 的全局实例 ``FRAME_ALLOCATOR``,并在正式分配物理页帧之前将 ``FRAME_ALLOCATOR`` 初始化,见 ``os/src/mm/frame_allocator.rs``。
+
+分配/回收物理页帧的接口
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+公开给其他子模块调用的分配/回收物理页帧的接口:
+
+.. code-block:: rust
+
+ // os/src/mm/frame_allocator.rs
+
+ pub fn frame_alloc() -> Option {
+ FRAME_ALLOCATOR
+ .exclusive_access()
+ .alloc()
+ .map(FrameTracker::new)
+ }
+
+ fn frame_dealloc(ppn: PhysPageNum) {
+ FRAME_ALLOCATOR.exclusive_access().dealloc(ppn);
+ }
+
+
+可以发现, ``frame_alloc`` 的返回值类型并不是 ``FrameAllocator`` 要求的物理页号 ``PhysPageNum`` ,而是将其
+进一步包装为一个 ``FrameTracker`` ,其定义如下。 ``FrameTracker`` 被创建时,需要从 ``FRAME_ALLOCATOR`` 中分配一个物理页帧:
+
+.. code-block:: rust
+
+ // os/src/mm/frame_allocator.rs
+
+ pub struct FrameTracker {
+ pub ppn: PhysPageNum,
+ }
+
+ impl FrameTracker {
+ pub fn new(ppn: PhysPageNum) -> Self {
+ // page cleaning
+ let bytes_array = ppn.get_bytes_array();
+ for i in bytes_array {
+ *i = 0;
+ }
+ Self { ppn }
+ }
+ }
+
+我们将分配来的物理页帧的物理页号作为参数传给 ``FrameTracker`` 的 ``new`` 方法来创建一个 ``FrameTracker``
+实例。由于这个物理页帧之前可能被分配过并用做其他用途,我们在这里直接将这个物理页帧上的所有字节清零。这一过程并不
+那么显然,我们后面再详细介绍。
+
+当一个 ``FrameTracker`` 生命周期结束被编译器回收的时候,我们需要将它控制的物理页帧回收掉 ``FRAME_ALLOCATOR`` 中:
+
+.. code-block:: rust
+
+ // os/src/mm/frame_allocator.rs
+
+ impl Drop for FrameTracker {
+ fn drop(&mut self) {
+ frame_dealloc(self.ppn);
+ }
+ }
+
+这里我们只需为 ``FrameTracker`` 实现 ``Drop`` Trait 即可。当一个 ``FrameTracker`` 实例被回收的时候,它的
+``drop`` 方法会自动被编译器调用,通过之前实现的 ``frame_dealloc`` 我们就将它控制的物理页帧回收以供后续使用了。
+
+最后做一个小结:从其他模块的视角看来,物理页帧分配的接口是调用 ``frame_alloc`` 函数得到一个 ``FrameTracker``
+(如果物理内存还有剩余),它就代表了一个物理页帧,当它的生命周期结束之后它所控制的物理页帧将被自动回收。
+
+多级页表实现
+-----------------------------------
+
+
+页表基本数据结构与访问接口
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+我们知道,SV39 多级页表是以节点为单位进行管理的。每个节点恰好存储在一个物理页帧中,它的位置可以用一个物理页号来表示。
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/mm/page_table.rs
+
+ pub struct PageTable {
+ root_ppn: PhysPageNum,
+ frames: Vec,
+ }
+
+ impl PageTable {
+ pub fn new() -> Self {
+ let frame = frame_alloc().unwrap();
+ PageTable {
+ root_ppn: frame.ppn,
+ frames: vec![frame],
+ }
+ }
+ }
+
+每个应用的地址空间都对应一个不同的多级页表,这也就意味这不同页表的起始地址(即页表根节点的地址)是不一样的。
+因此 ``PageTable`` 要保存它根节点的物理页号 ``root_ppn`` 作为页表唯一的区分标志。此外,
+向量 ``frames`` 以 ``FrameTracker`` 的形式保存了页表所有的节点(包括根节点)所在的物理页帧。这与物理页帧管理模块
+的测试程序是一个思路,即将这些 ``FrameTracker`` 的生命周期进一步绑定到 ``PageTable`` 下面。当 ``PageTable``
+生命周期结束后,向量 ``frames`` 里面的那些 ``FrameTracker`` 也会被回收,也就意味着存放多级页表节点的那些物理页帧
+被回收了。
+
+当我们通过 ``new`` 方法新建一个 ``PageTable`` 的时候,它只需有一个根节点。为此我们需要分配一个物理页帧
+``FrameTracker`` 并挂在向量 ``frames`` 下,然后更新根节点的物理页号 ``root_ppn`` 。
+
+多级页表并不是被创建出来之后就不再变化的,为了 MMU 能够通过地址转换正确找到应用地址空间中的数据实际被内核放在内存中
+位置,操作系统需要动态维护一个虚拟页号到页表项的映射,支持插入/删除键值对,其方法签名如下:
+
+.. code-block:: rust
+
+ // os/src/mm/page_table.rs
+
+ impl PageTable {
+ pub fn map(&mut self, vpn: VirtPageNum, ppn: PhysPageNum, flags: PTEFlags);
+ pub fn unmap(&mut self, vpn: VirtPageNum);
+ }
+
+- 我们通过 ``map`` 方法来在多级页表中插入一个键值对,注意这里我们将物理页号 ``ppn`` 和页表项标志位 ``flags`` 作为
+ 不同的参数传入而不是整合为一个页表项;
+- 相对的,我们通过 ``unmap`` 方法来删除一个键值对,在调用时仅需给出作为索引的虚拟页号即可。
+
+.. _modify-page-table:
+
+在这些操作的过程中,我们自然需要访问或修改多级页表节点的内容。每个节点都被保存在一个物理页帧中,在多级页表的架构中,我们以
+一个节点被存放在的物理页帧的物理页号作为指针指向该节点,这意味着,对于每个节点来说,一旦我们知道了指向它的物理页号,我们
+就能够修改这个节点的内容。
+
+.. _term-identical-mapping:
+
+这就需要我们提前扩充多级页表维护的映射,使得对于每一个对应于某一特定物理页帧的物理页号 ``ppn`` ,均存在一个虚拟页号
+``vpn`` 能够映射到它,而且要能够较为简单的针对一个 ``ppn`` 找到某一个能映射到它的 ``vpn`` 。这里我们采用一种最
+简单的 **恒等映射** (Identical Mapping) ,也就是说对于物理内存上的每个物理页帧,我们都在多级页表中用一个与其
+物理页号相等的虚拟页号映射到它。当我们想针对物理页号构造一个能映射到它的虚拟页号的时候,也只需使用一个和该物理页号
+相等的虚拟页号即可。
+
+
+内核中访问物理页帧的方法
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. _access-frame-in-kernel-as:
+
+
+于是,我们来看看在内核中应如何访问一个特定的物理页帧:
+
+.. code-block:: rust
+
+ // os/src/mm/address.rs
+
+ impl PhysPageNum {
+ pub fn get_pte_array(&self) -> &'static mut [PageTableEntry] {
+ let pa: PhysAddr = self.clone().into();
+ unsafe {
+ core::slice::from_raw_parts_mut(pa.0 as *mut PageTableEntry, 512)
+ }
+ }
+ pub fn get_bytes_array(&self) -> &'static mut [u8] {
+ let pa: PhysAddr = self.clone().into();
+ unsafe {
+ core::slice::from_raw_parts_mut(pa.0 as *mut u8, 4096)
+ }
+ }
+ pub fn get_mut(&self) -> &'static mut T {
+ let pa: PhysAddr = self.clone().into();
+ unsafe {
+ (pa.0 as *mut T).as_mut().unwrap()
+ }
+ }
+ }
+
+我们构造可变引用来直接访问一个物理页号 ``PhysPageNum`` 对应的物理页帧,不同的引用类型对应于物理页帧上的一种不同的
+内存布局,如 ``get_pte_array`` 返回的是一个页表项定长数组的可变引用,可以用来修改多级页表中的一个节点;而
+``get_bytes_array`` 返回的是一个字节数组的可变引用,可以以字节为粒度对物理页帧上的数据进行访问,前面进行数据清零
+就用到了这个方法; ``get_mut`` 是个泛型函数,可以获取一个恰好放在一个物理页帧开头的类型为 ``T`` 的数据的可变引用。
+
+在实现方面,都是先把物理页号转为物理地址 ``PhysAddr`` ,然后再转成 usize 形式的物理地址。接着,我们直接将它
+转为裸指针用来访问物理地址指向的物理内存。在分页机制开启前,这样做自然成立;而开启之后,虽然裸指针被视为一个虚拟地址,
+但是上面已经提到这种情况下虚拟地址会映射到一个相同的物理地址,因此在这种情况下也成立。注意,我们在返回值类型上附加了
+静态生命周期泛型 ``'static`` ,这是为了绕过 Rust 编译器的借用检查,实质上可以将返回的类型也看成一个裸指针,因为
+它也只是标识数据存放的位置以及类型。但与裸指针不同的是,无需通过 ``unsafe`` 的解引用访问它指向的数据,而是可以像一个
+正常的可变引用一样直接访问。
+
+
+建立和拆除虚实地址映射关系
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+接下来介绍建立和拆除虚实地址映射关系的 ``map`` 和 ``unmap`` 方法是如何实现的。它们都依赖于一个很重要的过程,
+也即在多级页表中找到一个虚拟地址对应的页表项。找到之后,只要修改页表项的内容即可完成键值对的插入和删除。
+在寻找页表项的时候,可能出现页表的中间级节点还未被创建的情况,这个时候我们需要手动分配一个物理页帧来存放这个节点,
+并将这个节点接入到当前的多级页表的某级中。
+
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/mm/address.rs
+
+ impl VirtPageNum {
+ pub fn indexes(&self) -> [usize; 3] {
+ let mut vpn = self.0;
+ let mut idx = [0usize; 3];
+ for i in (0..3).rev() {
+ idx[i] = vpn & 511;
+ vpn >>= 9;
+ }
+ idx
+ }
+ }
+
+ // os/src/mm/page_table.rs
+
+ impl PageTable {
+ fn find_pte_create(&mut self, vpn: VirtPageNum) -> Option<&mut PageTableEntry> {
+ let idxs = vpn.indexes();
+ let mut ppn = self.root_ppn;
+ let mut result: Option<&mut PageTableEntry> = None;
+ for i in 0..3 {
+ let pte = &mut ppn.get_pte_array()[idxs[i]];
+ if i == 2 {
+ result = Some(pte);
+ break;
+ }
+ if !pte.is_valid() {
+ let frame = frame_alloc().unwrap();
+ *pte = PageTableEntry::new(frame.ppn, PTEFlags::V);
+ self.frames.push(frame);
+ }
+ ppn = pte.ppn();
+ }
+ result
+ }
+ }
+
+- ``VirtPageNum`` 的 ``indexes`` 可以取出虚拟页号的三级页索引,并按照从高到低的顺序返回。注意它里面包裹的
+ usize 可能有 :math:`27` 位,也有可能有 :math:`64-12=52` 位,但这里我们是用来在多级页表上进行遍历,因此
+ 只取出低 :math:`27` 位。
+- ``PageTable::find_pte_create`` 在多级页表找到一个虚拟页号对应的页表项的可变引用方便后续的读写。如果在
+ 遍历的过程中发现有节点尚未创建则会新建一个节点。
+
+ 变量 ``ppn`` 表示当前节点的物理页号,最开始指向多级页表的根节点。随后每次循环通过 ``get_pte_array`` 将
+ 取出当前节点的页表项数组,并根据当前级页索引找到对应的页表项。如果当前节点是一个叶节点,那么直接返回这个页表项
+ 的可变引用;否则尝试向下走。走不下去的话就新建一个节点,更新作为下级节点指针的页表项,并将新分配的物理页帧移动到
+ 向量 ``frames`` 中方便后续的自动回收。注意在更新页表项的时候,不仅要更新物理页号,还要将标志位 V 置 1,
+ 不然硬件在查多级页表的时候,会认为这个页表项不合法,从而触发 Page Fault 而不能向下走。
+
+于是, ``map/unmap`` 就非常容易实现了:
+
+.. code-block:: rust
+
+ // os/src/mm/page_table.rs
+
+ impl PageTable {
+ pub fn map(&mut self, vpn: VirtPageNum, ppn: PhysPageNum, flags: PTEFlags) {
+ let pte = self.find_pte_create(vpn).unwrap();
+ assert!(!pte.is_valid(), "vpn {:?} is mapped before mapping", vpn);
+ *pte = PageTableEntry::new(ppn, flags | PTEFlags::V);
+ }
+ pub fn unmap(&mut self, vpn: VirtPageNum) {
+ let pte = self.find_pte_create(vpn).unwrap();
+ assert!(pte.is_valid(), "vpn {:?} is invalid before unmapping", vpn);
+ *pte = PageTableEntry::empty();
+ }
+ }
+
+只需根据虚拟页号找到页表项,然后修改或者直接清空其内容即可。
+
+.. warning::
+
+ 目前的实现方式并不打算对物理页帧耗尽的情形做任何处理而是直接 ``panic`` 退出。因此在前面的代码中能够看到
+ 很多 ``unwrap`` ,这种使用方式并不为 Rust 所推荐,只是由于简单起见暂且这样做。
+
+为了方便后面的实现,我们还需要 ``PageTable`` 提供一种不经过 MMU 而是手动查页表的方法:
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/mm/page_table.rs
+
+ impl PageTable {
+ /// Temporarily used to get arguments from user space.
+ pub fn from_token(satp: usize) -> Self {
+ Self {
+ root_ppn: PhysPageNum::from(satp & ((1usize << 44) - 1)),
+ frames: Vec::new(),
+ }
+ }
+ fn find_pte(&self, vpn: VirtPageNum) -> Option<&PageTableEntry> {
+ let idxs = vpn.indexes();
+ let mut ppn = self.root_ppn;
+ let mut result: Option<&PageTableEntry> = None;
+ for i in 0..3 {
+ let pte = &ppn.get_pte_array()[idxs[i]];
+ if i == 2 {
+ result = Some(pte);
+ break;
+ }
+ if !pte.is_valid() {
+ return None;
+ }
+ ppn = pte.ppn();
+ }
+ result
+ }
+ pub fn translate(&self, vpn: VirtPageNum) -> Option {
+ self.find_pte(vpn)
+ .map(|pte| {pte.clone()})
+ }
+ }
+
+- 第 5 行的 ``from_token`` 可以临时创建一个专用来手动查页表的 ``PageTable`` ,它仅有一个从传入的 ``satp`` token
+ 中得到的多级页表根节点的物理页号,它的 ``frames`` 字段为空,也即不实际控制任何资源;
+- 第 11 行的 ``find_pte`` 和之前的 ``find_pte_create`` 不同之处在于它不会试图分配物理页帧。一旦在多级页表上遍历
+ 遇到空指针它就会直接返回 ``None`` 表示无法正确找到传入的虚拟页号对应的页表项;
+- 第 28 行的 ``translate`` 调用 ``find_pte`` 来实现,如果能够找到页表项,那么它会将页表项拷贝一份并返回,否则就
+ 返回一个 ``None`` 。
+
+.. chyyuu 没有提到from_token的作用???
\ No newline at end of file
diff --git a/guide/source/chapter4/5kernel-app-spaces.rst b/guide/source/chapter4/5kernel-app-spaces.rst
new file mode 100644
index 0000000..3eb3da0
--- /dev/null
+++ b/guide/source/chapter4/5kernel-app-spaces.rst
@@ -0,0 +1,586 @@
+内核与应用的地址空间
+================================================
+
+
+本节我们就在内核中通过基于页表的各种数据结构实现地址空间的抽象。
+
+实现地址空间抽象
+------------------------------------------
+
+
+逻辑段:一段连续地址的虚拟内存
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+我们以逻辑段 ``MapArea`` 为单位描述一段连续地址的虚拟内存。所谓逻辑段,就是指地址区间中的一段实际可用(即 MMU 通过查多级页表
+可以正确完成地址转换)的地址连续的虚拟地址区间,该区间内包含的所有虚拟页面都以一种相同的方式映射到物理页帧,具有可读/可写/可执行等属性。
+
+.. code-block:: rust
+
+ // os/src/mm/memory_set.rs
+
+ pub struct MapArea {
+ vpn_range: VPNRange,
+ data_frames: BTreeMap,
+ map_type: MapType,
+ map_perm: MapPermission,
+ }
+
+其中 ``VPNRange`` 描述一段虚拟页号的连续区间,表示该逻辑段在地址区间中的位置和长度。它是一个迭代器,可以使用 Rust
+的语法糖 for-loop 进行迭代。有兴趣的读者可以参考 ``os/src/mm/address.rs`` 中它的实现。
+
+.. note::
+
+ **Rust 语法卡片:迭代器 Iterator**
+
+ Rust编程的迭代器模式允许你对一个序列的项进行某些处理。迭代器(iterator)是负责遍历序列中的每一项和决定序列何时结束的控制逻辑。
+ 对于如何使用迭代器处理元素序列和如何实现 Iterator trait 来创建自定义迭代器的内容,
+ 可以参考 `Rust 程序设计语言-中文版第十三章第二节 `_
+
+``MapType`` 描述该逻辑段内的所有虚拟页面映射到物理页帧的同一种方式,它是一个枚举类型,在内核当前的实现中支持两种方式:
+
+.. code-block:: rust
+
+ // os/src/mm/memory_set.rs
+
+ #[derive(Copy, Clone, PartialEq, Debug)]
+ pub enum MapType {
+ Identical,
+ Framed,
+ }
+
+其中 ``Identical`` 表示之前也有提到的恒等映射,用于在启用多级页表之后仍能够访问一个特定的物理地址指向的物理内存;而
+``Framed`` 则表示对于每个虚拟页面都需要映射到一个新分配的物理页帧。
+
+当逻辑段采用 ``MapType::Framed`` 方式映射到物理内存的时候, ``data_frames`` 是一个保存了该逻辑段内的每个虚拟页面
+和它被映射到的物理页帧 ``FrameTracker`` 的一个键值对容器 ``BTreeMap`` 中,这些物理页帧被用来存放实际内存数据而不是
+作为多级页表中的中间节点。和之前的 ``PageTable`` 一样,这也用到了 RAII 的思想,将这些物理页帧的生命周期绑定到它所在的逻辑段
+``MapArea`` 下,当逻辑段被回收之后这些之前分配的物理页帧也会自动地同时被回收。
+
+``MapPermission`` 表示控制该逻辑段的访问方式,它是页表项标志位 ``PTEFlags`` 的一个子集,仅保留 U/R/W/X
+四个标志位,因为其他的标志位仅与硬件的地址转换机制细节相关,这样的设计能避免引入错误的标志位。
+
+.. code-block:: rust
+
+ // os/src/mm/memory_set.rs
+
+ bitflags! {
+ pub struct MapPermission: u8 {
+ const R = 1 << 1;
+ const W = 1 << 2;
+ const X = 1 << 3;
+ const U = 1 << 4;
+ }
+ }
+
+
+
+地址空间:一系列有关联的逻辑段
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+地址空间是一系列有关联的逻辑段,这种关联一般是指这些逻辑段属于一个运行的程序(目前把一个运行的程序称为任务,后续会称为进程)。
+用来表明正在运行的应用所在执行环境中的可访问内存空间,在这个内存空间中,包含了一系列的不一定连续的逻辑段。
+这样我们就有任务的地址空间、内核的地址空间等说法了。地址空间使用 ``MemorySet`` 类型来表示:
+
+.. code-block:: rust
+
+ // os/src/mm/memory_set.rs
+
+ pub struct MemorySet {
+ page_table: PageTable,
+ areas: Vec,
+ }
+
+它包含了该地址空间的多级页表 ``page_table`` 和一个逻辑段 ``MapArea`` 的向量 ``areas`` 。注意 ``PageTable`` 下
+挂着所有多级页表的节点所在的物理页帧,而每个 ``MapArea`` 下则挂着对应逻辑段中的数据所在的物理页帧,这两部分
+合在一起构成了一个地址空间所需的所有物理页帧。这同样是一种 RAII 风格,当一个地址空间 ``MemorySet`` 生命周期结束后,
+这些物理页帧都会被回收。
+
+地址空间 ``MemorySet`` 的方法如下:
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/mm/memory_set.rs
+
+ impl MemorySet {
+ pub fn new_bare() -> Self {
+ Self {
+ page_table: PageTable::new(),
+ areas: Vec::new(),
+ }
+ }
+ fn push(&mut self, mut map_area: MapArea, data: Option<&[u8]>) {
+ map_area.map(&mut self.page_table);
+ if let Some(data) = data {
+ map_area.copy_data(&mut self.page_table, data);
+ }
+ self.areas.push(map_area);
+ }
+ /// Assume that no conflicts.
+ pub fn insert_framed_area(
+ &mut self,
+ start_va: VirtAddr, end_va: VirtAddr, permission: MapPermission
+ ) {
+ self.push(MapArea::new(
+ start_va,
+ end_va,
+ MapType::Framed,
+ permission,
+ ), None);
+ }
+ pub fn new_kernel() -> Self;
+ /// Include sections in elf and trampoline and TrapContext and user stack,
+ /// also returns user_sp and entry point.
+ pub fn from_elf(elf_data: &[u8]) -> (Self, usize, usize);
+ }
+
+- 第 4 行, ``new_bare`` 方法可以新建一个空的地址空间;
+- 第 10 行, ``push`` 方法可以在当前地址空间插入一个新的逻辑段 ``map_area`` ,如果它是以 ``Framed`` 方式映射到
+ 物理内存,还可以可选地在那些被映射到的物理页帧上写入一些初始化数据 ``data`` ;
+- 第 18 行, ``insert_framed_area`` 方法调用 ``push`` ,可以在当前地址空间插入一个 ``Framed`` 方式映射到
+ 物理内存的逻辑段。注意该方法的调用者要保证同一地址空间内的任意两个逻辑段不能存在交集,从后面即将分别介绍的内核和
+ 应用的地址空间布局可以看出这一要求得到了保证;
+- 第 29 行, ``new_kernel`` 可以生成内核的地址空间,而第 32 行的 ``from_elf`` 则可以应用的 ELF 格式可执行文件
+ 解析出各数据段并对应生成应用的地址空间。它们的实现我们将在后面讨论。
+
+在实现 ``push`` 方法在地址空间中插入一个逻辑段 ``MapArea`` 的时候,需要同时维护地址空间的多级页表 ``page_table``
+记录的虚拟页号到页表项的映射关系,也需要用到这个映射关系来找到向哪些物理页帧上拷贝初始数据。这用到了 ``MapArea``
+提供的另外几个方法:
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/mm/memory_set.rs
+
+ impl MapArea {
+ pub fn new(
+ start_va: VirtAddr,
+ end_va: VirtAddr,
+ map_type: MapType,
+ map_perm: MapPermission
+ ) -> Self {
+ let start_vpn: VirtPageNum = start_va.floor();
+ let end_vpn: VirtPageNum = end_va.ceil();
+ Self {
+ vpn_range: VPNRange::new(start_vpn, end_vpn),
+ data_frames: BTreeMap::new(),
+ map_type,
+ map_perm,
+ }
+ }
+ pub fn map(&mut self, page_table: &mut PageTable) {
+ for vpn in self.vpn_range {
+ self.map_one(page_table, vpn);
+ }
+ }
+ pub fn unmap(&mut self, page_table: &mut PageTable) {
+ for vpn in self.vpn_range {
+ self.unmap_one(page_table, vpn);
+ }
+ }
+ /// data: start-aligned but maybe with shorter length
+ /// assume that all frames were cleared before
+ pub fn copy_data(&mut self, page_table: &mut PageTable, data: &[u8]) {
+ assert_eq!(self.map_type, MapType::Framed);
+ let mut start: usize = 0;
+ let mut current_vpn = self.vpn_range.get_start();
+ let len = data.len();
+ loop {
+ let src = &data[start..len.min(start + PAGE_SIZE)];
+ let dst = &mut page_table
+ .translate(current_vpn)
+ .unwrap()
+ .ppn()
+ .get_bytes_array()[..src.len()];
+ dst.copy_from_slice(src);
+ start += PAGE_SIZE;
+ if start >= len {
+ break;
+ }
+ current_vpn.step();
+ }
+ }
+ }
+
+- 第 4 行的 ``new`` 方法可以新建一个逻辑段结构体,注意传入的起始/终止虚拟地址会分别被下取整/上取整为虚拟页号并传入
+ 迭代器 ``vpn_range`` 中;
+- 第 19 行的 ``map`` 和第 24 行的 ``unmap`` 可以将当前逻辑段到物理内存的映射从传入的该逻辑段所属的地址空间的
+ 多级页表中加入或删除。可以看到它们的实现是遍历逻辑段中的所有虚拟页面,并以每个虚拟页面为单位依次在多级页表中进行
+ 键值对的插入或删除,分别对应 ``MapArea`` 的 ``map_one`` 和 ``unmap_one`` 方法,我们后面将介绍它们的实现;
+- 第 31 行的 ``copy_data`` 方法将切片 ``data`` 中的数据拷贝到当前逻辑段实际被内核放置在的各物理页帧上,从而
+ 在地址空间中通过该逻辑段就能访问这些数据。调用它的时候需要满足:切片 ``data`` 中的数据大小不超过当前逻辑段的
+ 总大小,且切片中的数据会被对齐到逻辑段的开头,然后逐页拷贝到实际的物理页帧。
+
+ 从第 36 行开始的循环会遍历每一个需要拷贝数据的虚拟页面,在数据拷贝完成后会在第 48 行通过调用 ``step`` 方法,该
+ 方法来自于 ``os/src/mm/address.rs`` 中为 ``VirtPageNum`` 实现的 ``StepOne`` Trait,感兴趣的读者可以阅读
+ 代码确认其实现。
+
+ 每个页面的数据拷贝需要确定源 ``src`` 和目标 ``dst`` 两个切片并直接使用 ``copy_from_slice`` 完成复制。当确定
+ 目标切片 ``dst`` 的时候,第 ``39`` 行从传入的当前逻辑段所属的地址空间的多级页表中手动查找迭代到的虚拟页号被映射
+ 到的物理页帧,并通过 ``get_bytes_array`` 方法获取能够真正改写该物理页帧上内容的字节数组型可变引用,最后再获取它
+ 的切片用于数据拷贝。
+
+接下来介绍对逻辑段中的单个虚拟页面进行映射/解映射的方法 ``map_one`` 和 ``unmap_one`` 。显然它们的实现取决于当前
+逻辑段被映射到物理内存的方式:
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/mm/memory_set.rs
+
+ impl MemoryArea {
+ pub fn map_one(&mut self, page_table: &mut PageTable, vpn: VirtPageNum) {
+ let ppn: PhysPageNum;
+ match self.map_type {
+ MapType::Identical => {
+ ppn = PhysPageNum(vpn.0);
+ }
+ MapType::Framed => {
+ let frame = frame_alloc().unwrap();
+ ppn = frame.ppn;
+ self.data_frames.insert(vpn, frame);
+ }
+ }
+ let pte_flags = PTEFlags::from_bits(self.map_perm.bits).unwrap();
+ page_table.map(vpn, ppn, pte_flags);
+ }
+ pub fn unmap_one(&mut self, page_table: &mut PageTable, vpn: VirtPageNum) {
+ match self.map_type {
+ MapType::Framed => {
+ self.data_frames.remove(&vpn);
+ }
+ _ => {}
+ }
+ page_table.unmap(vpn);
+ }
+ }
+
+- 对于第 4 行的 ``map_one`` 来说,在虚拟页号 ``vpn`` 已经确定的情况下,它需要知道要将一个怎么样的页表项插入多级页表。
+ 页表项的标志位来源于当前逻辑段的类型为 ``MapPermission`` 的统一配置,只需将其转换为 ``PTEFlags`` ;而页表项的
+ 物理页号则取决于当前逻辑段映射到物理内存的方式:
+
+ - 当以恒等映射 ``Identical`` 方式映射的时候,物理页号就等于虚拟页号;
+ - 当以 ``Framed`` 方式映射的时候,需要分配一个物理页帧让当前的虚拟页面可以映射过去,此时页表项中的物理页号自然就是
+ 这个被分配的物理页帧的物理页号。此时还需要将这个物理页帧挂在逻辑段的 ``data_frames`` 字段下。
+
+ 当确定了页表项的标志位和物理页号之后,即可调用多级页表 ``PageTable`` 的 ``map`` 接口来插入键值对。
+- 对于第 19 行的 ``unmap_one`` 来说,基本上就是调用 ``PageTable`` 的 ``unmap`` 接口删除以传入的虚拟页号为键的
+ 键值对即可。然而,当以 ``Framed`` 映射的时候,不要忘记同时将虚拟页面被映射到的物理页帧 ``FrameTracker`` 从
+ ``data_frames`` 中移除,这样这个物理页帧才能立即被回收以备后续分配。
+
+内核地址空间
+------------------------------------------
+
+.. _term-isolation:
+
+在本章之前,内核和应用代码的访存地址都被视为一个物理地址直接访问物理内存,而在分页模式开启之后,它们都需要通过 MMU 的
+地址转换变成物理地址再交给 CPU 的访存单元去访问物理内存。地址空间抽象的重要意义在于 **隔离** (Isolation) ,当我们
+在执行每个应用的代码的时候,内核需要控制 MMU 使用这个应用地址空间的多级页表进行地址转换。由于每个应用地址空间在创建
+的时候也顺带设置好了多级页表使得只有那些存放了它的数据的物理页帧能够通过该多级页表被映射到,这样它就只能访问自己的数据
+而无法触及其他应用或是内核的数据。
+
+.. _term-trampoline:
+
+启用分页模式下,内核代码的访存地址也会被视为一个虚拟地址并需要经过 MMU 的地址转换,因此我们也需要为内核对应构造一个
+地址空间,它除了仍然需要允许内核的各数据段能够被正常访问之后,还需要包含所有应用的内核栈以及一个
+**跳板** (Trampoline) 。我们会在本章的最后一节再深入介绍跳板的机制。
+
+下图是软件看到的 64 位地址空间在 SV39 分页模式下实际可能通过 MMU 检查的最高 :math:`256\text{GiB}` (之前在
+:ref:`这里 ` 中解释过最高和最低 :math:`256\text{GiB}` 的问题):
+
+.. image:: kernel-as-high.png
+ :name: kernel-as-high
+ :align: center
+ :height: 400
+
+可以看到,跳板放在最高的一个虚拟页面中。接下来则是从高到低放置每个应用的内核栈,内核栈的大小由 ``config`` 子模块的
+``KERNEL_STACK_SIZE`` 给出。它们的映射方式为 ``MapPermission`` 中的 rw 两个标志位,意味着这个逻辑段仅允许
+CPU 处于内核态访问,且只能读或写。
+
+.. _term-guard-page:
+
+注意相邻两个内核栈之间会预留一个 **保护页面** (Guard Page) ,它是内核地址空间中的空洞,多级页表中并不存在与它相关的映射。
+它的意义在于当内核栈空间不足(如调用层数过多或死递归)的时候,代码会尝试访问
+空洞区域内的虚拟地址,然而它无法在多级页表中找到映射,便会触发异常,此时控制权会交给 trap handler 对这种情况进行
+处理。由于编译器会对访存顺序和局部变量在栈帧中的位置进行优化,我们难以确定一个已经溢出的栈帧中的哪些位置会先被访问,
+但总的来说,空洞区域被设置的越大,我们就能越早捕获到这一错误并避免它覆盖其他重要数据。由于我们的内核非常简单且内核栈
+的大小设置比较宽裕,在当前的设计中我们仅将空洞区域的大小设置为单个页面。
+
+下面则给出了内核地址空间的低 :math:`256\text{GiB}` 的布局:
+
+.. image:: kernel-as-low.png
+ :align: center
+ :height: 400
+
+四个逻辑段 ``.text/.rodata/.data/.bss`` 被恒等映射到物理内存,这使得我们在无需调整内核内存布局 ``os/src/linker.ld``
+的情况下就仍能和启用页表机制之前那样访问内核的各数据段。注意我们借用页表机制对这些逻辑段的访问方式做出了限制,这都是为了
+在硬件的帮助下能够尽可能发现内核中的 bug ,在这里:
+
+- 四个逻辑段的 U 标志位均未被设置,使得 CPU 只能在处于 S 特权级(或以上)时访问它们;
+- 代码段 ``.text`` 不允许被修改;
+- 只读数据段 ``.rodata`` 不允许被修改,也不允许从它上面取指;
+- ``.data/.bss`` 均允许被读写,但是不允许从它上面取指。
+
+此外, :ref:`之前 ` 提到过内核地址空间中需要存在一个恒等映射到内核数据段之外的可用物理
+页帧的逻辑段,这样才能在启用页表机制之后,内核仍能以纯软件的方式读写这些物理页帧。它们的标志位仅包含 rw ,意味着该
+逻辑段只能在 S 特权级以上访问,并且只能读写。
+
+下面我们给出创建内核地址空间的方法 ``new_kernel`` :
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/mm/memory_set.rs
+
+ extern "C" {
+ fn stext();
+ fn etext();
+ fn srodata();
+ fn erodata();
+ fn sdata();
+ fn edata();
+ fn sbss_with_stack();
+ fn ebss();
+ fn ekernel();
+ fn strampoline();
+ }
+
+ impl MemorySet {
+ /// Without kernel stacks.
+ pub fn new_kernel() -> Self {
+ let mut memory_set = Self::new_bare();
+ // map trampoline
+ memory_set.map_trampoline();
+ // map kernel sections
+ println!(".text [{:#x}, {:#x})", stext as usize, etext as usize);
+ println!(".rodata [{:#x}, {:#x})", srodata as usize, erodata as usize);
+ println!(".data [{:#x}, {:#x})", sdata as usize, edata as usize);
+ println!(".bss [{:#x}, {:#x})", sbss_with_stack as usize, ebss as usize);
+ println!("mapping .text section");
+ memory_set.push(MapArea::new(
+ (stext as usize).into(),
+ (etext as usize).into(),
+ MapType::Identical,
+ MapPermission::R | MapPermission::X,
+ ), None);
+ println!("mapping .rodata section");
+ memory_set.push(MapArea::new(
+ (srodata as usize).into(),
+ (erodata as usize).into(),
+ MapType::Identical,
+ MapPermission::R,
+ ), None);
+ println!("mapping .data section");
+ memory_set.push(MapArea::new(
+ (sdata as usize).into(),
+ (edata as usize).into(),
+ MapType::Identical,
+ MapPermission::R | MapPermission::W,
+ ), None);
+ println!("mapping .bss section");
+ memory_set.push(MapArea::new(
+ (sbss_with_stack as usize).into(),
+ (ebss as usize).into(),
+ MapType::Identical,
+ MapPermission::R | MapPermission::W,
+ ), None);
+ println!("mapping physical memory");
+ memory_set.push(MapArea::new(
+ (ekernel as usize).into(),
+ MEMORY_END.into(),
+ MapType::Identical,
+ MapPermission::R | MapPermission::W,
+ ), None);
+ memory_set
+ }
+ }
+
+``new_kernel`` 将映射跳板和地址空间中最低 :math:`256\text{GiB}` 中的所有的逻辑段。第 3 行开始,我们从
+``os/src/linker.ld`` 中引用了很多表示了各个段位置的符号,而后在 ``new_kernel`` 中,我们从低地址到高地址
+依次创建 5 个逻辑段并通过 ``push`` 方法将它们插入到内核地址空间中,上面我们已经详细介绍过这 5 个逻辑段。跳板
+是通过 ``map_trampoline`` 方法来映射的,我们也将在本章最后一节进行讲解。
+
+应用地址空间
+------------------------------------------
+
+现在我们来介绍如何创建应用的地址空间。在前面的章节中,我们直接将丢弃所有符号的应用二进制镜像链接到内核,在初始化的时候
+内核仅需将他们加载到正确的初始物理地址就能使它们正确执行。但本章中,我们希望效仿内核地址空间的设计,同样借助页表机制
+使得应用地址空间的各个逻辑段也可以有不同的访问方式限制,这样可以提早检测出应用的错误并及时将其终止以最小化它对系统带来的
+恶劣影响。
+
+在第三章中,每个应用链接脚本中的起始地址被要求是不同的,这样它们的代码和数据存放的位置才不会产生冲突。但是这是一种对于应用开发者
+极其不友好的设计。现在,借助地址空间的抽象,我们终于可以让所有应用程序都使用同样的起始地址,这也意味着所有应用可以使用同一个链接脚本了:
+
+.. code-block::
+ :linenos:
+
+ /* user/src/linker.ld */
+
+ OUTPUT_ARCH(riscv)
+ ENTRY(_start)
+
+ BASE_ADDRESS = 0x0;
+
+ SECTIONS
+ {
+ . = BASE_ADDRESS;
+ .text : {
+ *(.text.entry)
+ *(.text .text.*)
+ }
+ . = ALIGN(4K);
+ .rodata : {
+ *(.rodata .rodata.*)
+ }
+ . = ALIGN(4K);
+ .data : {
+ *(.data .data.*)
+ }
+ .bss : {
+ *(.bss .bss.*)
+ }
+ /DISCARD/ : {
+ *(.eh_frame)
+ *(.debug*)
+ }
+ }
+
+我们将起始地址 ``BASE_ADDRESS`` 设置为 :math:`\text{0x0}` ,显然它只能是一个地址空间中的虚拟地址而非物理地址。
+事实上由于我们将入口汇编代码段放在最低的地方,这也是整个应用的入口点。
+我们只需清楚这一事实即可,而无需像之前一样将其硬编码到代码中。此外,在 ``.text`` 和 ``.rodata`` 中间以及 ``.rodata`` 和
+``.data`` 中间我们进行了页面对齐,因为前后两个逻辑段的访问方式限制是不同的,由于我们只能以页为单位对这个限制进行设置,
+因此就只能将下一个逻辑段对齐到下一个页面开始放置。相对的, ``.data`` 和 ``.bss`` 两个逻辑段由于限制相同,它们中间
+则无需进行页面对齐。
+
+下图展示了应用地址空间的布局:
+
+.. image:: app-as-full.png
+ :align: center
+ :height: 400
+
+左侧给出了应用地址空间最低 :math:`256\text{GiB}` 的布局:从 :math:`\text{0x0}` 开始向高地址放置应用内存布局中的
+各个逻辑段,最后放置带有一个保护页面的用户栈。这些逻辑段都是以 ``Framed`` 方式映射到物理内存的,从访问方式上来说都加上
+了 U 标志位代表 CPU 可以在 U 特权级也就是执行应用代码的时候访问它们。右侧则给出了最高的 :math:`256\text{GiB}` ,
+可以看出它只是和内核地址空间一样将跳板放置在最高页,还将 Trap 上下文放置在次高页中。这两个虚拟页面虽然位于应用地址空间,
+但是它们并不包含 U 标志位,事实上它们在地址空间切换的时候才会发挥作用,请同样参考本章的最后一节。
+
+在 ``os/src/build.rs`` 中,我们不再将丢弃了所有符号的应用二进制镜像链接进内核,而是直接使用 ELF 格式的可执行文件,
+因为在前者中内存布局中各个逻辑段的位置和访问限制等信息都被裁剪掉了。而 ``loader`` 子模块也变得极其精简:
+
+.. code-block:: rust
+
+ // os/src/loader.rs
+
+ pub fn get_num_app() -> usize {
+ extern "C" { fn _num_app(); }
+ unsafe { (_num_app as usize as *const usize).read_volatile() }
+ }
+
+ pub fn get_app_data(app_id: usize) -> &'static [u8] {
+ extern "C" { fn _num_app(); }
+ let num_app_ptr = _num_app as usize as *const usize;
+ let num_app = get_num_app();
+ let app_start = unsafe {
+ core::slice::from_raw_parts(num_app_ptr.add(1), num_app + 1)
+ };
+ assert!(app_id < num_app);
+ unsafe {
+ core::slice::from_raw_parts(
+ app_start[app_id] as *const u8,
+ app_start[app_id + 1] - app_start[app_id]
+ )
+ }
+ }
+
+它仅需要提供两个函数: ``get_num_app`` 获取链接到内核内的应用的数目,而 ``get_app_data`` 则根据传入的应用编号
+取出对应应用的 ELF 格式可执行文件数据。它们和之前一样仍是基于 ``build.rs`` 生成的 ``link_app.S`` 给出的符号来
+确定其位置,并实际放在内核的数据段中。
+``loader`` 模块中原有的内核和用户栈则分别作为逻辑段放在内核和用户地址空间中,我们无需再去专门为其定义一种类型。
+
+在创建应用地址空间的时候,我们需要对 ``get_app_data`` 得到的 ELF 格式数据进行解析,找到各个逻辑段所在位置和访问
+限制并插入进来,最终得到一个完整的应用地址空间:
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/mm/memory_set.rs
+
+ impl MemorySet {
+ /// Include sections in elf and trampoline and TrapContext and user stack,
+ /// also returns user_sp and entry point.
+ pub fn from_elf(elf_data: &[u8]) -> (Self, usize, usize) {
+ let mut memory_set = Self::new_bare();
+ // map trampoline
+ memory_set.map_trampoline();
+ // map program headers of elf, with U flag
+ let elf = xmas_elf::ElfFile::new(elf_data).unwrap();
+ let elf_header = elf.header;
+ let magic = elf_header.pt1.magic;
+ assert_eq!(magic, [0x7f, 0x45, 0x4c, 0x46], "invalid elf!");
+ let ph_count = elf_header.pt2.ph_count();
+ let mut max_end_vpn = VirtPageNum(0);
+ for i in 0..ph_count {
+ let ph = elf.program_header(i).unwrap();
+ if ph.get_type().unwrap() == xmas_elf::program::Type::Load {
+ let start_va: VirtAddr = (ph.virtual_addr() as usize).into();
+ let end_va: VirtAddr = ((ph.virtual_addr() + ph.mem_size()) as usize).into();
+ let mut map_perm = MapPermission::U;
+ let ph_flags = ph.flags();
+ if ph_flags.is_read() { map_perm |= MapPermission::R; }
+ if ph_flags.is_write() { map_perm |= MapPermission::W; }
+ if ph_flags.is_execute() { map_perm |= MapPermission::X; }
+ let map_area = MapArea::new(
+ start_va,
+ end_va,
+ MapType::Framed,
+ map_perm,
+ );
+ max_end_vpn = map_area.vpn_range.get_end();
+ memory_set.push(
+ map_area,
+ Some(&elf.input[ph.offset() as usize..(ph.offset() + ph.file_size()) as usize])
+ );
+ }
+ }
+ // map user stack with U flags
+ let max_end_va: VirtAddr = max_end_vpn.into();
+ let mut user_stack_bottom: usize = max_end_va.into();
+ // guard page
+ user_stack_bottom += PAGE_SIZE;
+ let user_stack_top = user_stack_bottom + USER_STACK_SIZE;
+ memory_set.push(MapArea::new(
+ user_stack_bottom.into(),
+ user_stack_top.into(),
+ MapType::Framed,
+ MapPermission::R | MapPermission::W | MapPermission::U,
+ ), None);
+ // map TrapContext
+ memory_set.push(MapArea::new(
+ TRAP_CONTEXT.into(),
+ TRAMPOLINE.into(),
+ MapType::Framed,
+ MapPermission::R | MapPermission::W,
+ ), None);
+ (memory_set, user_stack_top, elf.header.pt2.entry_point() as usize)
+ }
+ }
+
+- 第 9 行,我们将跳板插入到应用地址空间;
+- 第 11 行,我们使用外部 crate ``xmas_elf`` 来解析传入的应用 ELF 数据并可以轻松取出各个部分。
+ :ref:`此前 ` 我们简要介绍过 ELF 格式的布局。第 14 行,我们取出 ELF 的魔数来判断
+ 它是不是一个合法的 ELF 。
+
+ 第 15 行,我们可以直接得到 program header 的数目,然后遍历所有的 program header 并将合适的区域加入
+ 到应用地址空间中。这一过程的主体在第 17~39 行之间。第 19 行我们确认 program header 的类型是 ``LOAD`` ,
+ 这表明它有被内核加载的必要,此时不必理会其他类型的 program header 。接着通过 ``ph.virtual_addr()`` 和
+ ``ph.mem_size()`` 来计算这一区域在应用地址空间中的位置,通过 ``ph.flags()`` 来确认这一区域访问方式的
+ 限制并将其转换为 ``MapPermission`` 类型(注意它默认包含 U 标志位)。最后我们在第 27 行创建逻辑段
+ ``map_area`` 并在第 34 行 ``push`` 到应用地址空间。在 ``push`` 的时候我们需要完成数据拷贝,当前
+ program header 数据被存放的位置可以通过 ``ph.offset()`` 和 ``ph.file_size()`` 来找到。 注意当
+ 存在一部分零初始化的时候, ``ph.file_size()`` 将会小于 ``ph.mem_size()`` ,因为这些零出于缩减可执行
+ 文件大小的原因不应该实际出现在 ELF 数据中。
+- 我们从第 40 行开始处理用户栈。注意在前面加载各个 program header 的时候,我们就已经维护了 ``max_end_vpn``
+ 记录目前涉及到的最大的虚拟页号,只需紧接着在它上面再放置一个保护页面和用户栈即可。
+- 第 53 行则在应用地址空间中映射次高页面来存放 Trap 上下文。
+- 第 59 行返回的时候,我们不仅返回应用地址空间 ``memory_set`` ,也同时返回用户栈虚拟地址 ``user_stack_top``
+ 以及从解析 ELF 得到的该应用入口点地址,它们将被我们用来创建应用的任务控制块。
\ No newline at end of file
diff --git a/guide/source/chapter4/6multitasking-based-on-as.rst b/guide/source/chapter4/6multitasking-based-on-as.rst
new file mode 100644
index 0000000..24ec9e7
--- /dev/null
+++ b/guide/source/chapter4/6multitasking-based-on-as.rst
@@ -0,0 +1,684 @@
+基于地址空间的分时多任务
+==============================================================
+
+本节我们介绍如何基于地址空间抽象来实现第三章的分时多任务系统。
+
+建立并开启基于分页模式的虚拟地址空间
+--------------------------------------------
+
+当 SBI 实现(本项目中基于 RustSBI)初始化完成后, CPU 将跳转到内核入口点并在 S 特权级上执行,此时还并没有开启分页模式
+,内核的每一次访存仍被视为一个物理地址直接访问物理内存。而在开启分页模式之后,内核的代码在访存的时候只能看到内核地址空间,
+此时每次访存将被视为一个虚拟地址且需要通过 MMU 基于内核地址空间的多级页表的地址转换。这两种模式之间的过渡在内核初始化期间
+完成。
+
+创建内核地址空间
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+
+我们创建内核地址空间的全局实例:
+
+.. code-block:: rust
+
+ // os/src/mm/memory_set.rs
+
+ lazy_static! {
+ pub static ref KERNEL_SPACE: Arc> = Arc::new(unsafe {
+ UPSafeCell::new(MemorySet::new_kernel()
+ )});
+ }
+
+从之前对于 ``lazy_static!`` 宏的介绍可知, ``KERNEL_SPACE`` 在运行期间它第一次被用到时才会实际进行初始化,而它所
+占据的空间则是编译期被放在全局数据段中。 ``Arc>`` 同时带来 ``Arc`` 提供的共享
+引用,和 ``UPSafeCell`` 提供的互斥访问。
+
+在 ``rust_main`` 函数中,我们首先调用 ``mm::init`` 进行内存管理子系统的初始化:
+
+.. code-block:: rust
+
+ // os/src/mm/mod.rs
+
+ pub use memory_set::KERNEL_SPACE;
+
+ pub fn init() {
+ heap_allocator::init_heap();
+ frame_allocator::init_frame_allocator();
+ KERNEL_SPACE.exclusive_access().activate();
+ }
+
+可以看到,我们最先进行了全局动态内存分配器的初始化,因为接下来马上就要用到 Rust 的堆数据结构。接下来我们初始化物理页帧
+管理器(内含堆数据结构 ``Vec`` )使能可用物理页帧的分配和回收能力。最后我们创建内核地址空间并让 CPU 开启分页模式,
+MMU 在地址转换的时候使用内核的多级页表,这一切均在一行之内做到:
+
+- 首先,我们引用 ``KERNEL_SPACE`` ,这是它第一次被使用,就在此时它会被初始化,调用 ``MemorySet::new_kernel``
+ 创建一个内核地址空间并使用 ``Arc>`` 包裹起来;
+
+- 最然后,我们调用 ``MemorySet::activate`` :
+
+ .. code-block:: rust
+ :linenos:
+
+ // os/src/mm/page_table.rs
+
+ pub fn token(&self) -> usize {
+ 8usize << 60 | self.root_ppn.0
+ }
+
+ // os/src/mm/memory_set.rs
+
+ impl MemorySet {
+ pub fn activate(&self) {
+ let satp = self.page_table.token();
+ unsafe {
+ satp::write(satp);
+ core::arch::asm!("sfence.vma");
+ }
+ }
+ }
+
+ ``PageTable::token`` 会按照 :ref:`satp CSR 格式要求 ` 构造一个无符号 64 位无符号整数,使得其
+ 分页模式为 SV39 ,且将当前多级页表的根节点所在的物理页号填充进去。在 ``activate`` 中,我们将这个值写入当前 CPU 的
+ satp CSR ,从这一刻开始 SV39 分页模式就被启用了,而且 MMU 会使用内核地址空间的多级页表进行地址转换。
+
+ 我们必须注意切换 satp CSR 是否是一个 *平滑* 的过渡:其含义是指,切换 satp 的指令及其下一条指令这两条相邻的指令的
+ 虚拟地址是相邻的(由于切换 satp 的指令并不是一条跳转指令, pc 只是简单的自增当前指令的字长),
+ 而它们所在的物理地址一般情况下也是相邻的,但是它们所经过的地址转换流程却是不同的——切换 satp 导致 MMU 查的多级页表
+ 是不同的。这就要求前后两个地址空间在切换 satp 的指令 *附近* 的映射满足某种意义上的连续性。
+
+ 幸运的是,我们做到了这一点。这条写入 satp 的指令及其下一条指令都在内核内存布局的代码段中,在切换之后是一个恒等映射,
+ 而在切换之前是视为物理地址直接取指,也可以将其看成一个恒等映射。这完全符合我们的期待:即使切换了地址空间,指令仍应该
+ 能够被连续的执行。
+
+注意到在 ``activate`` 的最后,我们插入了一条汇编指令 ``sfence.vma`` ,它又起到什么作用呢?
+
+让我们再来回顾一下多级页表:它相比线性表虽然大量节约了内存占用,但是却需要 MMU 进行更多的隐式访存。如果是一个线性表,
+MMU 仅需单次访存就能找到页表项并完成地址转换,而多级页表(以 SV39 为例,不考虑大页)最顺利的情况下也需要三次访存。这些
+额外的访存和真正访问数据的那些访存在空间上并不相邻,加大了多级缓存的压力,一旦缓存缺失将带来巨大的性能惩罚。如果采用
+多级页表实现,这个问题会变得更为严重,使得地址空间抽象的性能开销过大。
+
+.. _term-tlb:
+
+为了解决性能问题,一种常见的做法是在 CPU 中利用部分硬件资源额外加入一个 **快表**
+(TLB, Translation Lookaside Buffer) , 它维护了部分虚拟页号到页表项的键值对。当 MMU 进行地址转换的时候,首先
+会到快表中看看是否匹配,如果匹配的话直接取出页表项完成地址转换而无需访存;否则再去查页表并将键值对保存在快表中。一旦
+我们修改了 satp 切换了地址空间,快表中的键值对就会失效,因为它还表示着上个地址空间的映射关系。为了 MMU 的地址转换
+能够及时与 satp 的修改同步,我们可以选择立即使用 ``sfence.vma`` 指令将快表清空,这样 MMU 就不会看到快表中已经
+过期的键值对了。
+
+.. _term-trampoline:
+
+跳板的实现
+------------------------------------
+
+上一小节我们看到无论是内核还是应用的地址空间,最高的虚拟页面都是一个跳板。同时应用地址空间的次高虚拟页面还被设置为用来
+存放应用的 Trap 上下文。那么跳板究竟起什么作用呢?为何不直接把 Trap 上下文仍放到应用的内核栈中呢?
+
+回忆曾在第二章介绍过的,当一个应用 Trap 到内核的时候,
+``sscratch`` 已经指出了该应用内核栈的栈顶,我们用一条指令即可从用户栈切换到内核栈,然后直接将 Trap 上下文压入内核栈
+栈顶。当 Trap 处理完毕返回用户态的时候,将 Trap 上下文中的内容恢复到寄存器上,最后将保存着应用用户栈顶的 ``sscratch``
+与 sp 进行交换,也就从内核栈切换回了用户栈。在这个过程中, ``sscratch`` 起到了非常关键的作用,它使得我们可以在不破坏
+任何通用寄存器的情况下完成用户栈和内核栈顶的 Trap 上下文这两个工作区域之间的切换。
+
+然而,一旦使能了分页机制,一切就并没有这么简单了,我们必须在这个过程中同时完成地址空间的切换。
+具体来说,当 ``__alltraps`` 保存 Trap 上下文的时候,我们必须通过修改 satp 从应用地址空间切换到内核地址空间,
+因为 trap handler 只有在内核地址空间中才能访问;
+同理,在 ``__restore`` 恢复 Trap 上下文的时候,我们也必须从内核地址空间切换回应用地址空间,因为应用的代码和
+数据只能在它自己的地址空间中才能访问,内核地址空间是看不到的。
+进而,地址空间的切换不能影响指令的连续执行,这就要求应用和内核地址空间在切换地址空间指令附近是平滑的。
+
+.. _term-meltdown:
+
+.. note::
+
+ **内核与应用地址空间的隔离**
+
+ 目前我们的设计是有一个唯一的内核地址空间存放内核的代码、数据,同时对于每个应用维护一个它们自己的地址空间,因此在
+ Trap 的时候就需要进行地址空间切换,而在任务切换的时候无需进行(因为这个过程全程在内核内完成)。而教程前两版以及
+ :math:`\mu` core 中的设计是每个应用都有一个地址空间,可以将其中的逻辑段分为内核和用户两部分,分别映射到内核和
+ 用户的数据和代码,且分别在 CPU 处于 S/U 特权级时访问。此设计中并不存在一个单独的内核地址空间。
+
+ 之前设计方式的优点在于: Trap 的时候无需切换地址空间,而在任务切换的时候才需要切换地址空间。由于后者比前者更容易
+ 实现,这降低了实现的复杂度。而且在应用高频进行系统调用的时候能够避免地址空间切换的开销,这通常源于快表或 cache
+ 的失效问题。但是这种设计方式也有缺点:即内核的逻辑段需要在每个应用的地址空间内都映射一次,这会带来一些无法忽略的
+ 内存占用开销,并显著限制了嵌入式平台的任务并发数。此外,这种做法无法应对处理器的 `熔断
+ (Meltdown) 漏洞 `_ ,
+ 使得恶意应用能够以某种方式看到它本来无权访问的地址空间中内核部分的数据。将内核与地址空间隔离便是修复此漏洞的一种方法。
+
+ 经过权衡,在本教程中我们参考 MIT 的教学 OS `xv6 `_ ,
+ 采用内核和应用地址空间隔离的设计。
+
+我们为何将应用的 Trap 上下文放到应用地址空间的次高页面而不是内核地址空间中的内核栈中呢?原因在于,假如我们将其放在内核栈
+中,在保存 Trap 上下文之前我们必须先切换到内核地址空间,这就需要我们将内核地址空间的 token 写入 satp 寄存器,之后我们
+还需要有一个通用寄存器保存内核栈栈顶的位置,这样才能以它为基址保存 Trap 上下文。在保存 Trap 上下文之前我们必须完成这
+两项工作。然而,我们无法在不破坏任何一个通用寄存器的情况下做到这一点。因为事实上我们需要用到内核的两条信息:内核地址空间
+的 token 还有应用内核栈顶的位置,硬件却只提供一个 ``sscratch`` 可以用来进行周转。所以,我们不得不将 Trap 上下文保存在
+应用地址空间的一个虚拟页面中以避免切换到内核地址空间才能保存。
+
+为了方便实现,我们在 Trap 上下文中包含更多内容(和我们关于上下文的定义有些不同,它们在初始化之后便只会被读取而不会被写入
+,并不是每次都需要保存/恢复):
+
+.. code-block:: rust
+ :linenos:
+ :emphasize-lines: 8,9,10
+
+ // os/src/trap/context.rs
+
+ #[repr(C)]
+ pub struct TrapContext {
+ pub x: [usize; 32],
+ pub sstatus: Sstatus,
+ pub sepc: usize,
+ pub kernel_satp: usize,
+ pub kernel_sp: usize,
+ pub trap_handler: usize,
+ }
+
+在多出的三个字段中:
+
+- ``kernel_satp`` 表示内核地址空间的 token ;
+- ``kernel_sp`` 表示当前应用在内核地址空间中的内核栈栈顶的虚拟地址;
+- ``trap_handler`` 表示内核中 trap handler 入口点的虚拟地址。
+
+它们在应用初始化的时候由内核写入应用地址空间中的 TrapContext 的相应位置,此后就不再被修改。
+
+让我们来看一下现在的 ``__alltraps`` 和 ``__restore`` 各是如何在保存和恢复 Trap 上下文的同时也切换地址空间的:
+
+.. code-block:: riscv
+ :linenos:
+
+ # os/src/trap/trap.S
+
+ .section .text.trampoline
+ .globl __alltraps
+ .globl __restore
+ .align 2
+ __alltraps:
+ csrrw sp, sscratch, sp
+ # now sp->*TrapContext in user space, sscratch->user stack
+ # save other general purpose registers
+ sd x1, 1*8(sp)
+ # skip sp(x2), we will save it later
+ sd x3, 3*8(sp)
+ # skip tp(x4), application does not use it
+ # save x5~x31
+ .set n, 5
+ .rept 27
+ SAVE_GP %n
+ .set n, n+1
+ .endr
+ # we can use t0/t1/t2 freely, because they have been saved in TrapContext
+ csrr t0, sstatus
+ csrr t1, sepc
+ sd t0, 32*8(sp)
+ sd t1, 33*8(sp)
+ # read user stack from sscratch and save it in TrapContext
+ csrr t2, sscratch
+ sd t2, 2*8(sp)
+ # load kernel_satp into t0
+ ld t0, 34*8(sp)
+ # load trap_handler into t1
+ ld t1, 36*8(sp)
+ # move to kernel_sp
+ ld sp, 35*8(sp)
+ # switch to kernel space
+ csrw satp, t0
+ sfence.vma
+ # jump to trap_handler
+ jr t1
+
+ __restore:
+ # a0: *TrapContext in user space(Constant); a1: user space token
+ # switch to user space
+ csrw satp, a1
+ sfence.vma
+ csrw sscratch, a0
+ mv sp, a0
+ # now sp points to TrapContext in user space, start restoring based on it
+ # restore sstatus/sepc
+ ld t0, 32*8(sp)
+ ld t1, 33*8(sp)
+ csrw sstatus, t0
+ csrw sepc, t1
+ # restore general purpose registers except x0/sp/tp
+ ld x1, 1*8(sp)
+ ld x3, 3*8(sp)
+ .set n, 5
+ .rept 27
+ LOAD_GP %n
+ .set n, n+1
+ .endr
+ # back to user stack
+ ld sp, 2*8(sp)
+ sret
+
+- 当应用 Trap 进入内核的时候,硬件会设置一些 CSR 并在 S 特权级下跳转到 ``__alltraps`` 保存 Trap 上下文。此时
+ sp 寄存器仍指向用户栈,但 ``sscratch`` 则被设置为指向应用地址空间中存放 Trap 上下文的位置,实际在次高页面。
+ 随后,就像之前一样,我们 ``csrrw`` 交换 sp 和 ``sscratch`` ,并基于指向 Trap 上下文位置的 sp 开始保存通用
+ 寄存器和一些 CSR ,这个过程在第 28 行结束。到这里,我们就全程在应用地址空间中完成了保存 Trap 上下文的工作。
+
+- 接下来该考虑切换到内核地址空间并跳转到 trap handler 了。第 30 行我们将内核地址空间的 token 载入到 t0 寄存器中,
+ 第 32 行我们将 trap handler 入口点的虚拟地址载入到 t1 寄存器中,第 34 行我们直接将 sp 修改为应用内核栈顶的地址。
+ 这三条信息均是内核在初始化该应用的时候就已经设置好的。第 36~37 行我们将 satp 修改为内核地址空间的 token 并使用
+ ``sfence.vma`` 刷新快表,这就切换到了内核地址空间。最后在第 39 行我们通过 ``jr`` 指令跳转到 t1 寄存器所保存的
+ trap handler 入口点的地址。注意这里我们不能像之前的章节那样直接 ``call trap_handler`` ,原因稍后解释。
+- 当内核将 Trap 处理完毕准备返回用户态的时候会 *调用* ``__restore`` ,它有两个参数:第一个是 Trap 上下文在应用
+ 地址空间中的位置,这个对于所有的应用来说都是相同的,由调用规范在 a0 寄存器中传递;第二个则是即将回到的应用的地址空间
+ 的 token ,在 a1 寄存器中传递。由于 Trap 上下文是保存在应用地址空间中的,第 44~45 行我们先切换回应用地址空间。第
+ 46 行我们将传入的 Trap 上下文位置保存在 ``sscratch`` 寄存器中,这样 ``__alltraps`` 中才能基于它将 Trap 上下文
+ 保存到正确的位置。第 47 行我们将 sp 修改为 Trap 上下文的位置,后面基于它恢复各通用寄存器和 CSR。最后在第 64 行,
+ 我们通过 ``sret`` 指令返回用户态。
+
+接下来还需要考虑切换地址空间前后指令能否仍能连续执行。可以看到我们将 ``trap.S`` 中的整段汇编代码放置在
+``.text.trampoline`` 段,并在调整内存布局的时候将它对齐到代码段的一个页面中:
+
+.. code-block:: diff
+ :linenos:
+
+ # os/src/linker.ld
+
+ stext = .;
+ .text : {
+ *(.text.entry)
+ + . = ALIGN(4K);
+ + strampoline = .;
+ + *(.text.trampoline);
+ + . = ALIGN(4K);
+ *(.text .text.*)
+ }
+
+这样,这段汇编代码放在一个物理页帧中,且 ``__alltraps`` 恰好位于这个物理页帧的开头,其物理地址被外部符号
+``strampoline`` 标记。在开启分页模式之后,内核和应用代码都只能看到各自的虚拟地址空间,而在它们的视角中,这段汇编代码
+被放在它们地址空间的最高虚拟页面上,由于这段汇编代码在执行的时候涉及到地址空间切换,故而被称为跳板页面。
+
+那么在产生trap前后的一小段时间内会有一个比较 **极端** 的情况,即刚产生trap时,CPU已经进入了内核态(即Supervisor Mode),
+但此时执行代码和访问数据还是在应用程序所处的用户态虚拟地址空间中,而不是我们通常理解的内核虚拟地址空间。在这段特殊的时间内,CPU指令
+为什么能够被连续执行呢?这里需要注意:无论是内核还是应用的地址空间,跳板的虚拟页均位于同样位置,且它们也将会映射到同一个实际存放这段
+汇编代码的物理页帧。也就是说,在执行 ``__alltraps`` 或 ``__restore`` 函数进行地址空间切换的时候,
+应用的用户态虚拟地址空间和操作系统内核的内核态虚拟地址空间对切换地址空间的指令所在页的映射方式均是相同的,
+这就说明了这段切换地址空间的指令控制流仍是可以连续执行的。
+
+现在可以说明我们在创建用户/内核地址空间中用到的 ``map_trampoline`` 是如何实现的了:
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/config.rs
+
+ pub const TRAMPOLINE: usize = usize::MAX - PAGE_SIZE + 1;
+
+ // os/src/mm/memory_set.rs
+
+ impl MemorySet {
+ /// Mention that trampoline is not collected by areas.
+ fn map_trampoline(&mut self) {
+ self.page_table.map(
+ VirtAddr::from(TRAMPOLINE).into(),
+ PhysAddr::from(strampoline as usize).into(),
+ PTEFlags::R | PTEFlags::X,
+ );
+ }
+ }
+
+这里我们为了实现方便并没有新增逻辑段 ``MemoryArea`` 而是直接在多级页表中插入一个从地址空间的最高虚拟页面映射到
+跳板汇编代码所在的物理页帧的键值对,访问方式限制与代码段相同,即 RX 。
+
+最后可以解释为何我们在 ``__alltraps`` 中需要借助寄存器 ``jr`` 而不能直接 ``call trap_handler`` 了。因为在
+内存布局中,这条 ``.text.trampoline`` 段中的跳转指令和 ``trap_handler`` 都在代码段之内,汇编器(Assembler)
+和链接器(Linker)会根据 ``linker.ld`` 的地址布局描述,设定电子指令的地址,并计算二者地址偏移量
+并让跳转指令的实际效果为当前 pc 自增这个偏移量。但实际上我们知道由于我们设计的缘故,这条跳转指令在被执行的时候,
+它的虚拟地址被操作系统内核设置在地址空间中的最高页面之内,加上这个偏移量并不能正确的得到 ``trap_handler`` 的入口地址。
+
+**问题的本质可以概括为:跳转指令实际被执行时的虚拟地址和在编译器/汇编器/链接器进行后端代码生成和链接形成最终机器码时设置此指令的地址是不同的。**
+
+加载和执行应用程序
+------------------------------------
+
+扩展任务控制块
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+为了让应用在运行时有一个安全隔离且符合编译器给应用设定的地址空间布局的虚拟地址空间,操作系统需要对任务进行更多的管理,所以任务控制块相比第三章也包含了更多内容:
+
+.. code-block:: rust
+ :linenos:
+ :emphasize-lines: 6,7,8
+
+ // os/src/task/task.rs
+
+ pub struct TaskControlBlock {
+ pub task_status: TaskStatus,
+ pub task_cx: TaskContext,
+ pub memory_set: MemorySet,
+ pub trap_cx_ppn: PhysPageNum,
+ pub base_size: usize,
+ }
+
+除了应用的地址空间 ``memory_set`` 之外,还有位于应用地址空间次高页的 Trap 上下文被实际存放在物理页帧的物理页号
+``trap_cx_ppn`` ,它能够方便我们对于 Trap 上下文进行访问。此外, ``base_size`` 统计了应用数据的大小,也就是
+在应用地址空间中从 :math:`\text{0x0}` 开始到用户栈结束一共包含多少字节。它后续还应该包含用于应用动态内存分配的
+堆空间的大小,但我们暂不支持。
+
+
+
+更新对任务控制块的管理
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+下面是任务控制块的创建:
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/config.rs
+
+ /// Return (bottom, top) of a kernel stack in kernel space.
+ pub fn kernel_stack_position(app_id: usize) -> (usize, usize) {
+ let top = TRAMPOLINE - app_id * (KERNEL_STACK_SIZE + PAGE_SIZE);
+ let bottom = top - KERNEL_STACK_SIZE;
+ (bottom, top)
+ }
+
+ // os/src/task/task.rs
+
+ impl TaskControlBlock {
+ pub fn new(elf_data: &[u8], app_id: usize) -> Self {
+ // memory_set with elf program headers/trampoline/trap context/user stack
+ let (memory_set, user_sp, entry_point) = MemorySet::from_elf(elf_data);
+ let trap_cx_ppn = memory_set
+ .translate(VirtAddr::from(TRAP_CONTEXT).into())
+ .unwrap()
+ .ppn();
+ let task_status = TaskStatus::Ready;
+ // map a kernel-stack in kernel space
+ let (kernel_stack_bottom, kernel_stack_top) = kernel_stack_position(app_id);
+ KERNEL_SPACE
+ .exclusive_access()
+ .insert_framed_area(
+ kernel_stack_bottom.into(),
+ kernel_stack_top.into(),
+ MapPermission::R | MapPermission::W,
+ );
+ let task_control_block = Self {
+ task_status,
+ task_cx: TaskContext::goto_trap_return(kernel_stack_top),
+ memory_set,
+ trap_cx_ppn,
+ base_size: user_sp,
+ };
+ // prepare TrapContext in user space
+ let trap_cx = task_control_block.get_trap_cx();
+ *trap_cx = TrapContext::app_init_context(
+ entry_point,
+ user_sp,
+ KERNEL_SPACE.exclusive_access().token(),
+ kernel_stack_top,
+ trap_handler as usize,
+ );
+ task_control_block
+ }
+ }
+
+- 第 15 行,我们解析传入的 ELF 格式数据构造应用的地址空间 ``memory_set`` 并获得其他信息;
+- 第 16 行,我们从地址空间 ``memory_set`` 中查多级页表找到应用地址空间中的 Trap 上下文实际被放在哪个物理页帧;
+- 第 22 行,我们根据传入的应用 ID ``app_id`` 调用在 ``config`` 子模块中定义的 ``kernel_stack_position`` 找到
+ 应用的内核栈预计放在内核地址空间 ``KERNEL_SPACE`` 中的哪个位置,并通过 ``insert_framed_area`` 实际将这个逻辑段
+ 加入到内核地址空间中;
+
+.. _trap-return-intro:
+
+- 我们在应用的内核栈顶压入一个跳转到 ``trap_return`` 而不是 ``__restore`` 的任务上下文,
+ 这主要是为了能够支持对该应用的启动并顺利切换到用户地址空间执行。在构造方式上,只是将 ra 寄存器的值设置为
+ ``trap_return`` 的地址。 ``trap_return`` 是我们后面要介绍的新版的 Trap 处理的一部分。
+- 初始化该应用的 Trap 上下文,由于它是在应用地址空间而不是在内核地址空间中,我们只能手动查页表找到
+ Trap 上下文实际被放在的物理页帧,再获得在用户空间的 Trap 上下文的可变引用用于初始化:
+
+ .. code-block:: rust
+
+ // os/src/task/task.rs
+
+ impl TaskControlBlock {
+ pub fn get_trap_cx(&self) -> &'static mut TrapContext {
+ self.trap_cx_ppn.get_mut()
+ }
+ }
+
+ 此处需要说明的是,返回 ``'static`` 的可变引用和之前一样可以看成一个绕过 unsafe 的裸指针;而 ``PhysPageNum::get_mut``
+ 是一个泛型函数,由于我们已经声明了总体返回 ``TrapContext`` 的可变引用,则Rust编译器会给 ``get_mut`` 泛型函数针对具体类型 ``TrapContext``
+ 的情况生成一个特定版本的 ``get_mut`` 函数实现。在 ``get_trap_cx`` 函数中则会静态调用``get_mut`` 泛型函数的特定版本实现。
+
+ .. code-block:: rust
+ :linenos:
+ :emphasize-lines: 8,9,10,18,19,20
+
+ // os/src/trap/context.rs
+
+ impl TrapContext {
+ pub fn set_sp(&mut self, sp: usize) { self.x[2] = sp; }
+ pub fn app_init_context(
+ entry: usize,
+ sp: usize,
+ kernel_satp: usize,
+ kernel_sp: usize,
+ trap_handler: usize,
+ ) -> Self {
+ let mut sstatus = sstatus::read();
+ sstatus.set_spp(SPP::User);
+ let mut cx = Self {
+ x: [0; 32],
+ sstatus,
+ sepc: entry,
+ kernel_satp,
+ kernel_sp,
+ trap_handler,
+ };
+ cx.set_sp(sp);
+ cx
+ }
+ }
+
+ 和之前相比 ``TrapContext::app_init_context`` 需要补充上让应用在 ``__alltraps`` 能够顺利进入到内核地址空间
+ 并跳转到 trap handler 入口点的相关信息。
+
+在内核初始化的时候,需要将所有的应用加载到全局应用管理器中:
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/task/mod.rs
+
+ struct TaskManagerInner {
+ tasks: Vec,
+ current_task: usize,
+ }
+
+ lazy_static! {
+ pub static ref TASK_MANAGER: TaskManager = {
+ info!("init TASK_MANAGER");
+ let num_app = get_num_app();
+ info!("num_app = {}", num_app);
+ let mut tasks: Vec = Vec::new();
+ for i in 0..num_app {
+ tasks.push(TaskControlBlock::new(get_app_data(i), i));
+ }
+ TaskManager {
+ num_app,
+ inner: unsafe {
+ UPSafeCell::new(TaskManagerInner {
+ tasks,
+ current_task: 0,
+ })
+ },
+ }
+ };
+ }
+
+可以看到,在 ``TaskManagerInner`` 中我们使用向量 ``Vec`` 来保存任务控制块。在全局任务管理器 ``TASK_MANAGER``
+初始化的时候,只需使用 ``loader`` 子模块提供的 ``get_num_app`` 和 ``get_app_data`` 分别获取链接到内核的应用
+数量和每个应用的 ELF 文件格式的数据,然后依次给每个应用创建任务控制块并加入到向量中即可。我们还将 ``current_task`` 设置
+为 0 ,于是将从第 0 个应用开始执行。
+
+回过头来介绍一下应用构建器 ``os/build.rs`` 的改动:
+
+- 首先,我们在 ``.incbin`` 中不再插入清除全部符号的应用二进制镜像 ``*.bin`` ,而是将构建得到的 ELF 格式文件直接链接进来;
+- 其次,在链接每个 ELF 格式文件之前我们都加入一行 ``.align 3`` 来确保它们对齐到 8 字节,这是由于如果不这样做,
+ ``xmas-elf`` crate 可能会在解析 ELF 的时候进行不对齐的内存读写,例如使用 ``ld`` 指令从内存的一个没有对齐到 8 字节的地址加载一个 64 位的值到一个通用寄存器。
+
+为了方便后续的实现,全局任务管理器还需要提供关于当前应用与地址空间有关的一些信息。通过 ``current_user_token`` 和
+``current_trap_cx`` 分别可以获得当前正在执行的应用的地址空间的 token 和可以在
+内核地址空间中修改位于该应用地址空间中的 Trap 上下文的可变引用。
+
+改进 Trap 处理的实现
+------------------------------------
+
+为了能够支持地址空间,让我们来看现在 ``trap_handler`` 的改进实现:
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/trap/mod.rs
+
+ fn set_kernel_trap_entry() {
+ unsafe {
+ stvec::write(trap_from_kernel as usize, TrapMode::Direct);
+ }
+ }
+
+ #[no_mangle]
+ pub fn trap_from_kernel() -> ! {
+ panic!("a trap from kernel!");
+ }
+
+ #[no_mangle]
+ pub fn trap_handler() -> ! {
+ set_kernel_trap_entry();
+ let cx = current_trap_cx();
+ let scause = scause::read();
+ let stval = stval::read();
+ match scause.cause() {
+ ...
+ }
+ trap_return();
+ }
+
+由于应用的 Trap 上下文不在内核地址空间,因此我们调用 ``current_trap_cx`` 来获取当前应用的 Trap 上下文的可变引用
+而不是像之前那样作为参数传入 ``trap_handler`` 。至于 Trap 处理的过程则没有发生什么变化。
+
+注意到,在 ``trap_handler`` 的开头还调用 ``set_kernel_trap_entry`` 将 ``stvec`` 修改为同模块下另一个函数
+``trap_from_kernel`` 的地址。这就是说,一旦进入内核后再次触发到 S 的 Trap,则会在硬件设置一些 CSR 之后跳过寄存器
+的保存过程直接跳转到 ``trap_from_kernel`` 函数,在这里我们直接 ``panic`` 退出。这是因为内核和应用的地址空间分离
+之后,从 U 还是从 S Trap 到 S 的 Trap 上下文保存与恢复实现方式和 Trap 处理逻辑有很大差别,我们不得不实现两遍而
+不太可能将二者整合起来。这里简单起见我们弱化了从 S 到 S 的 Trap ,省略了 Trap 上下文保存过程而直接 ``panic`` 。
+
+在 ``trap_handler`` 完成 Trap 处理之后,我们需要调用 ``trap_return`` 返回用户态:
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/trap/mod.rs
+
+ fn set_user_trap_entry() {
+ unsafe {
+ stvec::write(TRAMPOLINE as usize, TrapMode::Direct);
+ }
+ }
+
+ #[no_mangle]
+ pub fn trap_return() -> ! {
+ set_user_trap_entry();
+ let trap_cx_ptr = TRAP_CONTEXT;
+ let user_satp = current_user_token();
+ extern "C" {
+ fn __alltraps();
+ fn __restore();
+ }
+ let restore_va = __restore as usize - __alltraps as usize + TRAMPOLINE;
+ unsafe {
+ core::arch::asm!(
+ "fence.i",
+ "jr {restore_va}",
+ restore_va = in(reg) restore_va,
+ in("a0") trap_cx_ptr,
+ in("a1") user_satp,
+ options(noreturn)
+ );
+ }
+ panic!("Unreachable in back_to_user!");
+ }
+
+- 第 11 行,在 ``trap_return`` 的开头我们调用 ``set_user_trap_entry`` 来让应用 Trap 到 S 的时候可以跳转到
+ ``__alltraps`` 。注意我们把 ``stvec`` 设置为内核和应用地址空间共享的跳板页面的起始地址 ``TRAMPOLINE`` 而不是
+ 编译器在链接时看到的 ``__alltraps`` 的地址,因为启用分页模式之后我们只能通过跳板页面上的虚拟地址来实际取得
+ ``__alltraps`` 和 ``__restore`` 的汇编代码。
+- 之前介绍的时候提到过 ``__restore`` 需要两个参数:分别是 Trap 上下文在应用地址空间中的虚拟地址和要继续执行的应用
+ 地址空间的 token 。第 12 和第 13 行则分别准备好这两个参数。
+- 最后我们需要跳转到 ``__restore`` 切换到应用地址空间从 Trap 上下文中恢复通用寄存器并 ``sret`` 继续执行应用。它的
+ 关键在于如何找到 ``__restore`` 在内核/应用地址空间中共同的虚拟地址。第 18 行我们展示了计算它的过程:由于
+ ``__alltraps`` 是对齐到地址空间跳板页面的起始地址 ``TRAMPOLINE`` 上的, 则 ``__restore`` 的虚拟地址只需在
+ ``TRAMPOLINE`` 基础上加上 ``__restore`` 相对于 ``__alltraps`` 的偏移量即可。这里 ``__alltraps`` 和
+ ``__restore`` 都是指编译器在链接时看到的内核内存布局中的地址。我们使用 ``jr`` 指令完成了跳转的任务。
+- 在开始执行应用之前,我们需要使用 ``fence.i`` 指令清空指令缓存 i-cache 。这是因为,在内核中进行的一些操作
+ 可能导致一些原先存放某个应用代码的物理页帧如今用来存放数据或者是其他应用的代码,i-cache 中可能还保存着该物理页帧的
+ 错误快照。因此我们直接将整个 i-cache 清空避免错误。
+
+改进 sys_write 的实现
+------------------------------------
+
+同样由于内核和应用地址空间的隔离, ``sys_write`` 不再能够直接访问位于应用空间中的数据,而需要手动查页表才能知道那些
+数据被放置在哪些物理页帧上并进行访问。
+
+为此,页表模块 ``page_table`` 提供了将应用地址空间中一个缓冲区转化为在内核空间中能够直接访问的形式的辅助函数:
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/mm/page_table.rs
+
+ pub fn translated_byte_buffer(
+ token: usize,
+ ptr: *const u8,
+ len: usize
+ ) -> Vec<&'static [u8]> {
+ let page_table = PageTable::from_token(token);
+ let mut start = ptr as usize;
+ let end = start + len;
+ let mut v = Vec::new();
+ while start < end {
+ let start_va = VirtAddr::from(start);
+ let mut vpn = start_va.floor();
+ let ppn = page_table
+ .translate(vpn)
+ .unwrap()
+ .ppn();
+ vpn.step();
+ let mut end_va: VirtAddr = vpn.into();
+ end_va = end_va.min(VirtAddr::from(end));
+ v.push(&ppn.get_bytes_array()[start_va.page_offset()..end_va.page_offset()]);
+ start = end_va.into();
+ }
+ v
+ }
+
+参数中的 ``token`` 是某个应用地址空间的 token , ``ptr`` 和 ``len`` 则分别表示该地址空间中的一段缓冲区的起始地址
+和长度。 ``translated_byte_buffer`` 会以向量的形式返回一组可以在内核空间中直接访问的字节数组切片,具体实现在这里
+不再赘述。
+
+进而,我们完成对 ``sys_write`` 系统调用的改造:
+
+.. code-block:: rust
+
+ // os/src/syscall/fs.rs
+
+ pub fn sys_write(fd: usize, buf: *const u8, len: usize) -> isize {
+ match fd {
+ FD_STDOUT => {
+ let buffers = translated_byte_buffer(current_user_token(), buf, len);
+ for buffer in buffers {
+ print!("{}", core::str::from_utf8(buffer).unwrap());
+ }
+ len as isize
+ },
+ _ => {
+ panic!("Unsupported fd in sys_write!");
+ }
+ }
+ }
+
+我们尝试将每个字节数组切片转化为字符串 ``&str`` 然后输出即可。
+
diff --git a/guide/source/chapter4/7exercise.rst b/guide/source/chapter4/7exercise.rst
new file mode 100644
index 0000000..c14f860
--- /dev/null
+++ b/guide/source/chapter4/7exercise.rst
@@ -0,0 +1,113 @@
+chapter4练习
+============================================
+
+编程作业
+---------------------------------------------
+
+重写 sys_get_time 和 sys_task_info
+++++++++++++++++++++++++++++++++++++++++++++
+
+引入虚存机制后,原来内核的 sys_get_time 和 sys_task_info 函数实现就无效了。请你重写这个函数,恢复其正常功能。
+
+mmap 和 munmap 匿名映射
+++++++++++++++++++++++++++++++++++++++++++++
+
+`mmap `_ 在 Linux 中主要用于在内存中映射文件,
+本次实验简化它的功能,仅用于申请内存。
+
+请实现 mmap 和 munmap 系统调用,mmap 定义如下:
+
+
+.. code-block:: rust
+
+ fn sys_mmap(start: usize, len: usize, port: usize) -> isize
+
+- syscall ID:222
+- 申请长度为 len 字节的物理内存(不要求实际物理内存位置,可以随便找一块),将其映射到 start 开始的虚存,内存页属性为 port
+- 参数:
+ - start 需要映射的虚存起始地址,要求按页对齐
+ - len 映射字节长度,可以为 0
+ - port:第 0 位表示是否可读,第 1 位表示是否可写,第 2 位表示是否可执行。其他位无效且必须为 0
+- 返回值:执行成功则返回 0,错误返回 -1
+- 说明:
+ - 为了简单,目标虚存区间要求按页对齐,len 可直接按页向上取整,不考虑分配失败时的页回收。
+- 可能的错误:
+ - start 没有按页大小对齐
+ - port & !0x7 != 0 (port 其余位必须为0)
+ - port & 0x7 = 0 (这样的内存无意义)
+ - [start, start + len) 中存在已经被映射的页
+ - 物理内存不足
+
+munmap 定义如下:
+
+.. code-block:: rust
+
+ fn sys_munmap(start: usize, len: usize) -> isize
+
+- syscall ID:215
+- 取消到 [start, start + len) 虚存的映射
+- 参数和返回值请参考 mmap
+- 说明:
+ - 为了简单,参数错误时不考虑内存的恢复和回收。
+- 可能的错误:
+ - [start, start + len) 中存在未被映射的虚存。
+
+tips:
+
+- 一定要注意 mmap 是的页表项,注意 riscv 页表项的格式与 port 的区别。
+- 你增加 PTE_U 了吗?
+
+实验要求
+++++++++++++++++++++++++++++++++++++++++++
+
+- 实现分支:ch4。
+- 实现 mmap 和 munmap 两个系统调用,通过所有测例。
+- 实验目录请参考 ch3,报告命名 lab2.md/pdf
+
+TIPS:注意 port 参数的语义,它与内核定义的 MapPermission 有明显不同!
+
+问答作业
+-------------------------------------------------
+
+1. 请列举 SV39 页表页表项的组成,描述其中的标志位有何作用?
+
+2. 缺页
+ 缺页指的是进程访问页面时页面不在页表中或在页表中无效的现象,此时 MMU 将会返回一个中断,
+ 告知 os 进程内存访问出了问题。os 选择填补页表并重新执行异常指令或者杀死进程。
+
+ - 请问哪些异常可能是缺页导致的?
+ - 发生缺页时,描述相关重要寄存器的值,上次实验描述过的可以简略。
+
+ 缺页有两个常见的原因,其一是 Lazy 策略,也就是直到内存页面被访问才实际进行页表操作。
+ 比如,一个程序被执行时,进程的代码段理论上需要从磁盘加载到内存。但是 os 并不会马上这样做,
+ 而是会保存 .text 段在磁盘的位置信息,在这些代码第一次被执行时才完成从磁盘的加载操作。
+
+ - 这样做有哪些好处?
+
+ 其实,我们的 mmap 也可以采取 Lazy 策略,比如:一个用户进程先后申请了 10G 的内存空间,
+ 然后用了其中 1M 就直接退出了。按照现在的做法,我们显然亏大了,进行了很多没有意义的页表操作。
+
+ - 处理 10G 连续的内存页面,对应的 SV39 页表大致占用多少内存 (估算数量级即可)?
+ - 请简单思考如何才能实现 Lazy 策略,缺页时又如何处理?描述合理即可,不需要考虑实现。
+
+ 缺页的另一个常见原因是 swap 策略,也就是内存页面可能被换到磁盘上了,导致对应页面失效。
+
+ - 此时页面失效如何表现在页表项(PTE)上?
+
+3. 双页表与单页表
+
+ 为了防范侧信道攻击,我们的 os 使用了双页表。但是传统的设计一直是单页表的,也就是说,
+ 用户线程和对应的内核线程共用同一张页表,只不过内核对应的地址只允许在内核态访问。
+ (备注:这里的单/双的说法仅为自创的通俗说法,并无这个名词概念,详情见 `KPTI `_ )
+
+ - 在单页表情况下,如何更换页表?
+ - 单页表情况下,如何控制用户态无法访问内核页面?(tips:看看上一题最后一问)
+ - 单页表有何优势?(回答合理即可)
+ - 双页表实现下,何时需要更换页表?假设你写一个单页表操作系统,你会选择何时更换页表(回答合理即可)?
+
+报告要求
+--------------------------------------------------------
+
+- 简单总结你实现的功能(200字以内,不要贴代码)。
+- 完成问答题。
+- (optional) 你对本次实验设计及难度/工作量的看法,以及有哪些需要改进的地方,欢迎畅所欲言。
diff --git a/guide/source/chapter4/address-translation.png b/guide/source/chapter4/address-translation.png
new file mode 100644
index 0000000..949120a
Binary files /dev/null and b/guide/source/chapter4/address-translation.png differ
diff --git a/guide/source/chapter4/app-as-full.png b/guide/source/chapter4/app-as-full.png
new file mode 100644
index 0000000..796c23e
Binary files /dev/null and b/guide/source/chapter4/app-as-full.png differ
diff --git a/guide/source/chapter4/index.rst b/guide/source/chapter4/index.rst
new file mode 100644
index 0000000..715e8e2
--- /dev/null
+++ b/guide/source/chapter4/index.rst
@@ -0,0 +1,12 @@
+第四章:地址空间
+==============================================
+
+.. toctree::
+ :maxdepth: 4
+
+ 0intro
+ 3sv39-implementation-1
+ 4sv39-implementation-2
+ 5kernel-app-spaces
+ 6multitasking-based-on-as
+ 7exercise
diff --git a/guide/source/chapter4/kernel-as-high.png b/guide/source/chapter4/kernel-as-high.png
new file mode 100644
index 0000000..344d94d
Binary files /dev/null and b/guide/source/chapter4/kernel-as-high.png differ
diff --git a/guide/source/chapter4/kernel-as-low.png b/guide/source/chapter4/kernel-as-low.png
new file mode 100644
index 0000000..8cf78a4
Binary files /dev/null and b/guide/source/chapter4/kernel-as-low.png differ
diff --git a/guide/source/chapter4/linear-table.png b/guide/source/chapter4/linear-table.png
new file mode 100644
index 0000000..ce3d95f
Binary files /dev/null and b/guide/source/chapter4/linear-table.png differ
diff --git a/guide/source/chapter4/page-table.png b/guide/source/chapter4/page-table.png
new file mode 100644
index 0000000..0d3715d
Binary files /dev/null and b/guide/source/chapter4/page-table.png differ
diff --git a/guide/source/chapter4/pte-rwx.png b/guide/source/chapter4/pte-rwx.png
new file mode 100644
index 0000000..e2c20df
Binary files /dev/null and b/guide/source/chapter4/pte-rwx.png differ
diff --git a/guide/source/chapter4/rust-containers.png b/guide/source/chapter4/rust-containers.png
new file mode 100644
index 0000000..239ecba
Binary files /dev/null and b/guide/source/chapter4/rust-containers.png differ
diff --git a/guide/source/chapter4/satp.png b/guide/source/chapter4/satp.png
new file mode 100644
index 0000000..33357b7
Binary files /dev/null and b/guide/source/chapter4/satp.png differ
diff --git a/guide/source/chapter4/segmentation.png b/guide/source/chapter4/segmentation.png
new file mode 100644
index 0000000..46bb1e6
Binary files /dev/null and b/guide/source/chapter4/segmentation.png differ
diff --git a/guide/source/chapter4/simple-base-bound.png b/guide/source/chapter4/simple-base-bound.png
new file mode 100755
index 0000000..55cb021
Binary files /dev/null and b/guide/source/chapter4/simple-base-bound.png differ
diff --git a/guide/source/chapter4/sv39-full.png b/guide/source/chapter4/sv39-full.png
new file mode 100644
index 0000000..5678542
Binary files /dev/null and b/guide/source/chapter4/sv39-full.png differ
diff --git a/guide/source/chapter4/sv39-pte.png b/guide/source/chapter4/sv39-pte.png
new file mode 100644
index 0000000..7f69390
Binary files /dev/null and b/guide/source/chapter4/sv39-pte.png differ
diff --git a/guide/source/chapter4/sv39-va-pa.png b/guide/source/chapter4/sv39-va-pa.png
new file mode 100644
index 0000000..daf34be
Binary files /dev/null and b/guide/source/chapter4/sv39-va-pa.png differ
diff --git a/guide/source/chapter4/trie-1.png b/guide/source/chapter4/trie-1.png
new file mode 100644
index 0000000..29df0c1
Binary files /dev/null and b/guide/source/chapter4/trie-1.png differ
diff --git a/guide/source/chapter4/trie.png b/guide/source/chapter4/trie.png
new file mode 100644
index 0000000..0a6e3cf
Binary files /dev/null and b/guide/source/chapter4/trie.png differ
diff --git a/guide/source/chapter5/0intro.rst b/guide/source/chapter5/0intro.rst
new file mode 100644
index 0000000..4bbf4e7
--- /dev/null
+++ b/guide/source/chapter5/0intro.rst
@@ -0,0 +1,175 @@
+引言
+===========================================
+
+本章导读
+-------------------------------------------
+
+我们将开发一个用户 **终端** (Terminal) 或 **命令行** (Command Line Application, 俗称 **Shell** ) ,
+形成用户与操作系统进行交互的命令行界面 (Command Line Interface)。
+
+为此,我们要对任务建立新的抽象: **进程** ,并实现若干基于 **进程** 的强大系统调用。
+
+.. note::
+
+ **任务和进程的关系与区别**
+
+ 第三章提到的 **任务** 是这里提到的 **进程** 的初级阶段,与任务相比,进程能在运行中创建 **子进程** 、
+ 用新的 **程序** 内容覆盖已有的 **程序** 内容、可管理更多物理或虚拟 **资源** 。
+
+实践体验
+-------------------------------------------
+
+获取本章代码:
+
+.. code-block:: console
+
+ $ git clone https://github.com/LearningOS/rCore-Tutorial-Code-2022S.git
+ $ cd rCore-Tutorial-Code-2022S
+ $ git checkout ch5
+ $ git clone https://github.com/LearningOS/rCore-Tutorial-Test-2022S.git user
+
+请仿照ch4的做法将代码在本地更新并push到自己的实验仓库中。
+
+注意:user仓库有对ch5的测例更新,请重新clone或者使用git pull等获取。
+
+在 qemu 模拟器上运行本章代码:
+
+.. code-block:: console
+
+ $ cd os
+ $ make run
+
+待内核初始化完毕之后,将在屏幕上打印可用的应用列表并进入shell程序:
+
+.. code-block::
+
+ [rustsbi] RustSBI version 0.2.0-alpha.4
+ .______ __ __ _______.___________. _______..______ __
+ | _ \ | | | | / | | / || _ \ | |
+ | |_) | | | | | | (----`---| |----`| (----`| |_) || |
+ | / | | | | \ \ | | \ \ | _ < | |
+ | |\ \----.| `--' |.----) | | | .----) | | |_) || |
+ | _| `._____| \______/ |_______/ |__| |_______/ |______/ |__|
+
+ [rustsbi] Implementation: RustSBI-QEMU Version 0.0.1
+ [rustsbi-dtb] Hart count: cluster0 with 1 cores
+ [rustsbi] misa: RV64ACDFIMSU
+ [rustsbi] mideleg: ssoft, stimer, sext (0x222)
+ [rustsbi] medeleg: ima, ia, bkpt, la, sa, uecall, ipage, lpage, spage (0xb1ab)
+ [rustsbi] pmp0: 0x80000000 ..= 0x800fffff (rwx)
+ [rustsbi] pmp1: 0x80000000 ..= 0x807fffff (rwx)
+ [rustsbi] pmp2: 0x0 ..= 0xffffffffffffff (---)
+ [rustsbi] enter supervisor 0x80200000
+ [kernel] Hello, world!
+ /**** APPS ****
+ ch2b_bad_address
+ ch2b_bad_instructions
+ ch2b_bad_register
+ ch2b_hello_world
+ ch2b_power_3
+ ch2b_power_5
+ ch2b_power_7
+ ch3b_sleep
+ ch3b_sleep1
+ ch3b_yield0
+ ch3b_yield1
+ ch3b_yield2
+ ch5b_exit
+ ch5b_forktest
+ ch5b_forktest2
+ ch5b_forktest_simple
+ ch5b_forktree
+ ch5b_initproc
+ ch5b_user_shell
+ **************/
+ Rust user shell
+ >>
+
+可以通过输入ch5b开头的应用来测试ch5实现的fork等功能:
+
+.. code-block::
+
+ >> ch5b_forktest_simple
+
+ sys_wait without child process test passed!
+ parent start, pid = 2!
+ ready waiting on parent process!
+ hello child process!
+ child process pid = 3, exit code = 100
+ Shell: Process 2 exited with code 0
+
+本章代码树
+--------------------------------------
+
+.. code-block::
+ :linenos:
+
+ ├── os
+ ├── build.rs(修改:基于应用名的应用构建器)
+ ├── ...
+ └── src
+ ├── ...
+ ├── loader.rs(修改:基于应用名的应用加载器)
+ ├── main.rs(修改)
+ ├── mm(修改:为了支持本章的系统调用对此模块做若干增强)
+ │ ├── address.rs
+ │ ├── frame_allocator.rs
+ │ ├── heap_allocator.rs
+ │ ├── memory_set.rs
+ │ ├── mod.rs
+ │ └── page_table.rs
+ ├── syscall
+ │ ├── fs.rs(修改:新增 sys_read)
+ │ ├── mod.rs(修改:新的系统调用的分发处理)
+ │ └── process.rs(修改:新增 sys_getpid/fork/exec/waitpid)
+ ├── task
+ │ ├── context.rs
+ │ ├── manager.rs(新增:任务管理器,为上一章任务管理器功能的一部分)
+ │ ├── mod.rs(修改:调整原来的接口实现以支持进程)
+ │ ├── pid.rs(新增:进程标识符和内核栈的 Rust 抽象)
+ │ ├── processor.rs(新增:处理器管理结构 ``Processor`` ,为上一章任务管理器功能的一部分)
+ │ ├── switch.rs
+ │ ├── switch.S
+ │ └── task.rs(修改:支持进程机制的任务控制块)
+ └── trap
+ ├── context.rs
+ ├── mod.rs(修改:对于系统调用的实现进行修改以支持进程系统调用)
+ └── trap.S
+
+ cloc os
+ -------------------------------------------------------------------------------
+ Language files blank comment code
+ -------------------------------------------------------------------------------
+ Rust 29 180 138 2049
+ Assembly 4 20 26 229
+ make 1 11 4 36
+ TOML 1 2 1 13
+ -------------------------------------------------------------------------------
+ SUM: 35 213 169 2327
+ -------------------------------------------------------------------------------
+
+
+.. 本章代码导读
+.. -----------------------------------------------------
+
+.. 本章的第一小节 :doc:`/chapter5/1process` 介绍了操作系统中经典的进程概念,并描述我们将要实现的参考自 Unix 系内核并经过简化的精简版进程模型。在该模型下,若想对进程进行管理,实现创建、退出等操作,核心就在于 ``fork/exec/waitpid`` 三个系统调用。
+
+.. 首先我们修改运行在应用态的应用软件,它们均放置在 ``user`` 目录下。在新增系统调用的时候,需要在 ``user/src/lib.rs`` 中新增一个 ``sys_*`` 的函数,它的作用是将对应的系统调用按照与内核约定的 ABI 在 ``syscall`` 中转化为一条用于触发系统调用的 ``ecall`` 的指令;还需要在用户库 ``user_lib`` 将 ``sys_*`` 进一步封装成一个应用可以直接调用的与系统调用同名的函数。通过这种方式我们新增三个进程模型中核心的系统调用 ``fork/exec/waitpid`` ,一个查看进程 PID 的系统调用 ``getpid`` ,还有一个允许应用程序获取用户键盘输入的 ``read`` 系统调用。
+
+.. 基于进程模型,我们在 ``user/src/bin`` 目录下重新实现了一组应用程序。其中有两个特殊的应用程序:用户初始程序 ``initproc.rs`` 和 shell 程序 ``user_shell.rs`` ,可以认为它们位于内核和其他应用程序之间的中间层提供一些基础功能,但是它们仍处于应用层。前者会被内核唯一自动加载、也是最早加载并执行,后者则负责从键盘接收用户输入的应用名并执行对应的应用。剩下的应用从不同层面测试了我们内核实现的正确性,读者可以自行参考。值得一提的是, ``usertests`` 可以按照顺序执行绝大部分应用,会在测试的时候为我们提供很多方便。
+
+.. 接下来就需要在内核中实现简化版的进程机制并支持新增的系统调用。在本章第二小节 :doc:`/chapter5/2core-data-structures` 中我们对一些进程机制相关的数据结构进行了重构或者修改:
+
+.. - 为了支持基于应用名而不是应用 ID 来查找应用 ELF 可执行文件,从而实现灵活的应用加载,在 ``os/build.rs`` 以及 ``os/src/loader.rs`` 中更新了 ``link_app.S`` 的格式使得它包含每个应用的名字,另外提供 ``get_app_data_by_name`` 接口获取应用的 ELF 数据。
+.. - 在本章之前,任务管理器 ``TaskManager`` 不仅负责管理所有的任务状态,还维护着我们的 CPU 当前正在执行哪个任务。这种设计耦合度较高,我们将后一个功能分离到 ``os/src/task/processor.rs`` 中的处理器管理结构 ``Processor`` 中,它负责管理 CPU 上执行的任务和一些其他信息;而 ``os/src/task/manager.rs`` 中的任务管理器 ``TaskManager`` 仅负责管理所有任务。
+.. - 针对新的进程模型,我们复用前面章节的任务控制块 ``TaskControlBlock`` 作为进程控制块来保存进程的一些信息,相比前面章节还要新增 PID、内核栈、应用数据大小、父子进程、退出码等信息。它声明在 ``os/src/task/task.rs`` 中。
+.. - 从本章开始,内核栈在内核地址空间中的位置由所在进程的 PID 决定,我们需要在二者之间建立联系并提供一些相应的资源自动回收机制。可以参考 ``os/src/task/pid.rs`` 。
+
+.. 有了这些数据结构的支撑,我们在本章第三小节 :doc:`/chapter5/3implement-process-mechanism` 实现进程机制。它可以分成如下几个方面:
+
+.. - 初始进程的自动创建。在内核初始化的时候需要调用 ``os/src/task/mod.rs`` 中的 ``add_initproc`` 函数,它会调用 ``TaskControlBlock::new`` 读取并解析初始应用 ``initproc`` 的 ELF 文件数据并创建初始进程 ``INITPROC`` ,随后会将它加入到全局任务管理器 ``TASK_MANAGER`` 中参与调度。
+.. - 进程切换机制。当一个进程退出或者是主动/被动交出 CPU 使用权之后需要由内核将 CPU 使用权交给其他进程。在本章中我们沿用 ``os/src/task/mod.rs`` 中的 ``suspend_current_and_run_next`` 和 ``exit_current_and_run_next`` 两个接口来实现进程切换功能,但是需要适当调整它们的实现。我们需要调用 ``os/src/task/task.rs`` 中的 ``schedule`` 函数进行进程切换,它会首先切换到处理器的 idle 控制流(即 ``os/src/task/processor`` 的 ``Processor::run`` 方法),然后在里面选取要切换到的进程并切换过去。
+.. - 进程调度机制。在进程切换的时候我们需要选取一个进程切换过去。选取进程逻辑可以参考 ``os/src/task/manager.rs`` 中的 ``TaskManager::fetch_task`` 方法。
+.. - 进程生成机制。这主要是指 ``fork/exec`` 两个系统调用。它们的实现分别可以在 ``os/src/syscall/process.rs`` 中找到,分别基于 ``os/src/process/task.rs`` 中的 ``TaskControlBlock::fork/exec`` 。
+.. - 进程资源回收机制。当一个进程主动退出或出错退出的时候,在 ``exit_current_and_run_next`` 中会立即回收一部分资源并在进程控制块中保存退出码;而需要等到它的父进程通过 ``waitpid`` 系统调用(与 ``fork/exec`` 两个系统调用放在相同位置)捕获到它的退出码之后,它的进程控制块才会被回收,从而所有资源都被回收。
+.. - 为了支持用户终端 ``user_shell`` 读取用户键盘输入的功能,还需要实现 ``read`` 系统调用,它可以在 ``os/src/syscall/fs.rs`` 中找到。
\ No newline at end of file
diff --git a/guide/source/chapter5/1process.rst b/guide/source/chapter5/1process.rst
new file mode 100644
index 0000000..2024aa3
--- /dev/null
+++ b/guide/source/chapter5/1process.rst
@@ -0,0 +1,230 @@
+与进程有关的重要系统调用
+================================================
+
+重要系统调用
+------------------------------------------------------------
+
+fork 系统调用
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: rust
+
+ /// 功能:由当前进程 fork 出一个子进程。
+ /// 返回值:对于子进程返回 0,对于当前进程则返回子进程的 PID 。
+ /// syscall ID:220
+ pub fn sys_fork() -> isize;
+
+exec 系统调用
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: rust
+
+ /// 功能:将当前进程的地址空间清空并加载一个特定的可执行文件,返回用户态后开始它的执行。
+ /// 参数:字符串 path 给出了要加载的可执行文件的名字;
+ /// 返回值:如果出错的话(如找不到名字相符的可执行文件)则返回 -1,否则不应该返回。
+ /// 注意:path 必须以 "\0" 结尾,否则内核将无法确定其长度
+ /// syscall ID:221
+ pub fn sys_exec(path: &str) -> isize;
+
+利用 ``fork`` 和 ``exec`` 的组合,我们能让创建一个子进程,并令其执行特定的可执行文件。
+
+waitpid 系统调用
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: rust
+
+ /// 功能:当前进程等待一个子进程变为僵尸进程,回收其全部资源并收集其返回值。
+ /// 参数:pid 表示要等待的子进程的进程 ID,如果为 -1 的话表示等待任意一个子进程;
+ /// exit_code 表示保存子进程返回值的地址,如果这个地址为 0 的话表示不必保存。
+ /// 返回值:如果要等待的子进程不存在则返回 -1;否则如果要等待的子进程均未结束则返回 -2;
+ /// 否则返回结束的子进程的进程 ID。
+ /// syscall ID:260
+ pub fn sys_waitpid(pid: isize, exit_code: *mut i32) -> isize;
+
+
+``sys_waitpid`` 在用户库中被封装成两个不同的 API, ``wait(exit_code: &mut i32)`` 和 ``waitpid(pid: usize, exit_code: &mut i32)``,
+前者用于等待任意一个子进程,后者用于等待特定子进程。它们实现的策略是如果子进程还未结束,就以 yield 让出时间片:
+
+.. code-block:: rust
+ :linenos:
+
+ // user/src/lib.rs
+
+ pub fn wait(exit_code: &mut i32) -> isize {
+ loop {
+ match sys_waitpid(-1, exit_code as *mut _) {
+ -2 => { sys_yield(); }
+ n => { return n; }
+ }
+ }
+ }
+
+
+应用程序示例
+-----------------------------------------------
+
+借助这三个重要系统调用,我们可以开发功能更强大的应用。下面是两个案例: **用户初始程序-init** 和 **shell程序-user_shell** 。
+
+用户初始程序-initproc
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+在内核初始化完毕后创建的第一个进程,是 **用户初始进程** (Initial Process) ,它将通过
+``fork+exec`` 创建 ``user_shell`` 子进程,并将被用于回收僵尸进程。
+
+.. code-block:: rust
+ :linenos:
+
+ // user/src/bin/ch5b_initproc.rs
+
+ #![no_std]
+ #![no_main]
+
+ #[macro_use]
+ extern crate user_lib;
+
+ use user_lib::{
+ fork,
+ wait,
+ exec,
+ yield_,
+ };
+
+ #[no_mangle]
+ fn main() -> i32 {
+ if fork() == 0 {
+ exec("ch5b_user_shell\0");
+ } else {
+ loop {
+ let mut exit_code: i32 = 0;
+ let pid = wait(&mut exit_code);
+ if pid == -1 {
+ yield_();
+ continue;
+ }
+ println!(
+ "[initproc] Released a zombie process, pid={}, exit_code={}",
+ pid,
+ exit_code,
+ );
+ }
+ }
+ 0
+ }
+
+- 第 19 行为 ``fork`` 出的子进程分支,通过 ``exec`` 启动shell程序 ``user_shell`` ,
+ 注意我们需要在字符串末尾手动加入 ``\0`` 。
+- 第 21 行开始则为父进程分支,表示用户初始程序-initproc自身。它不断循环调用 ``wait`` 来等待并回收系统中的僵尸进程占据的资源。
+ 如果回收成功的话则会打印一条报告信息给出被回收子进程的 PID 和返回值;否则就 ``yield_`` 交出 CPU 资源并在下次轮到它执行的时候再回收看看。
+
+
+shell程序-user_shell
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+user_shell 需要捕获用户输入并进行解析处理,为此添加一个能获取用户输入的系统调用:
+
+.. code-block:: rust
+
+ /// 功能:从文件中读取一段内容到缓冲区。
+ /// 参数:fd 是待读取文件的文件描述符,切片 buffer 则给出缓冲区。
+ /// 返回值:如果出现了错误则返回 -1,否则返回实际读到的字节数。
+ /// syscall ID:63
+ pub fn sys_read(fd: usize, buffer: &mut [u8]) -> isize;
+
+实际调用时,我们必须要同时向内核提供缓冲区的起始地址及长度:
+
+.. code-block:: rust
+
+ // user/src/syscall.rs
+
+ pub fn sys_read(fd: usize, buffer: &mut [u8]) -> isize {
+ syscall(SYSCALL_READ, [fd, buffer.as_mut_ptr() as usize, buffer.len()])
+ }
+
+我们在用户库中将其进一步封装成每次能够从 **标准输入** 中获取一个字符的 ``getchar`` 函数。
+
+shell程序 ``user_shell`` 实现如下:
+
+.. code-block:: rust
+ :linenos:
+ :emphasize-lines: 28,53,61
+
+ // user/src/bin/ch5b_user_shell.rs
+
+ #![no_std]
+ #![no_main]
+
+ extern crate alloc;
+
+ #[macro_use]
+ extern crate user_lib;
+
+ const LF: u8 = 0x0au8;
+ const CR: u8 = 0x0du8;
+ const DL: u8 = 0x7fu8;
+ const BS: u8 = 0x08u8;
+
+ use alloc::string::String;
+ use user_lib::{fork, exec, waitpid, yield_};
+ use user_lib::console::getchar;
+
+ #[no_mangle]
+ pub fn main() -> i32 {
+ println!("Rust user shell");
+ let mut line: String = String::new();
+ print!(">> ");
+ loop {
+ let c = getchar();
+ match c {
+ LF | CR => {
+ println!("");
+ if !line.is_empty() {
+ line.push('\0');
+ let pid = fork();
+ if pid == 0 {
+ // child process
+ if exec(line.as_str()) == -1 {
+ println!("Error when executing!");
+ return -4;
+ }
+ unreachable!();
+ } else {
+ let mut exit_code: i32 = 0;
+ let exit_pid = waitpid(pid as usize, &mut exit_code);
+ assert_eq!(pid, exit_pid);
+ println!(
+ "Shell: Process {} exited with code {}",
+ pid, exit_code
+ );
+ }
+ line.clear();
+ }
+ print!(">> ");
+ }
+ BS | DL => {
+ if !line.is_empty() {
+ print!("{}", BS as char);
+ print!(" ");
+ print!("{}", BS as char);
+ line.pop();
+ }
+ }
+ _ => {
+ print!("{}", c as char);
+ line.push(c as char);
+ }
+ }
+ }
+ }
+
+可以看到,在以第 25 行开头的主循环中,每次都是调用 ``getchar`` 获取一个用户输入的字符,
+并根据它相应进行一些动作。第 23 行声明的字符串 ``line`` 则维护着用户当前输入的命令内容,它也在不断发生变化。
+
+- 如果用户输入回车键(第 28 行),那么user_shell 会 fork 出一个子进程(第 34 行开始)并试图通过
+ ``exec`` 系统调用执行一个应用,应用的名字在字符串 ``line`` 中给出。如果 exec 的返回值为 -1 ,
+ 说明在应用管理器中找不到对应名字的应用,此时子进程就直接打印错误信息并退出;否则子进程将开始执行目标应用。
+
+ fork 之后的 user_shell 进程自己的逻辑可以在第 41 行找到。它在等待 fork 出来的子进程结束并回收掉它的资源,还会顺带收集子进程的退出状态并打印出来。
+- 如果用户输入退格键(第 53 行),首先我们需要将屏幕上当前行的最后一个字符用空格替换掉,
+ 这可以通过输入一个特殊的退格字节 ``BS`` 来实现。其次,user_shell 进程内维护的 ``line`` 也需要弹出最后一个字符。
+- 如果用户输入了一个其他字符(第 61 行),就接将它打印在屏幕上,并加入到 ``line`` 中。
+- 按键 ``Ctrl+A`` 再输入 ``X`` 来退出qemu模拟器。
\ No newline at end of file
diff --git a/guide/source/chapter5/2core-data-structures.rst b/guide/source/chapter5/2core-data-structures.rst
new file mode 100644
index 0000000..b43388a
--- /dev/null
+++ b/guide/source/chapter5/2core-data-structures.rst
@@ -0,0 +1,540 @@
+进程管理的核心数据结构
+===================================
+
+本节导读
+-----------------------------------
+
+为了更好实现进程管理,我们需要设计和调整内核中的一些数据结构,包括:
+
+- 基于应用名的应用链接/加载器
+- 进程标识符 ``PidHandle`` 以及内核栈 ``KernelStack``
+- 任务控制块 ``TaskControlBlock``
+- 任务管理器 ``TaskManager``
+- 处理器管理结构 ``Processor``
+
+基于应用名的应用链接/加载器
+------------------------------------------------------------------------
+
+在实现 ``exec`` 系统调用的时候,我们需要根据应用的名字而不仅仅是一个编号来获取应用的 ELF 格式数据。
+因此,在链接器 ``os/build.rs`` 中,我们按顺序保存链接进来的每个应用的名字:
+
+.. code-block::
+ :linenos:
+ :emphasize-lines: 8-13
+
+ // os/build.rs
+
+ for i in 0..apps.len() {
+ writeln!(f, r#" .quad app_{}_start"#, i)?;
+ }
+ writeln!(f, r#" .quad app_{}_end"#, apps.len() - 1)?;
+
+ writeln!(f, r#"
+ .global _app_names
+ _app_names:"#)?;
+ for app in apps.iter() {
+ writeln!(f, r#" .string "{}""#, app)?;
+ }
+
+ for (idx, app) in apps.iter().enumerate() {
+ ...
+ }
+
+第 8~13 行,各个应用的名字通过 ``.string`` 伪指令放到数据段中,注意链接器会自动在每个字符串的结尾加入分隔符
+``\0`` ,它们的位置由全局符号 ``_app_names`` 指出。
+
+而在加载器 ``loader.rs`` 中,我们用一个全局可见的 *只读* 向量 ``APP_NAMES`` 来按照顺序将所有应用的名字保存在内存中:
+
+.. code-block:: Rust
+
+ // os/src/loader.rs
+
+ lazy_static! {
+ static ref APP_NAMES: Vec<&'static str> = {
+ let num_app = get_num_app();
+ extern "C" { fn _app_names(); }
+ let mut start = _app_names as usize as *const u8;
+ let mut v = Vec::new();
+ unsafe {
+ for _ in 0..num_app {
+ let mut end = start;
+ while end.read_volatile() != '\0' as u8 {
+ end = end.add(1);
+ }
+ let slice = core::slice::from_raw_parts(start, end as usize - start as usize);
+ let str = core::str::from_utf8(slice).unwrap();
+ v.push(str);
+ start = end.add(1);
+ }
+ }
+ v
+ };
+ }
+
+使用 ``get_app_data_by_name`` 可以按照应用的名字来查找获得应用的 ELF 数据,而 ``list_apps``
+在内核初始化时被调用,它可以打印出所有可用应用的名字。
+
+.. code-block:: rust
+
+ // os/src/loader.rs
+
+ pub fn get_app_data_by_name(name: &str) -> Option<&'static [u8]> {
+ let num_app = get_num_app();
+ (0..num_app)
+ .find(|&i| APP_NAMES[i] == name)
+ .map(|i| get_app_data(i))
+ }
+
+ pub fn list_apps() {
+ println!("/**** APPS ****");
+ for app in APP_NAMES.iter() {
+ println!("{}", app);
+ }
+ println!("**************/")
+ }
+
+
+进程标识符和内核栈
+------------------------------------------------------------------------
+
+进程标识符
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+同一时间存在的所有进程都有一个自己的进程标识符,它们是互不相同的整数。这里将其抽象为一个 ``PidHandle``
+类型,当它的生命周期结束后,对应的整数会被编译器自动回收:
+
+.. code-block:: rust
+
+ // os/src/task/pid.rs
+
+ pub struct PidHandle(pub usize);
+
+类似之前的物理页帧分配器 ``FrameAllocator`` ,我们实现一个同样使用简单栈式分配策略的进程标识符分配器
+``PidAllocator`` ,并将其全局实例化为 ``PID_ALLOCATOR`` :
+
+.. code-block:: rust
+
+ // os/src/task/pid.rs
+
+ struct PidAllocator {
+ current: usize,
+ recycled: Vec,
+ }
+
+ impl PidAllocator {
+ pub fn new() -> Self {
+ PidAllocator {
+ current: 0,
+ recycled: Vec::new(),
+ }
+ }
+ pub fn alloc(&mut self) -> PidHandle {
+ if let Some(pid) = self.recycled.pop() {
+ PidHandle(pid)
+ } else {
+ self.current += 1;
+ PidHandle(self.current - 1)
+ }
+ }
+ pub fn dealloc(&mut self, pid: usize) {
+ assert!(pid < self.current);
+ assert!(
+ self.recycled.iter().find(|ppid| **ppid == pid).is_none(),
+ "pid {} has been deallocated!", pid
+ );
+ self.recycled.push(pid);
+ }
+ }
+
+ lazy_static! {
+ static ref PID_ALLOCATOR: UPSafeCell =
+ unsafe { UPSafeCell::new(PidAllocator::new()) };
+ }
+
+``PidAllocator::alloc`` 将会分配出去一个将 ``usize`` 包装之后的 ``PidHandle`` 。
+我们将其包装为一个全局分配进程标识符的接口 ``pid_alloc``:
+
+.. code-block:: rust
+
+ // os/src/task/pid.rs
+
+ pub fn pid_alloc() -> PidHandle {
+ PID_ALLOCATOR.exclusive_access().alloc()
+ }
+
+同时我们也需要为 ``PidHandle`` 实现 ``Drop`` Trait 来允许编译器进行自动的资源回收:
+
+.. code-block:: rust
+
+ // os/src/task/pid.rs
+
+ impl Drop for PidHandle {
+ fn drop(&mut self) {
+ //println!("drop pid {}", self.0);
+ PID_ALLOCATOR.exclusive_access().dealloc(self.0);
+ }
+ }
+
+内核栈
+~~~~~~~~~~~~~~~~~~~~~~
+
+从本章开始,我们将应用编号替换为进程标识符来决定每个进程内核栈在地址空间中的位置。
+
+在内核栈 ``KernelStack`` 中保存着它所属进程的 PID :
+
+.. code-block:: rust
+
+ // os/src/task/pid.rs
+
+ pub struct KernelStack {
+ pid: usize,
+ }
+
+它提供以下方法:
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/task/pid.rs
+
+ /// Return (bottom, top) of a kernel stack in kernel space.
+ pub fn kernel_stack_position(app_id: usize) -> (usize, usize) {
+ let top = TRAMPOLINE - app_id * (KERNEL_STACK_SIZE + PAGE_SIZE);
+ let bottom = top - KERNEL_STACK_SIZE;
+ (bottom, top)
+ }
+
+ impl KernelStack {
+ pub fn new(pid_handle: &PidHandle) -> Self {
+ let pid = pid_handle.0;
+ let (kernel_stack_bottom, kernel_stack_top) = kernel_stack_position(pid);
+ KERNEL_SPACE.exclusive_access().insert_framed_area(
+ kernel_stack_bottom.into(),
+ kernel_stack_top.into(),
+ MapPermission::R | MapPermission::W,
+ );
+ KernelStack {
+ pid: pid_handle.0,
+ }
+ }
+ pub fn push_on_top(&self, value: T) -> *mut T where
+ T: Sized, {
+ let kernel_stack_top = self.get_top();
+ let ptr_mut = (kernel_stack_top - core::mem::size_of::()) as *mut T;
+ unsafe { *ptr_mut = value; }
+ ptr_mut
+ }
+ pub fn get_top(&self) -> usize {
+ let (_, kernel_stack_top) = kernel_stack_position(self.pid);
+ kernel_stack_top
+ }
+ }
+
+- 第 11 行, ``new`` 方法可以从一个 ``PidHandle`` ,也就是一个已分配的进程标识符中对应生成一个内核栈 ``KernelStack`` 。
+ 它调用了第 4 行声明的 ``kernel_stack_position`` 函数来根据进程标识符计算内核栈在内核地址空间中的位置,
+ 随即在第 14 行将一个逻辑段插入内核地址空间 ``KERNEL_SPACE`` 中。
+- 第 25 行的 ``push_on_top`` 方法可以将一个类型为 ``T`` 的变量压入内核栈顶并返回其裸指针,
+ 这也是一个泛型函数。它在实现的时候用到了第 32 行的 ``get_top`` 方法来获取当前内核栈顶在内核地址空间中的地址。
+
+内核栈 ``KernelStack`` 用到了 RAII 的思想,具体来说,实际保存它的物理页帧的生命周期被绑定到它下面,当
+``KernelStack`` 生命周期结束后,这些物理页帧也将会被编译器自动回收:
+
+.. code-block:: rust
+
+ // os/src/task/pid.rs
+
+ impl Drop for KernelStack {
+ fn drop(&mut self) {
+ let (kernel_stack_bottom, _) = kernel_stack_position(self.pid);
+ let kernel_stack_bottom_va: VirtAddr = kernel_stack_bottom.into();
+ KERNEL_SPACE
+ .exclusive_access()
+ .remove_area_with_start_vpn(kernel_stack_bottom_va.into());
+ }
+ }
+
+
+为 ``KernelStack`` 实现 ``Drop`` Trait,一旦它的生命周期结束,就将内核地址空间中对应的逻辑段删除,为此在 ``MemorySet``
+中新增了一个名为 ``remove_area_with_start_vpn`` 的方法,感兴趣的读者可以查阅。
+
+进程控制块
+------------------------------------------------------------------------
+
+在内核中,每个进程的执行状态、资源控制等元数据均保存在一个被称为 **进程控制块** (PCB, Process Control Block)
+的结构中,它是内核对进程进行管理的单位。在内核看来,它就等价于一个进程。
+
+承接前面的章节,我们仅需对任务控制块 ``TaskControlBlock`` 进行若干改动,让它直接承担进程控制块的功能:
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/task/task.rs
+
+ pub struct TaskControlBlock {
+ // immutable
+ pub pid: PidHandle,
+ pub kernel_stack: KernelStack,
+ // mutable
+ inner: UPSafeCell,
+ }
+
+ pub struct TaskControlBlockInner {
+ pub trap_cx_ppn: PhysPageNum,
+ pub base_size: usize,
+ pub task_cx: TaskContext,
+ pub task_status: TaskStatus,
+ pub memory_set: MemorySet,
+ pub parent: Option>,
+ pub children: Vec>,
+ pub exit_code: i32,
+ }
+
+
+任务控制块中包含两部分:
+
+- 在初始化之后就不再变化的作为一个字段直接放在任务控制块中。这里将进程标识符 ``PidHandle`` 和内核栈 ``KernelStack`` 放在其中;
+- 在运行过程中可能发生变化的则放在 ``TaskControlBlockInner`` 中,将它再包裹上一层 ``UPSafeCell`` 放在任务控制块中。
+ 在此使用 ``UPSafeCell`` 可以提供互斥从而避免数据竞争。
+
+``TaskControlBlockInner`` 中包含下面这些内容:
+
+- ``trap_cx_ppn`` 指出了应用地址空间中的 Trap 上下文被放在的物理页帧的物理页号。
+- ``base_size`` 的含义是:应用数据仅有可能出现在应用地址空间低于 ``base_size`` 字节的区域中。借助它我们可以清楚的知道应用有多少数据驻留在内存中。
+- ``task_cx`` 保存任务上下文,用于任务切换。
+- ``task_status`` 维护当前进程的执行状态。
+- ``memory_set`` 表示应用地址空间。
+- ``parent`` 指向当前进程的父进程(如果存在的话)。注意我们使用 ``Weak`` 而非 ``Arc``
+ 来包裹另一个任务控制块,因此这个智能指针将不会影响父进程的引用计数。
+- ``children`` 则将当前进程的所有子进程的任务控制块以 ``Arc`` 智能指针的形式保存在一个向量中,这样才能够更方便的找到它们。
+- 当进程调用 exit 系统调用主动退出或者执行出错由内核终止的时候,它的退出码 ``exit_code``
+ 会被内核保存在它的任务控制块中,并等待它的父进程通过 waitpid 回收它的资源的同时也收集它的 PID 以及退出码。
+
+注意我们在维护父子进程关系的时候大量用到了智能指针 ``Arc/Weak`` ,当且仅当它的引用计数变为 0 的时候,进程控制块以及被绑定到它上面的各类资源才会被回收。
+
+``TaskControlBlockInner`` 提供的方法主要是对于它内部字段的快捷访问:
+
+.. code-block:: rust
+
+ // os/src/task/task.rs
+
+ impl TaskControlBlockInner {
+ pub fn get_trap_cx(&self) -> &'static mut TrapContext {
+ self.trap_cx_ppn.get_mut()
+ }
+ pub fn get_user_token(&self) -> usize {
+ self.memory_set.token()
+ }
+ fn get_status(&self) -> TaskStatus {
+ self.task_status
+ }
+ pub fn is_zombie(&self) -> bool {
+ self.get_status() == TaskStatus::Zombie
+ }
+ }
+
+而任务控制块 ``TaskControlBlock`` 目前提供以下方法:
+
+.. code-block:: rust
+
+ // os/src/task/task.rs
+
+ impl TaskControlBlock {
+ pub fn inner_exclusive_access(&self) -> RefMut<'_, TaskControlBlockInner> {
+ self.inner.exclusive_access()
+ }
+ pub fn getpid(&self) -> usize {
+ self.pid.0
+ }
+ pub fn new(elf_data: &[u8]) -> Self {...}
+ pub fn exec(&self, elf_data: &[u8]) {...}
+ pub fn fork(self: &Arc) -> Arc {...}
+ }
+
+- ``inner_exclusive_access`` 尝试获取互斥锁来得到 ``TaskControlBlockInner`` 的可变引用。
+- ``getpid`` 以 ``usize`` 的形式返回当前进程的进程标识符。
+- ``new`` 用来创建一个新的进程,目前仅用于内核中手动创建唯一一个初始进程 ``initproc`` 。
+- ``exec`` 用来实现 ``exec`` 系统调用,即当前进程加载并执行另一个 ELF 格式可执行文件。
+- ``fork`` 用来实现 ``fork`` 系统调用,即当前进程 fork 出来一个与之几乎相同的子进程。
+
+``new/exec/fork`` 的实现我们将在下一小节再介绍。
+
+任务管理器
+------------------------------------------------------------------------
+
+在前面的章节中,任务管理器 ``TaskManager`` 不仅负责管理所有的任务,还维护着 CPU 当前在执行哪个任务。
+由于这种设计不够灵活,我们需要将任务管理器对于 CPU 的监控职能拆分到处理器管理结构 ``Processor`` 中去,
+任务管理器自身仅负责管理所有任务。在这里,任务指的就是进程。
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/task/manager.rs
+
+ pub struct TaskManager {
+ ready_queue: VecDeque>,
+ }
+
+ /// A simple FIFO scheduler.
+ impl TaskManager {
+ pub fn new() -> Self {
+ Self {
+ ready_queue: VecDeque::new(),
+ }
+ }
+ pub fn add(&mut self, task: Arc) {
+ self.ready_queue.push_back(task);
+ }
+ pub fn fetch(&mut self) -> Option> {
+ self.ready_queue.pop_front()
+ }
+ }
+
+ lazy_static! {
+ pub static ref TASK_MANAGER: UPSafeCell =
+ unsafe { UPSafeCell::new(TaskManager::new()) };
+ }
+
+ pub fn add_task(task: Arc) {
+ TASK_MANAGER.exclusive_access().add(task);
+ }
+
+ pub fn fetch_task() -> Option> {
+ TASK_MANAGER.exclusive_access().fetch()
+ }
+
+``TaskManager`` 将所有的任务控制块用引用计数 ``Arc`` 智能指针包裹后放在一个双端队列 ``VecDeque`` 中。
+使用智能指针的原因在于,任务控制块经常需要被放入/取出,如果直接移动任务控制块自身将会带来大量的数据拷贝开销,
+而对于智能指针进行移动则没有多少开销。其次,允许任务控制块的共享引用在某些情况下能够让我们的实现更加方便。
+
+``TaskManager`` 提供 ``add/fetch`` 两个操作,前者表示将一个任务加入队尾,后者则表示从队头中取出一个任务来执行。
+从调度算法来看,这里用到的就是最简单的 RR 算法。全局实例 ``TASK_MANAGER`` 则提供给内核的其他子模块 ``add_task/fetch_task`` 两个函数。
+
+处理器管理结构
+------------------------------------------------------------------------
+
+处理器管理结构 ``Processor`` 负责维护从任务管理器 ``TaskManager`` 分离出去的那部分 CPU 状态:
+
+.. code-block:: rust
+
+ // os/src/task/processor.rs
+
+ pub struct Processor {
+ current: Option>,
+ idle_task_cx: TaskContext,
+ }
+
+包括:
+
+- ``current`` 表示在当前处理器上正在执行的任务;
+- ``idle_task_cx_ptr`` 表示当前处理器上的 idle 控制流的任务上下文的地址。
+
+在单核环境下,我们仅创建单个 ``Processor`` 的全局实例 ``PROCESSOR`` :
+
+.. code-block:: rust
+
+ // os/src/task/processor.rs
+
+ lazy_static! {
+ pub static ref PROCESSOR: UPSafeCell = unsafe { UPSafeCell::new(Processor::new()) };
+ }
+
+正在执行的任务
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/task/processor.rs
+
+ impl Processor {
+ pub fn take_current(&mut self) -> Option> {
+ self.current.take()
+ }
+ pub fn current(&self) -> Option> {
+ self.current.as_ref().map(|task| Arc::clone(task))
+ }
+ }
+
+ pub fn take_current_task() -> Option> {
+ PROCESSOR.take_current()
+ }
+
+ pub fn current_task() -> Option> {
+ PROCESSOR.current()
+ }
+
+ pub fn current_user_token() -> usize {
+ let task = current_task().unwrap();
+ let token = task.inner_exclusive_access().get_user_token();
+ token
+ }
+
+ pub fn current_trap_cx() -> &'static mut TrapContext {
+ current_task()
+ .unwrap()
+ .inner_exclusive_access()
+ .get_trap_cx()
+ }
+
+
+- 第 4 行的 ``Processor::take_current`` 可以取出当前正在执行的任务。 ``Option::take`` 意味着 ``current`` 字段也变为 ``None`` 。
+- 第 7 行的 ``Processor::current`` 返回当前执行的任务的一份拷贝。。
+- ``current_user_token`` 和 ``current_trap_cx`` 基于 ``current_task`` 实现,提供当前正在执行的任务的更多信息。
+
+
+任务调度的 idle 控制流
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+每个 ``Processor`` 都有一个 idle 控制流,它们运行在每个核各自的启动栈上,功能是尝试从任务管理器中选出一个任务来在当前核上执行。
+在内核初始化完毕之后,核通过调用 ``run_tasks`` 函数来进入 idle 控制流:
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/task/processor.rs
+
+ impl Processor {
+ fn get_idle_task_cx_ptr(&mut self) -> *mut TaskContext {
+ &mut self.idle_task_cx as *mut _
+ }
+ }
+
+ pub fn run_tasks() {
+ loop {
+ let mut processor = PROCESSOR.exclusive_access();
+ if let Some(task) = fetch_task() {
+ let idle_task_cx_ptr = processor.get_idle_task_cx_ptr();
+ // access coming task TCB exclusively
+ let mut task_inner = task.inner_exclusive_access();
+ let next_task_cx_ptr = &task_inner.task_cx as *const TaskContext;
+ task_inner.task_status = TaskStatus::Running;
+ drop(task_inner);
+ // release coming task TCB manually
+ processor.current = Some(task);
+ // release processor manually
+ drop(processor);
+ unsafe {
+ __switch(idle_task_cx_ptr, next_task_cx_ptr);
+ }
+ }
+ }
+ }
+
+调度功能的主体在 ``run_tasks`` 中实现。它循环调用 ``fetch_task`` 直到顺利从任务管理器中取出一个任务,然后获得
+``__switch`` 两个参数进行任务切换。注意在整个过程中要严格控制临界区。
+
+当一个应用交出 CPU 使用权时,进入内核后它会调用 ``schedule`` 函数来切换到 idle 控制流并开启新一轮的任务调度。
+
+.. code-block:: rust
+
+ // os/src/task/processor.rs
+
+ pub fn schedule(switched_task_cx_ptr: *mut TaskContext) {
+ let mut processor = PROCESSOR.exclusive_access();
+ let idle_task_cx_ptr = processor.get_idle_task_cx_ptr();
+ drop(processor);
+ unsafe {
+ __switch(switched_task_cx_ptr, idle_task_cx_ptr);
+ }
+ }
+
+切换回去之后,我们将跳转到 ``Processor::run`` 中 ``__switch`` 返回之后的位置,也即开启了下一轮循环。
diff --git a/guide/source/chapter5/3implement-process-mechanism.rst b/guide/source/chapter5/3implement-process-mechanism.rst
new file mode 100644
index 0000000..1de6985
--- /dev/null
+++ b/guide/source/chapter5/3implement-process-mechanism.rst
@@ -0,0 +1,665 @@
+进程管理机制的设计实现
+============================================
+
+本节导读
+--------------------------------------------
+
+本节将介绍如何基于上一节设计的内核数据结构来实现进程管理:
+
+- 初始进程 ``initproc`` 的创建;
+- 进程调度机制:当进程主动调用 ``sys_yield`` 交出 CPU 使用权,或者内核本轮分配的时间片用尽之后如何切换到下一个进程;
+- 进程生成机制:介绍进程相关的两个重要系统调用 ``sys_fork/sys_exec`` 的实现;
+- 字符输入机制:介绍 ``sys_read`` 系统调用的实现;
+- 进程资源回收机制:当进程调用 ``sys_exit`` 正常退出或者出错被内核终止后,如何保存其退出码,其父进程又是如何通过
+ ``sys_waitpid`` 收集该进程的信息并回收其资源。
+
+初始进程的创建
+--------------------------------------------
+
+内核初始化完毕之后,即会调用 ``task`` 子模块提供的 ``add_initproc`` 函数来将初始进程 ``initproc``
+加入任务管理器,但在这之前,我们需要初始进程的进程控制块 ``INITPROC`` ,这基于 ``lazy_static`` 在运行时完成。
+
+.. code-block:: rust
+
+ // os/src/task/mod.rs
+
+ lazy_static! {
+ pub static ref INITPROC: Arc = Arc::new(TaskControlBlock::new(
+ get_app_data_by_name("initproc").unwrap()
+ ));
+ }
+
+ pub fn add_initproc() {
+ add_task(INITPROC.clone());
+ }
+
+我们调用 ``TaskControlBlock::new`` 来创建一个进程控制块,它需要传入 ELF 可执行文件的数据切片作为参数,
+这可以通过加载器 ``loader`` 子模块提供的 ``get_app_data_by_name`` 接口查找 ``initproc`` 的 ELF 数据来获得。在初始化
+``INITPROC`` 之后,则在 ``add_initproc`` 中可以调用 ``task`` 的任务管理器 ``manager`` 子模块提供的 ``add_task`` 接口将其加入到任务管理器。
+
+接下来介绍 ``TaskControlBlock::new`` 是如何实现的:
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/task/task.rs
+
+ use super::{PidHandle, pid_alloc, KernelStack};
+ use super::TaskContext;
+ use crate::config::TRAP_CONTEXT;
+ use crate::trap::TrapContext;
+
+ // impl TaskControlBlock
+ pub fn new(elf_data: &[u8]) -> Self {
+ // memory_set with elf program headers/trampoline/trap context/user stack
+ let (memory_set, user_sp, entry_point) = MemorySet::from_elf(elf_data);
+ let trap_cx_ppn = memory_set
+ .translate(VirtAddr::from(TRAP_CONTEXT).into())
+ .unwrap()
+ .ppn();
+ // alloc a pid and a kernel stack in kernel space
+ let pid_handle = pid_alloc();
+ let kernel_stack = KernelStack::new(&pid_handle);
+ let kernel_stack_top = kernel_stack.get_top();
+ // push a task context which goes to trap_return to the top of kernel stack
+ let task_cx_ptr = kernel_stack.push_on_top(TaskContext::goto_trap_return());
+ let task_control_block = Self {
+ pid: pid_handle,
+ kernel_stack,
+ inner: unsafe { UPSafeCell::new(TaskControlBlockInner {
+ trap_cx_ppn,
+ base_size: user_sp,
+ task_cx: TaskContext::goto_trap_return(kernel_stack_top),
+ task_status: TaskStatus::Ready,
+ memory_set,
+ parent: None,
+ children: Vec::new(),
+ exit_code: 0,
+ })
+ },
+ };
+ // prepare TrapContext in user space
+ let trap_cx = task_control_block.inner_exclusive_access().get_trap_cx();
+ *trap_cx = TrapContext::app_init_context(
+ entry_point,
+ user_sp,
+ KERNEL_SPACE.exclusive_access().token(),
+ kernel_stack_top,
+ trap_handler as usize,
+ );
+ task_control_block
+ }
+
+- 第 10 行,解析 ELF 得到应用地址空间 ``memory_set`` ,用户栈在应用地址空间中的位置 ``user_sp`` 以及应用的入口点 ``entry_point`` 。
+- 第 11 行,手动查页表找到应用地址空间中的 Trap 上下文实际所在的物理页帧。
+- 第 16~18 行,为新进程分配 PID 以及内核栈,并记录下内核栈在内核地址空间的位置 ``kernel_stack_top`` 。
+- 第 20 行,在该进程的内核栈上压入初始化的任务上下文,使得第一次任务切换到它的时候可以跳转到 ``trap_return`` 并进入用户态开始执行。
+- 第 21 行,整合之前的部分信息创建进程控制块 ``task_control_block`` 。
+- 第 39 行,初始化位于该进程应用地址空间中的 Trap 上下文,使得第一次进入用户态时,能正确跳转到应用入口点并设置好用户栈,
+ 同时也保证在 Trap 的时候用户态能正确进入内核态。
+
+进程调度机制
+--------------------------------------------
+
+调用 ``task`` 子模块提供的 ``suspend_current_and_run_next`` 函数可以暂停当前任务,并切换到下一个任务,下面给出了两种典型的使用场景:
+
+.. code-block:: rust
+ :emphasize-lines: 4,18
+
+ // os/src/syscall/process.rs
+
+ pub fn sys_yield() -> isize {
+ suspend_current_and_run_next();
+ 0
+ }
+
+ // os/src/trap/mod.rs
+
+ #[no_mangle]
+ pub fn trap_handler() -> ! {
+ set_kernel_trap_entry();
+ let scause = scause::read();
+ let stval = stval::read();
+ match scause.cause() {
+ Trap::Interrupt(Interrupt::SupervisorTimer) => {
+ set_next_trigger();
+ suspend_current_and_run_next();
+ }
+ ...
+ }
+ trap_return();
+ }
+
+随着进程概念的引入, ``suspend_current_and_run_next`` 的实现也需要发生变化:
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/task/mod.rs
+
+ use processor::{task_current_task, schedule};
+ use manager::add_task;
+
+ pub fn suspend_current_and_run_next() {
+ // There must be an application running.
+ let task = take_current_task().unwrap();
+
+ // ---- access current TCB exclusively
+ let mut task_inner = task.inner_exclusive_access();
+ let task_cx_ptr = &mut task_inner.task_cx as *mut TaskContext;
+ // Change status to Ready
+ task_inner.task_status = TaskStatus::Ready;
+ drop(task_inner);
+ // ---- release current PCB
+
+ // push back to ready queue.
+ add_task(task);
+ // jump to scheduling cycle
+ schedule(task_cx_ptr);
+ }
+
+首先通过 ``take_current_task`` 来取出当前正在执行的任务,修改其进程控制块内的状态,随后将这个任务放入任务管理器的队尾。接着调用
+``schedule`` 函数来触发调度并切换任务。当仅有一个任务的时候, ``suspend_current_and_run_next`` 的效果是会继续执行这个任务。
+
+进程的生成机制
+--------------------------------------------
+
+fork 系统调用的实现
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+实现 fork 时,最为关键且困难一点的是为子进程创建一个和父进程几乎完全相同的地址空间。我们的实现如下:
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/mm/memory_set.rs
+
+ impl MapArea {
+ pub fn from_another(another: &MapArea) -> Self {
+ Self {
+ vpn_range: VPNRange::new(
+ another.vpn_range.get_start(),
+ another.vpn_range.get_end()
+ ),
+ data_frames: BTreeMap::new(),
+ map_type: another.map_type,
+ map_perm: another.map_perm,
+ }
+ }
+ }
+
+ impl MemorySet {
+ pub fn from_existed_user(user_space: &MemorySet) -> MemorySet {
+ let mut memory_set = Self::new_bare();
+ // map trampoline
+ memory_set.map_trampoline();
+ // copy data sections/trap_context/user_stack
+ for area in user_space.areas.iter() {
+ let new_area = MapArea::from_another(area);
+ memory_set.push(new_area, None);
+ // copy data from another space
+ for vpn in area.vpn_range {
+ let src_ppn = user_space.translate(vpn).unwrap().ppn();
+ let dst_ppn = memory_set.translate(vpn).unwrap().ppn();
+ dst_ppn.get_bytes_array().copy_from_slice(src_ppn.get_bytes_array());
+ }
+ }
+ memory_set
+ }
+ }
+
+这需要对内存管理子模块 ``mm`` 做一些拓展:
+
+- 第 4 行的 ``MapArea::from_another`` 可以从一个逻辑段复制得到一个虚拟地址区间、映射方式和权限控制均相同的逻辑段,
+ 不同的是由于它还没有真正被映射到物理页帧上,所以 ``data_frames`` 字段为空。
+- 第 18 行的 ``MemorySet::from_existed_user`` 可以复制一个完全相同的地址空间。首先在第 19 行,我们通过 ``new_bare``
+ 新创建一个空的地址空间,并在第 21 行通过 ``map_trampoline`` 为这个地址空间映射上跳板页面,这是因为我们解析 ELF
+ 创建地址空间的时候,并没有将跳板页作为一个单独的逻辑段插入到地址空间的逻辑段向量 ``areas`` 中,所以这里需要单独映射上。
+
+ 剩下的逻辑段都包含在 ``areas`` 中。我们遍历原地址空间中的所有逻辑段,将复制之后的逻辑段插入新的地址空间,
+ 在插入的时候就已经实际分配了物理页帧了。接着我们遍历逻辑段中的每个虚拟页面,对应完成数据复制,
+ 这只需要找出两个地址空间中的虚拟页面各被映射到哪个物理页帧,就可转化为将数据从物理内存中的一个位置复制到另一个位置,使用
+ ``copy_from_slice`` 即可轻松实现。
+
+接着,我们实现 ``TaskControlBlock::fork`` 来从父进程的进程控制块创建一份子进程的控制块:
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/task/task.rs
+
+ impl TaskControlBlock {
+ pub fn fork(self: &Arc) -> Arc {
+ // ---- access parent PCB exclusively
+ let mut parent_inner = self.inner_exclusive_access();
+ // copy user space(include trap context)
+ let memory_set = MemorySet::from_existed_user(&parent_inner.memory_set);
+ let trap_cx_ppn = memory_set
+ .translate(VirtAddr::from(TRAP_CONTEXT).into())
+ .unwrap()
+ .ppn();
+ // alloc a pid and a kernel stack in kernel space
+ let pid_handle = pid_alloc();
+ let kernel_stack = KernelStack::new(&pid_handle);
+ let kernel_stack_top = kernel_stack.get_top();
+ let task_control_block = Arc::new(TaskControlBlock {
+ pid: pid_handle,
+ kernel_stack,
+ inner: unsafe {
+ UPSafeCell::new(TaskControlBlockInner {
+ trap_cx_ppn,
+ base_size: parent_inner.base_size,
+ task_cx: TaskContext::goto_trap_return(kernel_stack_top),
+ task_status: TaskStatus::Ready,
+ memory_set,
+ parent: Some(Arc::downgrade(self)),
+ children: Vec::new(),
+ exit_code: 0,
+ })
+ },
+ });
+ // add child
+ parent_inner.children.push(task_control_block.clone());
+ // modify kernel_sp in trap_cx
+ // **** access children PCB exclusively
+ let trap_cx = task_control_block.inner_exclusive_access().get_trap_cx();
+ trap_cx.kernel_sp = kernel_stack_top;
+ // return
+ task_control_block
+ // ---- release parent PCB automatically
+ // **** release children PCB automatically
+ }
+ }
+
+它基本上和新建进程控制块的 ``TaskControlBlock::new`` 是相同的,但要注意以下几点:
+
+- 子进程的地址空间不是通过解析 ELF,而是通过在第 8 行调用 ``MemorySet::from_existed_user`` 复制父进程地址空间得到的;
+- 在 fork 的时候需要注意父子进程关系的维护。既要将父进程的弱引用计数放到子进程的进程控制块中,又要将子进程插入到父进程的孩子向量 ``children`` 中。
+
+实现 ``sys_fork`` 时,我们需要特别注意如何体现父子进程的差异:
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/syscall/process.rs
+
+ pub fn sys_fork() -> isize {
+ let current_task = current_task().unwrap();
+ let new_task = current_task.fork();
+ let new_pid = new_task.pid.0;
+ // modify trap context of new_task, because it returns immediately after switching
+ let trap_cx = new_task.inner_exclusive_access().get_trap_cx();
+ // we do not have to move to next instruction since we have done it before
+ // for child process, fork returns 0
+ trap_cx.x[10] = 0;
+ // add new task to scheduler
+ add_task(new_task);
+ new_pid as isize
+ }
+
+在调用 ``sys_fork`` 之前,我们已经将当前进程 Trap 上下文中的 sepc 向后移动了 4 字节,使得它回到用户态之后会从 ecall
+的下一条指令开始执行。之后,当我们复制地址空间时,子进程地址空间 Trap 上下文的 sepc 也是移动之后的值,我们无需再进行修改。
+
+父子进程回到用户态的瞬间都处于刚刚从一次系统调用返回的状态,但二者返回值不同。第 8~11 行我们将子进程的 Trap
+上下文中用来存放系统调用返回值的 a0 寄存器修改为 0 ,而父进程系统调用的返回值会在 ``syscall`` 返回之后再设置为 ``sys_fork``
+的返回值。这就做到了父进程 ``fork`` 的返回值为子进程的 PID ,而子进程的返回值为 0。
+
+exec 系统调用的实现
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+``exec`` 系统调用使得一个进程能够加载一个新的 ELF 可执行文件替换原有的应用地址空间并开始执行。我们先从进程控制块的层面进行修改:
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/task/task.rs
+
+ impl TaskControlBlock {
+ pub fn exec(&self, elf_data: &[u8]) {
+ // memory_set with elf program headers/trampoline/trap context/user stack
+ let (memory_set, user_sp, entry_point) = MemorySet::from_elf(elf_data);
+ let trap_cx_ppn = memory_set
+ .translate(VirtAddr::from(TRAP_CONTEXT).into())
+ .unwrap()
+ .ppn();
+
+ // **** access inner exclusively
+ let mut inner = self.inner_exclusive_access();
+ // substitute memory_set
+ inner.memory_set = memory_set;
+ // update trap_cx ppn
+ inner.trap_cx_ppn = trap_cx_ppn;
+ // initialize trap_cx
+ let trap_cx = inner.get_trap_cx();
+ *trap_cx = TrapContext::app_init_context(
+ entry_point,
+ user_sp,
+ KERNEL_SPACE.exclusive_access().token(),
+ self.kernel_stack.get_top(),
+ trap_handler as usize,
+ );
+ // **** release inner automatically
+ }
+ }
+
+它在解析传入的 ELF 格式数据之后只做了两件事情:
+
+- 首先从 ELF 生成一个全新的地址空间并直接替换进来(第 15 行),这将导致原有地址空间生命周期结束,里面包含的全部物理页帧都会被回收;
+- 然后修改新的地址空间中的 Trap 上下文,将解析得到的应用入口点、用户栈位置以及一些内核的信息进行初始化,这样才能正常实现 Trap 机制。
+
+``sys_exec`` 的实现如下,它调用 ``translated_str`` 找到要执行的应用名,并试图从应用加载器提供的 ``get_app_data_by_name``
+接口中获取对应的 ELF 数据,如果找到的话就调用 ``TaskControlBlock::exec`` 替换地址空间。
+
+
+
+.. code-block:: rust
+
+ // os/src/syscall/process.rs
+
+ pub fn sys_exec(path: *const u8) -> isize {
+ let token = current_user_token();
+ let path = translated_str(token, path);
+ if let Some(data) = get_app_data_by_name(path.as_str()) {
+ let task = current_task().unwrap();
+ task.exec(data);
+ 0
+ } else {
+ -1
+ }
+ }
+
+应用在 ``sys_exec`` 系统调用中传递给内核的只有一个应用名字符串在用户地址空间中的首地址,内核必限手动查页表来获得字符串的值。
+
+``translated_str`` 用来从用户地址空间中查找字符串,其原理就是逐字节查页表直到发现一个 ``\0`` 为止。为什么要逐字节查页表?
+因为内核不知道字符串的长度,且字符串可能是跨物理页的。
+
+.. code-block:: rust
+
+ // os/src/mm/page_table.rs
+
+ pub fn translated_str(token: usize, ptr: *const u8) -> String {
+ let page_table = PageTable::from_token(token);
+ let mut string = String::new();
+ let mut va = ptr as usize;
+ loop {
+ let ch: u8 = *(page_table.translate_va(VirtAddr::from(va)).unwrap().get_mut());
+ if ch == 0 {
+ break;
+ } else {
+ string.push(ch as char);
+ va += 1;
+ }
+ }
+ string
+ }
+
+系统调用后重新获取 Trap 上下文
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+原来在 ``trap_handler`` 中我们是这样处理系统调用的:
+
+.. code-block:: rust
+
+ // os/src/trap/mod.rs
+
+ #[no_mangle]
+ pub fn trap_handler() -> ! {
+ set_kernel_trap_entry();
+ let cx = current_trap_cx();
+ let scause = scause::read();
+ let stval = stval::read();
+ match scause.cause() {
+ Trap::Exception(Exception::UserEnvCall) => {
+ cx.sepc += 4;
+ cx.x[10] = syscall(cx.x[17], [cx.x[10], cx.x[11], cx.x[12]]) as usize;
+ }
+ ...
+ }
+ trap_return();
+ }
+
+这里的 ``cx`` 是当前应用的 Trap 上下文的可变引用,我们需要通过查页表找到它具体被放在哪个物理页帧上,
+并构造相同的虚拟地址来在内核中访问它。对于系统调用 ``sys_exec`` 来说,调用它之后, ``trap_handler``
+原来上下文中的 ``cx`` 失效了,因为它是就原来的地址空间而言的。为了能够处理类似的这种情况,我们在 ``syscall``
+返回之后需要重新获取 ``cx`` ,目前的实现如下:
+
+.. code-block:: rust
+
+ // os/src/trap/mod.rs
+
+ #[no_mangle]
+ pub fn trap_handler() -> ! {
+ set_kernel_trap_entry();
+ let scause = scause::read();
+ let stval = stval::read();
+ match scause.cause() {
+ Trap::Exception(Exception::UserEnvCall) => {
+ // jump to next instruction anyway
+ let mut cx = current_trap_cx();
+ cx.sepc += 4;
+ // get system call return value
+ let result = syscall(cx.x[17], [cx.x[10], cx.x[11], cx.x[12]]);
+ // cx is changed during sys_exec, so we have to call it again
+ cx = current_trap_cx();
+ cx.x[10] = result as usize;
+ }
+ ...
+ }
+ trap_return();
+ }
+
+
+sys_read 获取输入
+--------------------------------------------
+
+我们需要实现 ``sys_read`` 系统调用,使应用能够取得用户的键盘输入。
+
+.. code-block:: rust
+
+ // os/src/syscall/fs.rs
+
+ use crate::sbi::console_getchar;
+
+ const FD_STDIN: usize = 0;
+
+ pub fn sys_read(fd: usize, buf: *const u8, len: usize) -> isize {
+ match fd {
+ FD_STDIN => {
+ assert_eq!(len, 1, "Only support len = 1 in sys_read!");
+ let mut c: usize;
+ loop {
+ c = console_getchar();
+ if c == 0 {
+ suspend_current_and_run_next();
+ continue;
+ } else {
+ break;
+ }
+ }
+ let ch = c as u8;
+ let mut buffers = translated_byte_buffer(current_user_token(), buf, len);
+ unsafe { buffers[0].as_mut_ptr().write_volatile(ch); }
+ 1
+ }
+ _ => {
+ panic!("Unsupported fd in sys_read!");
+ }
+ }
+ }
+
+目前我们仅支持从标准输入 ``FD_STDIN`` 即文件描述符 0 读入,且每次只能读入一个字符,这是利用 ``sbi``
+提供的接口 ``console_getchar`` 实现的。如果还没有输入,我们就切换到其他进程,等下次切换回来时再看看是否有输入了。
+获取到输入后就退出循环,并手动查页表将输入字符正确写入到应用地址空间。
+
+进程资源回收机制
+--------------------------------------------
+
+进程的退出
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+当应用调用 ``sys_exit`` 系统调用主动退出,或者出错由内核终止之后,会在内核中调用 ``exit_current_and_run_next`` 函数:
+
+.. code-block:: rust
+ :linenos:
+ :emphasize-lines: 4,29,34
+
+ // os/src/syscall/process.rs
+
+ pub fn sys_exit(exit_code: i32) -> ! {
+ exit_current_and_run_next(exit_code);
+ panic!("Unreachable in sys_exit!");
+ }
+
+ // os/src/trap/mod.rs
+
+ #[no_mangle]
+ pub fn trap_handler() -> ! {
+ set_kernel_trap_entry();
+ let scause = scause::read();
+ let stval = stval::read();
+ match scause.cause() {
+ Trap::Exception(Exception::StoreFault) |
+ Trap::Exception(Exception::StorePageFault) |
+ Trap::Exception(Exception::InstructionFault) |
+ Trap::Exception(Exception::InstructionPageFault) |
+ Trap::Exception(Exception::LoadFault) |
+ Trap::Exception(Exception::LoadPageFault) => {
+ println!(
+ "[kernel] {:?} in application, bad addr = {:#x}, bad instruction = {:#x}, core dumped.",
+ scause.cause(),
+ stval,
+ current_trap_cx().sepc,
+ );
+ // page fault exit code
+ exit_current_and_run_next(-2);
+ }
+ Trap::Exception(Exception::IllegalInstruction) => {
+ println!("[kernel] IllegalInstruction in application, core dumped.");
+ // illegal instruction exit code
+ exit_current_and_run_next(-3);
+ }
+ ...
+ }
+ trap_return();
+ }
+
+相比前面的章节, ``exit_current_and_run_next`` 带有一个退出码作为参数,这个退出码会在
+``exit_current_and_run_next`` 写入当前进程的进程控制块:
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/mm/memory_set.rs
+
+ impl MemorySet {
+ pub fn recycle_data_pages(&mut self) {
+ self.areas.clear();
+ }
+ }
+
+ // os/src/task/mod.rs
+
+ pub fn exit_current_and_run_next(exit_code: i32) {
+ // take from Processor
+ let task = take_current_task().unwrap();
+ // **** access current TCB exclusively
+ let mut inner = task.inner_exclusive_access();
+ // Change status to Zombie
+ inner.task_status = TaskStatus::Zombie;
+ // Record exit code
+ inner.exit_code = exit_code;
+ // do not move to its parent but under initproc
+
+ // ++++++ access initproc TCB exclusively
+ {
+ let mut initproc_inner = INITPROC.inner_exclusive_access();
+ for child in inner.children.iter() {
+ child.inner_exclusive_access().parent = Some(Arc::downgrade(&INITPROC));
+ initproc_inner.children.push(child.clone());
+ }
+ }
+ // ++++++ release parent PCB
+
+ inner.children.clear();
+ // deallocate user space
+ inner.memory_set.recycle_data_pages();
+ drop(inner);
+ // **** release current PCB
+ // drop task manually to maintain rc correctly
+ drop(task);
+ // we do not have to save task context
+ let mut _unused = TaskContext::zero_init();
+ schedule(&mut _unused as *mut _);
+ }
+
+
+- 第 13 行,调用 ``take_current_task`` 来将当前进程控制块从处理器监控 ``PROCESSOR``
+ 中取出,而不只是得到一份拷贝,这是为了正确维护进程控制块的引用计数;
+- 第 17 行将进程控制块中的状态修改为 ``TaskStatus::Zombie`` 即僵尸进程;
+- 第 19 行将传入的退出码 ``exit_code`` 写入进程控制块中,后续父进程在 ``waitpid`` 的时候可以收集;
+- 第 24~26 行所做的事情是,将当前进程的所有子进程挂在初始进程 ``initproc`` 下面。第 32 行将当前进程的孩子向量清空。
+- 第 34 行,对于当前进程占用的资源进行早期回收。 ``MemorySet::recycle_data_pages`` 只是将地址空间中的逻辑段列表
+ ``areas`` 清空,这将导致应用地址空间的所有数据被存放在的物理页帧被回收,而用来存放页表的那些物理页帧此时则不会被回收。
+- 最后在第 41 行我们调用 ``schedule`` 触发调度及任务切换,我们再也不会回到该进程的执行过程,因此无需关心任务上下文的保存。
+
+父进程回收子进程资源
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/syscall/process.rs
+
+ /// If there is not a child process whose pid is same as given, return -1.
+ /// Else if there is a child process but it is still running, return -2.
+ pub fn sys_waitpid(pid: isize, exit_code_ptr: *mut i32) -> isize {
+ let task = current_task().unwrap();
+ // find a child process
+
+ // ---- access current TCB exclusively
+ let mut inner = task.inner_exclusive_access();
+ if !inner
+ .children
+ .iter()
+ .any(|p| pid == -1 || pid as usize == p.getpid())
+ {
+ return -1;
+ // ---- release current PCB
+ }
+ let pair = inner.children.iter().enumerate().find(|(_, p)| {
+ // ++++ temporarily access child PCB lock exclusively
+ p.inner_exclusive_access().is_zombie() && (pid == -1 || pid as usize == p.getpid())
+ // ++++ release child PCB
+ });
+ if let Some((idx, _)) = pair {
+ let child = inner.children.remove(idx);
+ // confirm that child will be deallocated after removing from children list
+ assert_eq!(Arc::strong_count(&child), 1);
+ let found_pid = child.getpid();
+ // ++++ temporarily access child TCB exclusively
+ let exit_code = child.inner_exclusive_access().exit_code;
+ // ++++ release child PCB
+ *translated_refmut(inner.memory_set.token(), exit_code_ptr) = exit_code;
+ found_pid as isize
+ } else {
+ -2
+ }
+ // ---- release current PCB lock automatically
+ }
+
+``sys_waitpid`` 是一个立即返回的系统调用,它的返回值语义是:如果当前的进程不存在一个符合要求的子进程,则返回
+-1;如果至少存在一个,但是其中没有僵尸进程(也即仍未退出)则返回 -2;如果都不是的话则可以正常回收并返回回收子进程的
+pid 。但在编写应用的开发者看来, ``wait/waitpid`` 两个辅助函数都必定能够返回一个有意义的结果,要么是 -1,要么是一个正数
+PID ,是不存在 -2 这种通过等待即可消除的中间结果的。等待的过程由用户库 ``user_lib`` 完成。
+
+首先判断 ``sys_waitpid`` 是否会返回 -1 ,这取决于当前进程是否有一个符合要求的子进程。当传入的 ``pid`` 为 -1
+的时候,任何一个子进程都算是符合要求;但 ``pid`` 不为 -1 的时候,则只有 PID 恰好与 ``pid``
+相同的子进程才算符合条件。我们简单通过迭代器即可完成判断。
+
+再判断符合要求的子进程中是否有僵尸进程。如果找不到的话直接返回 ``-2`` ,否则进行下一步处理:
+
+我们将子进程从向量中移除并置于当前上下文中,当它所在的代码块结束,这次引用变量的生命周期结束,子进程进程控制块的引用计数将变为
+0 ,内核将彻底回收掉它占用的所有资源,包括内核栈、它的 PID 、存放页表的那些物理页帧等等。
+
+获得子进程退出码后,考虑到应用传入的指针指向应用地址空间,我们还需要手动查页表找到对应物理内存中的位置。
+``translated_refmut`` 的实现可以在 ``os/src/mm/page_table.rs`` 中找到。
\ No newline at end of file
diff --git a/guide/source/chapter5/4exercise.rst b/guide/source/chapter5/4exercise.rst
new file mode 100644
index 0000000..0a4e915
--- /dev/null
+++ b/guide/source/chapter5/4exercise.rst
@@ -0,0 +1,137 @@
+chapter5练习
+==============================================
+
+编程作业
+---------------------------------------------
+
+进程创建
++++++++++++++++++++++++++++++++++++++++++++++
+
+大家一定好奇过为啥进程创建要用 fork + exec 这么一个奇怪的系统调用,就不能直接搞一个新进程吗?
+思而不学则殆,我们就来试一试!这章的编程练习请大家实现一个完全 DIY 的系统调用 spawn,用以创建一个新进程。
+
+spawn 系统调用定义( `标准spawn看这里 `_ ):
+
+.. code-block:: rust
+
+ fn sys_spawn(path: *const u8) -> isize
+
+- syscall ID: 400
+- 功能:新建子进程,使其执行目标程序。
+- 说明:成功返回子进程id,否则返回 -1。
+- 可能的错误:
+ - 无效的文件名。
+ - 进程池满/内存不足等资源错误。
+
+TIPS:虽然测例很简单,但提醒读者 spawn **不必** 像 fork 一样复制父进程的地址空间。
+
+stride 调度算法
++++++++++++++++++++++++++++++++++++++++++
+
+ch3 中我们实现的调度算法十分简单。现在我们要为我们的 os 实现一种带优先级的调度算法:stride 调度算法。
+
+算法描述如下:
+
+(1) 为每个进程设置一个当前 stride,表示该进程当前已经运行的“长度”。另外设置其对应的 pass
+值(只与进程的优先权有关系),表示对应进程在调度后,stride 需要进行的累加值。
+
+(2) 每次需要调度时,从当前 runnable 态的进程中选择 stride 最小的进程调度。对于获得调度的进程 P,将对应的 stride 加上其对应的步长 pass。
+
+(3) 一个时间片后,回到上一步骤,重新调度当前 stride 最小的进程。
+
+可以证明,如果令 P.pass = BigStride / P.priority 其中 P.priority 表示进程的优先权(大于 1),而
+BigStride 表示一个预先定义的大常数,则该调度方案为每个进程分配的时间将与其优先级成正比。证明过程我们在这里略去,有兴趣的同学可以在网上查找相关资料。
+
+其他实验细节:
+
+- stride 调度要求进程优先级 :math:`\geq 2`,所以设定进程优先级 :math:`\leq 1` 会导致错误。
+- 进程初始 stride 设置为 0 即可。
+- 进程初始优先级设置为 16。
+
+为了实现该调度算法,内核还要增加 set_prio 系统调用
+
+.. code-block:: rust
+
+ // syscall ID:140
+ // 设置当前进程优先级为 prio
+ // 参数:prio 进程优先级,要求 prio >= 2
+ // 返回值:如果输入合法则返回 prio,否则返回 -1
+ fn sys_set_priority(prio: isize) -> isize;
+
+实现 tips:
+
+- 你可以在TCB加入新的字段来支持优先级等。
+- 为了减少整数除的误差,BIG_STRIDE 一般需要很大,但为了不至于发生反转现象(详见问答作业),或许选择一个适中的数即可,当然能进行溢出处理就更好了。
+- stride 算法要找到 stride 最小的进程,使用优先级队列是效率不错的办法,但是我们的实验测例很简单,所以效率完全不是问题。事实上,很推荐使用暴力扫一遍的办法找最小值。
+- 注意设置进程的初始优先级。
+
+.. attention::
+
+ 为了让大家能在本编程作业中使用 ``Vec`` 等数据结构,我们利用第三方库 ``buddy_system_allocator``
+ 为大家实现了堆内存分配器,相关代码位于 ``mm/heap_allocator`` 模块。
+
+ 背景知识: `Rust 中的动态内存分配 `_
+
+实验要求
++++++++++++++++++++++++++++++++++++++++++++++
+- 实现分支:ch5。
+- 实验目录请参考 ch3。注意在reports中放入lab1-3的所有报告。
+- 通过所有测例。
+
+ 在 os 目录下 ``make run BASE=2`` 加载所有测例, ``ch5_usertest`` 打包了所有你需要通过的测例,
+ 你也可以通过修改这个文件调整本地测试的内容, 或者单独运行某测例来纠正特定的错误。 ``ch5_stride``
+ 检查 stride 调度算法是否满足公平性要求,六个子程序运行的次数应该大致与其优先级呈正比,测试通过标准是
+ :math:`\max{\frac{runtimes}{prio}}/ \min{\frac{runtimes}{prio}} < 1.5`.
+
+ CI 的原理是用 ``ch5_usertest`` 替代 ``ch5b_initproc`` ,使内核在所有测例执行完后直接退出。
+
+ 从本章开始,你的内核必须前向兼容,能通过前一章的所有测例。
+
+.. note::
+
+ 利用 ``git cherry-pick`` 系列指令,能方便地将前一章分支 commit 移植到本章分支。
+
+问答作业
+--------------------------------------------
+
+stride 算法深入
+
+ stride 算法原理非常简单,但是有一个比较大的问题。例如两个 pass = 10 的进程,使用 8bit 无符号整形储存
+ stride, p1.stride = 255, p2.stride = 250,在 p2 执行一个时间片后,理论上下一次应该 p1 执行。
+
+ - 实际情况是轮到 p1 执行吗?为什么?
+
+ 我们之前要求进程优先级 >= 2 其实就是为了解决这个问题。可以证明, **在不考虑溢出的情况下** , 在进程优先级全部 >= 2
+ 的情况下,如果严格按照算法执行,那么 STRIDE_MAX – STRIDE_MIN <= BigStride / 2。
+
+ - 为什么?尝试简单说明(不要求严格证明)。
+
+ - 已知以上结论,**考虑溢出的情况下**,可以为 Stride 设计特别的比较器,让 BinaryHeap 的 pop
+ 方法能返回真正最小的 Stride。补全下列代码中的 ``partial_cmp`` 函数,假设两个 Stride 永远不会相等。
+
+ .. code-block:: rust
+
+ use core::cmp::Ordering;
+
+ struct Stride(u64);
+
+ impl PartialOrd for Stride {
+ fn partial_cmp(&self, other: &Self) -> Option {
+ // ...
+ }
+ }
+
+ impl PartialEq for Stride {
+ fn eq(&self, other: &Self) -> bool {
+ false
+ }
+ }
+
+ TIPS: 使用 8 bits 存储 stride, BigStride = 255, 则: ``(125 < 255) == false``, ``(129 < 255) == true``.
+
+报告要求
+------------------------------------------------------------
+
+- 简单总结你实现的功能(200字以内,不要贴代码)。
+- 完成问答题。
+- (optional) 你对本次实验设计及难度/工作量的看法,以及有哪些需要改进的地方,欢迎畅所欲言。
diff --git a/guide/source/chapter5/index.rst b/guide/source/chapter5/index.rst
new file mode 100644
index 0000000..7aaf8e1
--- /dev/null
+++ b/guide/source/chapter5/index.rst
@@ -0,0 +1,12 @@
+第五章:进程及进程管理
+==============================================
+
+.. toctree::
+ :maxdepth: 4
+
+ 0intro
+ 1process
+ 2core-data-structures
+ 3implement-process-mechanism
+ 4exercise
+
\ No newline at end of file
diff --git a/guide/source/chapter6/0intro.rst b/guide/source/chapter6/0intro.rst
new file mode 100644
index 0000000..4e7dd2a
--- /dev/null
+++ b/guide/source/chapter6/0intro.rst
@@ -0,0 +1,159 @@
+引言
+=========================================
+
+本章导读
+-----------------------------------------
+
+本章我们将实现一个简单的文件系统 -- easyfs,能够对 **持久存储设备** (Persistent Storage) I/O 资源进行管理;将设计两种文件:常规文件和目录文件,它们均以文件系统所维护的 **磁盘文件** 形式被组织并保存在持久存储设备上。
+
+实践体验
+-----------------------------------------
+
+获取本章代码:
+
+.. code-block:: console
+
+ $ git clone https://github.com/LearningOS/rCore-Tutorial-Code-2022S.git
+ $ cd rCore-Tutorial-Code-2022S
+ $ git checkout ch6
+
+在 qemu 模拟器上运行本章代码:
+
+.. code-block:: console
+
+ $ cd os
+ $ make run
+
+内核初始化完成之后就会进入shell程序,在这里我们运行一下本章的测例 ``ch6b_filetest_simple`` :
+
+.. code-block::
+
+ >> ch6b_filetest_simple
+ file_test passed!
+ Shell: Process 2 exited with code 0
+ >>
+
+它会将 ``Hello, world!`` 输出到另一个文件 ``filea`` ,并读取里面的内容确认输出正确。我们也可以通过命令行工具 ``ch6b_cat`` 来查看 ``filea`` 中的内容:
+
+.. code-block::
+
+ >> ch6b_cat
+ Hello, world!
+ Shell: Process 2 exited with code 0
+ >>
+
+本章代码树
+-----------------------------------------
+
+.. code-block::
+ :linenos:
+
+ ├── easy-fs(新增:从内核中独立出来的一个简单的文件系统 EasyFileSystem 的实现)
+ │ ├── Cargo.toml
+ │ └── src
+ │ ├── bitmap.rs(位图抽象)
+ │ ├── block_cache.rs(块缓存层,将块设备中的部分块缓存在内存中)
+ │ ├── block_dev.rs(声明块设备抽象接口 BlockDevice,需要库的使用者提供其实现)
+ │ ├── efs.rs(实现整个 EasyFileSystem 的磁盘布局)
+ │ ├── layout.rs(一些保存在磁盘上的数据结构的内存布局)
+ │ ├── lib.rs
+ │ └── vfs.rs(提供虚拟文件系统的核心抽象,即索引节点 Inode)
+ ├── easy-fs-fuse(新增:将当前 OS 上的应用可执行文件按照 easy-fs 的格式进行打包)
+ │ ├── Cargo.toml
+ │ └── src
+ │ └── main.rs
+ ├── os
+ ├── build.rs(修改:不再需要将用户态程序链接到内核中)
+ ├── Cargo.toml(修改:新增 Qemu 的块设备驱动依赖 crate)
+ ├── Makefile(修改:新增文件系统的构建流程)
+ └── src
+ ├── config.rs(修改:新增访问块设备所需的一些 MMIO 配置)
+ ├── ...
+ ├── drivers(新增:Qemu 平台的块设备驱动)
+ │ ├── block
+ │ │ ├── mod.rs(将不同平台上的块设备全局实例化为 BLOCK_DEVICE 提供给其他模块使用)
+ │ │ └── virtio_blk.rs(Qemu 平台的 virtio-blk 块设备)
+ │ └── mod.rs
+ ├── fs(新增:对文件系统及文件抽象)
+ │ ├── inode.rs(新增:将 easy-fs 提供的 Inode 抽象封装为内核看到的 OSInode
+ │ │ 并实现 fs 子模块的 File Trait)
+ │ ├── mod.rs
+ │ └── stdio.rs(新增:将标准输入输出也抽象为文件)
+ ├── loader.rs(移除:应用加载器 loader 子模块,本章开始从文件系统中加载应用)
+ ├── mm
+ │ ├── address.rs
+ │ ├── frame_allocator.rs
+ │ ├── heap_allocator.rs
+ │ ├── memory_set.rs(修改:在创建地址空间的时候插入 MMIO 虚拟页面)
+ │ ├── mod.rs
+ │ └── page_table.rs(新增:应用地址空间的缓冲区抽象 UserBuffer 及其迭代器实现)
+ ├── syscall
+ │ ├── fs.rs(修改:新增 sys_open,修改sys_read、sys_write)
+ │ ├── mod.rs
+ │ └── process.rs(修改:sys_exec 改为从文件系统中加载 ELF)
+ ├── task
+ ├── context.rs
+ ├── manager.rs
+ ├── mod.rs(修改:初始进程 INITPROC 的初始化)
+ ├── pid.rs
+ ├── processor.rs
+ ├── switch.rs
+ ├── switch.S
+ └── task.rs(修改:在任务控制块中加入文件描述符表的相关机制)
+
+ cloc easy-fs os
+ -------------------------------------------------------------------------------
+ Language files blank comment code
+ -------------------------------------------------------------------------------
+ Rust 41 306 418 3349
+ Assembly 4 53 26 526
+ make 1 13 4 48
+ TOML 2 4 2 23
+ -------------------------------------------------------------------------------
+ SUM: 48 376 450 3946
+ -------------------------------------------------------------------------------
+
+.. 本章代码导读
+.. -----------------------------------------------------
+
+.. 本章涉及的代码量相对较多,且与进程执行相关的管理还有直接的关系。其实我们是参考经典的UNIX基于索引的文件系统,设计了一个简化的有一级目录并支持创建/打开/读写/关闭文件一系列操作的文件系统。这里简要介绍一下在内核中添加文件系统的大致开发过程。
+
+.. 第一步是能够写出与文件访问相关的应用。这里是参考了Linux的创建/打开/读写/关闭文件的系统调用接口,力图实现一个 :ref:`简化版的文件系统模型 ` 。在用户态我们只需要遵从相关系统调用的接口约定,在用户库里完成对应的封装即可。这一过程我们在前面的章节中已经重复过多次,读者应当对其比较熟悉。其中最为关键的是系统调用可以参考 :ref:`sys_open 语义介绍 ` ,此外我们还给出了 :ref:`测例代码解读 ` 。
+
+.. 第二步就是要实现 easyfs 文件系统了。由于 Rust 语言的特点,我们可以在用户态实现 easyfs 文件系统,并在用户态完成文件系统功能的基本测试并基本验证其实现正确性之后,就可以放心的将该模块嵌入到操作系统内核中。当然,有了文件系统的具体实现,还需要对上一章的操作系统内核进行扩展,实现与 easyfs 文件系统对接的接口,这样才可以让操作系统拥有一个简单可用的文件系统。从而,内核可以支持允许文件读写功能的更复杂的应用,在命令行参数机制的加持下,可以进一步提升整个系统的灵活性,让应用的开发和调试变得更为轻松。
+
+.. easyfs 文件系统的整体架构自下而上可分为五层。它的最底层就是对块设备的访问操作接口。在 ``easy-fs/src/block_dev.rs`` 中,可以看到 ``BlockDevice`` trait 代表了一个抽象块设备,该 trait 仅需求两个函数 ``read_block`` 和 ``write_block`` ,分别代表将数据从块设备读到内存中的缓冲区中,或者将数据从内存中的缓冲区写回到块设备中,数据需要以块为单位进行读写。easy-fs 库的使用者需要负责为它们看到的实际的块设备具体实现 ``BlockDevice`` trait 并提供给 easy-fs 库的上层,这样的话 easy-fs 库的最底层就与一个具体的执行环境对接起来了。至于为什么块设备层位于 easy-fs 的最底层,是因为文件系统仅仅是在块设备上存储的结构稍微复杂一点的数据,但无论它的操作变换如何复杂,从块设备的角度终究可以被分解成若干次块读写。
+
+.. 尽管在最底层我们就已经有了块读写的能力,但从编程方便性和性能的角度,仅有块读写这么基础的底层接口是不足以实现如此复杂的文件系统的,虽然它已经被我们大幅简化过了。比如,将一个块的内容读到内存的缓冲区,对缓冲区进行修改,并尚未写回的时候,如果由于编程上的不小心再次将该块的内容读到另一个缓冲区,而不是使用已有的缓冲区,这将会造成不一致问题。此外还有可能增加很多不必要的块读写次数,大幅降低文件系统的性能。因此,通过程序自动而非程序员手动对块的缓冲区进行统一管理也就势在必行了,该机制被我们抽象为 easy-fs 自底向上的第二层,即块缓存层。在 ``easy-fs/src/block_cache.rs`` 中, ``BlockCache`` 代表一个被我们管理起来的块的缓冲区,它带有缓冲区本体以及块的编号等信息。当它被创建的时候,将触发一次 ``read_block`` 将数据从块设备读到它的缓冲区中。接下来只要它驻留在内存中,便可保证对于同一个块的所有操作都会直接在它的缓冲区中进行而无需额外的 ``read_block`` 。块缓存管理器 ``BlockManager`` 在内存中管理有限个 ``BlockCache`` 并实现了类似 FIFO 的缓存替换算法,当一个块缓存被换出的时候视情况可能调用 ``write_block`` 将缓冲区数据写回块设备。总之,块缓存层对上提供 ``get_block_cache`` 接口来屏蔽掉相关细节,从而可以透明的读写一个块。
+
+.. 有了块缓存,我们就可以在内存中方便地处理easyfs文件系统在磁盘上的各种数据了,这就是第三层文件系统的磁盘数据结构。easyfs文件系统中的所有需要持久保存的数据都会放到磁盘上,这包括了管理这个文件系统的 **超级块 (Super Block)**,管理空闲磁盘块的 **索引节点位图区** 和 **数据块位图区** ,以及管理文件的 **索引节点区** 和 放置文件数据的 **数据块区** 组成。
+
+.. easyfs文件系统中管理这些磁盘数据的控制逻辑主要集中在 **磁盘块管理器** 中,这是文件系统的第四层。对于文件系统管理而言,其核心是 ``EasyFileSystem`` 数据结构及其关键成员函数:
+
+.. - EasyFileSystem.create:创建文件系统
+.. - EasyFileSystem.open:打开文件系统
+.. - EasyFileSystem.alloc_inode:分配inode (dealloc_inode未实现,所以还不能删除文件)
+.. - EasyFileSystem.alloc_data:分配数据块
+.. - EasyFileSystem.dealloc_data:回收数据块
+
+.. 对于单个文件的管理和读写的控制逻辑主要是 **索引节点** 来完成,这是文件系统的第五层,其核心是 ``Inode`` 数据结构及其关键成员函数:
+
+.. - Inode.new:在磁盘上的文件系统中创建一个inode
+.. - Inode.find:根据文件名查找对应的磁盘上的inode
+.. - Inode.create:在根目录下创建一个文件
+.. - Inode.read_at:根据inode找到文件数据所在的磁盘数据块,并读到内存中
+.. - Inode.write_at:根据inode找到文件数据所在的磁盘数据块,把内存中数据写入到磁盘数据块中
+
+.. 上述五层就构成了easyfs文件系统的整个内容。我们可以把easyfs文件系统看成是一个库,被应用程序调用。而 ``easy-fs-fuse`` 这个应用就通过调用easyfs文件系统库中各种函数,并用Linux上的文件模拟了一个块设备,就可以在这个模拟的块设备上创建了一个easyfs文件系统。
+
+.. 第三步,我们需要把easyfs文件系统加入到我们的操作系统内核中。这还需要做两件事情,第一件是在Qemu模拟的 ``virtio`` 块设备上实现块设备驱动程序 ``os/src/drivers/block/virtio_blk.rs`` 。由于我们可以直接使用 ``virtio-drivers`` crate中的块设备驱动,所以只要提供这个块设备驱动所需要的内存申请与释放以及虚实地址转换的4个函数就可以了。而我们之前操作系统中的虚存管理实现中,以及有这些函数,导致块设备驱动程序很简单,具体实现细节都被 ``virtio-drivers`` crate封装好了。
+
+.. 第二件事情是把文件访问相关的系统调用与easyfs文件系统连接起来。在easfs文件系统中是没有进程的概念的。而进程是程序运行过程中访问资源的管理实体,这就要对 ``easy-fs`` crate 提供的 ``Inode`` 结构进一步封装,形成 ``OSInode`` 结构,以表示进程中一个打开的常规文件。对于应用程序而言,它理解的磁盘数据是常规的文件和目录,不是 ``OSInode`` 这样相对复杂的结构。其实常规文件对应的 OSInode 是文件在操作系统内核中的内部表示,因此需要为它实现 File Trait 从而能够可以将它放入到进程文件描述符表中,并通过 sys_read/write 系统调用进行读写。这样就建立了文件与 ``OSInode`` 的对应关系,并通过上面描述的三个步骤完成了包含文件系统的操作系统内核,并能给应用提供基于文件的系统调用服务。
+
+.. 完成包含文件系统的操作系统内核后,我们在shell程序和内核中支持命令行参数的解析和传递,这样可以让应用根据灵活地通过命令行参数来动态地表示要操作的文件。这需要扩展对应的系统调用 ``sys_exec`` ,主要的改动就是在创建新进程时,把命令行参数压入用户栈中,这样应用程序在执行时就可以从用户栈中获取到命令行的参数值了。
+
+.. 在上一章,我们提到了把标准输出设备在文件描述符表中的文件描述符的值规定为 1 ,用 Stdin 表示;把标准输入设备在文件描述符表中的文件描述符的值规定为 0,用 stdout 表示 。另外,还有一条文件描述符相关的重要规则:即进程打开一个文件的时候,内核总是会将文件分配到该进程文件描述符表中编号 最小的 空闲位置。利用这些约定,只实现新的系统调用 ``sys_dup`` 完成对文件描述符的复制,就可以巧妙地实现标准 I/O 重定向功能了。
+
+.. 具体思路是,在某应用进程执行之前,父进程(比如 user_shell进程)要对子应用进程的文件描述符表进行某种替换。以输出为例,父进程在创建子进程前,提前打开一个常规文件 A,然后 ``fork`` 子进程,在子进程的最初执行中,通过 ``sys_close`` 关闭 Stdout 文件描述符,用 ``sys_dup`` 复制常规文件 A 的文件描述符,这样 Stdout 文件描述符实际上指向的就是常规文件A了,这时再通过 ``sys_close`` 关闭常规文件 A 的文件描述符。至此,常规文件 A 替换掉了应用文件描述符表位置 1 处的标准输出文件,这就完成了所谓的 **重定向** ,即完成了执行新应用前的准备工作。
+
+.. 接下来是子进程调用 ``sys_exec`` 系统调用,创建并开始执行新子应用进程。在重定向之后,新的子应用进程认为自己输出到 fd=1 的标准输出文件,但实际上是输出到父进程(比如 user_shell进程)指定的文件A中。文件这一抽象概念透明化了文件、I/O设备之间的差异,因为在进程看来无论是标准输出还是常规文件都是一种文件,可以通过同样的接口来读写。这就是文件的强大之处。
diff --git a/guide/source/chapter6/1file-descriptor.rst b/guide/source/chapter6/1file-descriptor.rst
new file mode 100644
index 0000000..8fe5b58
--- /dev/null
+++ b/guide/source/chapter6/1file-descriptor.rst
@@ -0,0 +1,243 @@
+文件与文件描述符
+===========================================
+
+文件简介
+-------------------------------------------
+
+文件可代表很多种不同类型的I/O 资源,但是在进程看来,所有文件的访问都可以通过一个简洁统一的抽象接口 ``File`` 进行:
+
+.. code-block:: rust
+
+ // os/src/fs/mod.rs
+
+ pub trait File : Send + Sync {
+ fn readable(&self) -> bool;
+ fn writable(&self) -> bool;
+ fn read(&self, buf: UserBuffer) -> usize;
+ fn write(&self, buf: UserBuffer) -> usize;
+ }
+
+
+这个接口在内存和I/O资源之间建立了数据交换的通道。其中 ``UserBuffer`` 是我们在 ``mm`` 子模块中定义的应用地址空间中的一段缓冲区,我们可以将它看成一个 ``&[u8]`` 切片。
+
+``read`` 指的是从文件(即I/O资源)中读取数据放到缓冲区中,最多将缓冲区填满(即读取缓冲区的长度那么多字节),并返回实际读取的字节数;而 ``write`` 指的是将缓冲区中的数据写入文件,最多将缓冲区中的数据全部写入,并返回直接写入的字节数。
+
+回过头来再看一下用户缓冲区的抽象 ``UserBuffer`` ,它的声明如下:
+
+.. code-block:: rust
+
+ // os/src/mm/page_table.rs
+
+ pub fn translated_byte_buffer(
+ token: usize,
+ ptr: *const u8,
+ len: usize
+ ) -> Vec<&'static mut [u8]>;
+
+ pub struct UserBuffer {
+ pub buffers: Vec<&'static mut [u8]>,
+ }
+
+ impl UserBuffer {
+ pub fn new(buffers: Vec<&'static mut [u8]>) -> Self {
+ Self { buffers }
+ }
+ pub fn len(&self) -> usize {
+ let mut total: usize = 0;
+ for b in self.buffers.iter() {
+ total += b.len();
+ }
+ total
+ }
+ }
+
+它只是将我们调用 ``translated_byte_buffer`` 获得的包含多个切片的 ``Vec`` 进一步包装起来,通过 ``len`` 方法可以得到缓冲区的长度。此外,我们还让它作为一个迭代器可以逐字节进行读写。有兴趣的读者可以参考类型 ``UserBufferIterator`` 还有 ``IntoIterator`` 和 ``Iterator`` 两个 Trait 的使用方法。
+
+标准输入和标准输出
+--------------------------------------------
+
+其实我们在第二章就对应用程序引入了基于 **文件** 的标准输出接口 ``sys_write`` ,在第五章引入标准输入接口 ``sys_read`` 。我们提前把标准输出设备在文件描述符表中的文件描述符的值规定为 ``1`` ,用 ``Stdout`` 表示;把标准输入设备文件描述符规定为 ``0``,用 ``Stdin`` 表示 。现在,我们重写这些系统调用,先为标准输入和标准输出实现 ``File`` Trait:
+
+.. code-block:: rust
+ :linenos:
+
+ // os/src/fs/stdio.rs
+
+ pub struct Stdin;
+
+ pub struct Stdout;
+
+ impl File for Stdin {
+ fn readable(&self) -> bool { true }
+ fn writable(&self) -> bool { false }
+ fn read(&self, mut user_buf: UserBuffer) -> usize {
+ assert_eq!(user_buf.len(), 1);
+ // busy loop
+ let mut c: usize;
+ loop {
+ c = console_getchar();
+ if c == 0 {
+ suspend_current_and_run_next();
+ continue;
+ } else {
+ break;
+ }
+ }
+ let ch = c as u8;
+ unsafe { user_buf.buffers[0].as_mut_ptr().write_volatile(ch); }
+ 1
+ }
+ fn write(&self, _user_buf: UserBuffer) -> usize {
+ panic!("Cannot write to stdin!");
+ }
+ }
+
+ impl File for Stdout {
+ fn readable(&self) -> bool { false }
+ fn writable(&self) -> bool { true }
+ fn read(&self, _user_buf: UserBuffer) -> usize{
+ panic!("Cannot read from stdout!");
+ }
+ fn write(&self, user_buf: UserBuffer) -> usize {
+ for buffer in user_buf.buffers.iter() {
+ print!("{}", core::str::from_utf8(*buffer).unwrap());
+ }
+ user_buf.len()
+ }
+ }
+
+可以看到,标准输入文件 ``Stdin`` 是只读文件,只允许进程通过 ``read`` 从里面读入,目前每次仅支持读入一个字符,其实现与之前的 ``sys_read`` 基本相同,只是需要通过 ``UserBuffer`` 来获取具体将字节写入的位置。相反,标准输出文件 ``Stdout`` 是只写文件,只允许进程通过 ``write`` 写入到里面,实现方法是遍历每个切片,将其转化为字符串通过 ``print!`` 宏来输出。
+
+文件描述符与文件描述符表
+--------------------------------------------
+
+为简化操作系统设计实现,可以让每个进程都带有一个线性的 **文件描述符表** ,记录所有它请求内核打开并可以读写的那些文件集合。而 **文件描述符** (File Descriptor) 则是一个非负整数,表示文件描述符表中一个打开的 **文件描述符** 所处的位置(可理解为数组下标)。进程通过文件描述符,可以在自身的文件描述符表中找到对应的文件记录信息,从而也就找到了对应的文件,并对文件进行读写。当打开( ``open`` )或创建( ``create`` ) 一个文件的时候,如果顺利,内核会返回给应用刚刚打开或创建的文件对应的文件描述符;而当应用想关闭( ``close`` )一个文件的时候,也需要向内核提供对应的文件描述符。
+
+
+文件I/O操作
+-------------------------------------------
+
+在进程控制块中加入文件描述符表的相应字段:
+
+.. code-block:: rust
+ :linenos:
+ :emphasize-lines: 12
+
+ // os/src/task/task.rs
+
+ pub struct TaskControlBlockInner {
+ pub trap_cx_ppn: PhysPageNum,
+ pub base_size: usize,
+ pub task_cx: TaskContext,
+ pub task_status: TaskStatus,
+ pub memory_set: MemorySet,
+ pub parent: Option>,
+ pub children: Vec>,
+ pub exit_code: i32,
+ pub fd_table: Vec