├── 实验2实验报告TyporaGallery
├── END.png
├── A2.1.png
├── A2.2.png
├── A2.3.png
├── A2.4.png
├── A2.5.png
├── A2.6.png
├── A3.1.png
├── LAST.png
├── A1-1实验结果.png
└── A1实验结果.png
├── 实验3实验报告TyporaGallery
├── 段描述符.PNG
├── image-20240325212549105.png
├── image-20240325215219973.png
├── image-20240328103718515.png
├── image-20240328105036828.png
├── image-20240328105853638.png
├── image-20240328110148915.png
├── image-20240328110150745.png
├── image-20240328110617270.png
├── image-20240328110929625.png
├── image-20240328113454994.png
├── image-20240328113857657.png
├── image-20240328114508621.png
├── image-20240328114829933.png
├── image-20240328114941124.png
├── image-20240328140958022.png
└── image-20240328143144006.png
├── 实验8实验报告TyporaGallery
├── image-20240603104830454.png
├── image-20240603105441711.png
├── image-20240603110731308.png
├── image-20240603111743521.png
├── image-20240603112804861.png
├── image-20240603113857161.png
├── image-20240603113905497.png
├── image-20240603114456818.png
├── image-20240603131906689.png
├── image-20240603204334891.png
├── image-20240603204637084.png
├── image-20240603204806942.png
├── image-20240603205242460.png
├── image-20240603205908054.png
├── image-20240603210332497.png
├── image-20240603210823094.png
├── image-20240603210921087.png
├── image-20240603211047149.png
├── image-20240603212946280.png
├── image-20240603213639781.png
├── image-20240603213851804.png
├── image-20240603214355759.png
├── image-20240603215020809.png
├── image-20240603215315363.png
├── image-20240603225609603.png
├── image-20240603230248919.png
├── image-20240604143809060.png
├── image-20240604143828351.png
├── image-20240604150039123.png
├── image-20240604150658608.png
├── image-20240604151929158.png
├── image-20240604153650017.png
└── image-20240604154055715.png
├── 实验七实验报告TyporaGallery
├── image-20240601171904609.png
├── image-20240601200512566.png
├── image-20240602165829427.png
└── image-20240602235700022.png
├── 实验五实验报告TyporaGallery
├── image-20240509203659770.png
├── image-20240509204104728.png
├── image-20240509210101230.png
├── image-20240511101717552.png
├── image-20240511102548416.png
├── image-20240511103419892.png
├── image-20240511103722579.png
├── image-20240511104443656.png
├── image-20240511105012487.png
├── image-20240511105619310.png
├── image-20240511110917937.png
├── image-20240511111148837.png
├── image-20240511111924580.png
├── image-20240511112038324.png
├── image-20240511113459404.png
├── image-20240511114404019.png
├── image-20240511114920687.png
├── image-20240511114940468.png
├── image-20240511115522863.png
├── image-20240511120409621.png
├── image-20240511135826875.png
├── image-20240511150613053.png
├── image-20240511150717362.png
├── image-20240511150926038.png
├── image-20240511154231264.png
├── image-20240512010445918.png
├── image-20240512013730506.png
├── image-20240512013839361.png
└── image-20240512014612713.png
├── 实验六实验报告TyporaGallery
├── image-20240513204558707.png
├── image-20240513210327468.png
├── image-20240514145124769.png
├── image-20240514150329687.png
├── image-20240514151024191.png
├── image-20240514155110805.png
├── image-20240514161734542.png
├── image-20240514163012037.png
├── image-20240514165545163.png
└── image-20240514165957341.png
├── 实验四实验报告TyporaGallery
├── image-20240414152009354.png
├── image-20240414154222504.png
├── image-20240414173235814.png
├── image-20240415203556350.png
├── image-20240415203818866.png
├── image-20240415214610657.png
├── image-20240415232832072.png
├── image-20240416004650958.png
└── image-20240416004708664.png
├── README.md
├── 07_实验七实验报告.md
├── 02_实验2实验报告.md
├── 06_实验六实验报告.md
├── 03_实验3实验报告.md
├── 08_实验8实验报告.md
├── 05_实验五实验报告.md
└── 04_实验四实验报告.md
/实验2实验报告TyporaGallery/END.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验2实验报告TyporaGallery/END.png
--------------------------------------------------------------------------------
/实验2实验报告TyporaGallery/A2.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验2实验报告TyporaGallery/A2.1.png
--------------------------------------------------------------------------------
/实验2实验报告TyporaGallery/A2.2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验2实验报告TyporaGallery/A2.2.png
--------------------------------------------------------------------------------
/实验2实验报告TyporaGallery/A2.3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验2实验报告TyporaGallery/A2.3.png
--------------------------------------------------------------------------------
/实验2实验报告TyporaGallery/A2.4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验2实验报告TyporaGallery/A2.4.png
--------------------------------------------------------------------------------
/实验2实验报告TyporaGallery/A2.5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验2实验报告TyporaGallery/A2.5.png
--------------------------------------------------------------------------------
/实验2实验报告TyporaGallery/A2.6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验2实验报告TyporaGallery/A2.6.png
--------------------------------------------------------------------------------
/实验2实验报告TyporaGallery/A3.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验2实验报告TyporaGallery/A3.1.png
--------------------------------------------------------------------------------
/实验2实验报告TyporaGallery/LAST.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验2实验报告TyporaGallery/LAST.png
--------------------------------------------------------------------------------
/实验3实验报告TyporaGallery/段描述符.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验3实验报告TyporaGallery/段描述符.PNG
--------------------------------------------------------------------------------
/实验2实验报告TyporaGallery/A1-1实验结果.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验2实验报告TyporaGallery/A1-1实验结果.png
--------------------------------------------------------------------------------
/实验2实验报告TyporaGallery/A1实验结果.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验2实验报告TyporaGallery/A1实验结果.png
--------------------------------------------------------------------------------
/实验3实验报告TyporaGallery/image-20240325212549105.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验3实验报告TyporaGallery/image-20240325212549105.png
--------------------------------------------------------------------------------
/实验3实验报告TyporaGallery/image-20240325215219973.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验3实验报告TyporaGallery/image-20240325215219973.png
--------------------------------------------------------------------------------
/实验3实验报告TyporaGallery/image-20240328103718515.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验3实验报告TyporaGallery/image-20240328103718515.png
--------------------------------------------------------------------------------
/实验3实验报告TyporaGallery/image-20240328105036828.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验3实验报告TyporaGallery/image-20240328105036828.png
--------------------------------------------------------------------------------
/实验3实验报告TyporaGallery/image-20240328105853638.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验3实验报告TyporaGallery/image-20240328105853638.png
--------------------------------------------------------------------------------
/实验3实验报告TyporaGallery/image-20240328110148915.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验3实验报告TyporaGallery/image-20240328110148915.png
--------------------------------------------------------------------------------
/实验3实验报告TyporaGallery/image-20240328110150745.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验3实验报告TyporaGallery/image-20240328110150745.png
--------------------------------------------------------------------------------
/实验3实验报告TyporaGallery/image-20240328110617270.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验3实验报告TyporaGallery/image-20240328110617270.png
--------------------------------------------------------------------------------
/实验3实验报告TyporaGallery/image-20240328110929625.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验3实验报告TyporaGallery/image-20240328110929625.png
--------------------------------------------------------------------------------
/实验3实验报告TyporaGallery/image-20240328113454994.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验3实验报告TyporaGallery/image-20240328113454994.png
--------------------------------------------------------------------------------
/实验3实验报告TyporaGallery/image-20240328113857657.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验3实验报告TyporaGallery/image-20240328113857657.png
--------------------------------------------------------------------------------
/实验3实验报告TyporaGallery/image-20240328114508621.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验3实验报告TyporaGallery/image-20240328114508621.png
--------------------------------------------------------------------------------
/实验3实验报告TyporaGallery/image-20240328114829933.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验3实验报告TyporaGallery/image-20240328114829933.png
--------------------------------------------------------------------------------
/实验3实验报告TyporaGallery/image-20240328114941124.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验3实验报告TyporaGallery/image-20240328114941124.png
--------------------------------------------------------------------------------
/实验3实验报告TyporaGallery/image-20240328140958022.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验3实验报告TyporaGallery/image-20240328140958022.png
--------------------------------------------------------------------------------
/实验3实验报告TyporaGallery/image-20240328143144006.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验3实验报告TyporaGallery/image-20240328143144006.png
--------------------------------------------------------------------------------
/实验8实验报告TyporaGallery/image-20240603104830454.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验8实验报告TyporaGallery/image-20240603104830454.png
--------------------------------------------------------------------------------
/实验8实验报告TyporaGallery/image-20240603105441711.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验8实验报告TyporaGallery/image-20240603105441711.png
--------------------------------------------------------------------------------
/实验8实验报告TyporaGallery/image-20240603110731308.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验8实验报告TyporaGallery/image-20240603110731308.png
--------------------------------------------------------------------------------
/实验8实验报告TyporaGallery/image-20240603111743521.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验8实验报告TyporaGallery/image-20240603111743521.png
--------------------------------------------------------------------------------
/实验8实验报告TyporaGallery/image-20240603112804861.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验8实验报告TyporaGallery/image-20240603112804861.png
--------------------------------------------------------------------------------
/实验8实验报告TyporaGallery/image-20240603113857161.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验8实验报告TyporaGallery/image-20240603113857161.png
--------------------------------------------------------------------------------
/实验8实验报告TyporaGallery/image-20240603113905497.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验8实验报告TyporaGallery/image-20240603113905497.png
--------------------------------------------------------------------------------
/实验8实验报告TyporaGallery/image-20240603114456818.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验8实验报告TyporaGallery/image-20240603114456818.png
--------------------------------------------------------------------------------
/实验8实验报告TyporaGallery/image-20240603131906689.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验8实验报告TyporaGallery/image-20240603131906689.png
--------------------------------------------------------------------------------
/实验8实验报告TyporaGallery/image-20240603204334891.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验8实验报告TyporaGallery/image-20240603204334891.png
--------------------------------------------------------------------------------
/实验8实验报告TyporaGallery/image-20240603204637084.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验8实验报告TyporaGallery/image-20240603204637084.png
--------------------------------------------------------------------------------
/实验8实验报告TyporaGallery/image-20240603204806942.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验8实验报告TyporaGallery/image-20240603204806942.png
--------------------------------------------------------------------------------
/实验8实验报告TyporaGallery/image-20240603205242460.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验8实验报告TyporaGallery/image-20240603205242460.png
--------------------------------------------------------------------------------
/实验8实验报告TyporaGallery/image-20240603205908054.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验8实验报告TyporaGallery/image-20240603205908054.png
--------------------------------------------------------------------------------
/实验8实验报告TyporaGallery/image-20240603210332497.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验8实验报告TyporaGallery/image-20240603210332497.png
--------------------------------------------------------------------------------
/实验8实验报告TyporaGallery/image-20240603210823094.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验8实验报告TyporaGallery/image-20240603210823094.png
--------------------------------------------------------------------------------
/实验8实验报告TyporaGallery/image-20240603210921087.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验8实验报告TyporaGallery/image-20240603210921087.png
--------------------------------------------------------------------------------
/实验8实验报告TyporaGallery/image-20240603211047149.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验8实验报告TyporaGallery/image-20240603211047149.png
--------------------------------------------------------------------------------
/实验8实验报告TyporaGallery/image-20240603212946280.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验8实验报告TyporaGallery/image-20240603212946280.png
--------------------------------------------------------------------------------
/实验8实验报告TyporaGallery/image-20240603213639781.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验8实验报告TyporaGallery/image-20240603213639781.png
--------------------------------------------------------------------------------
/实验8实验报告TyporaGallery/image-20240603213851804.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验8实验报告TyporaGallery/image-20240603213851804.png
--------------------------------------------------------------------------------
/实验8实验报告TyporaGallery/image-20240603214355759.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验8实验报告TyporaGallery/image-20240603214355759.png
--------------------------------------------------------------------------------
/实验8实验报告TyporaGallery/image-20240603215020809.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验8实验报告TyporaGallery/image-20240603215020809.png
--------------------------------------------------------------------------------
/实验8实验报告TyporaGallery/image-20240603215315363.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验8实验报告TyporaGallery/image-20240603215315363.png
--------------------------------------------------------------------------------
/实验8实验报告TyporaGallery/image-20240603225609603.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验8实验报告TyporaGallery/image-20240603225609603.png
--------------------------------------------------------------------------------
/实验8实验报告TyporaGallery/image-20240603230248919.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验8实验报告TyporaGallery/image-20240603230248919.png
--------------------------------------------------------------------------------
/实验8实验报告TyporaGallery/image-20240604143809060.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验8实验报告TyporaGallery/image-20240604143809060.png
--------------------------------------------------------------------------------
/实验8实验报告TyporaGallery/image-20240604143828351.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验8实验报告TyporaGallery/image-20240604143828351.png
--------------------------------------------------------------------------------
/实验8实验报告TyporaGallery/image-20240604150039123.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验8实验报告TyporaGallery/image-20240604150039123.png
--------------------------------------------------------------------------------
/实验8实验报告TyporaGallery/image-20240604150658608.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验8实验报告TyporaGallery/image-20240604150658608.png
--------------------------------------------------------------------------------
/实验8实验报告TyporaGallery/image-20240604151929158.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验8实验报告TyporaGallery/image-20240604151929158.png
--------------------------------------------------------------------------------
/实验8实验报告TyporaGallery/image-20240604153650017.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验8实验报告TyporaGallery/image-20240604153650017.png
--------------------------------------------------------------------------------
/实验8实验报告TyporaGallery/image-20240604154055715.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验8实验报告TyporaGallery/image-20240604154055715.png
--------------------------------------------------------------------------------
/实验七实验报告TyporaGallery/image-20240601171904609.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验七实验报告TyporaGallery/image-20240601171904609.png
--------------------------------------------------------------------------------
/实验七实验报告TyporaGallery/image-20240601200512566.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验七实验报告TyporaGallery/image-20240601200512566.png
--------------------------------------------------------------------------------
/实验七实验报告TyporaGallery/image-20240602165829427.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验七实验报告TyporaGallery/image-20240602165829427.png
--------------------------------------------------------------------------------
/实验七实验报告TyporaGallery/image-20240602235700022.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验七实验报告TyporaGallery/image-20240602235700022.png
--------------------------------------------------------------------------------
/实验五实验报告TyporaGallery/image-20240509203659770.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验五实验报告TyporaGallery/image-20240509203659770.png
--------------------------------------------------------------------------------
/实验五实验报告TyporaGallery/image-20240509204104728.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验五实验报告TyporaGallery/image-20240509204104728.png
--------------------------------------------------------------------------------
/实验五实验报告TyporaGallery/image-20240509210101230.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验五实验报告TyporaGallery/image-20240509210101230.png
--------------------------------------------------------------------------------
/实验五实验报告TyporaGallery/image-20240511101717552.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验五实验报告TyporaGallery/image-20240511101717552.png
--------------------------------------------------------------------------------
/实验五实验报告TyporaGallery/image-20240511102548416.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验五实验报告TyporaGallery/image-20240511102548416.png
--------------------------------------------------------------------------------
/实验五实验报告TyporaGallery/image-20240511103419892.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验五实验报告TyporaGallery/image-20240511103419892.png
--------------------------------------------------------------------------------
/实验五实验报告TyporaGallery/image-20240511103722579.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验五实验报告TyporaGallery/image-20240511103722579.png
--------------------------------------------------------------------------------
/实验五实验报告TyporaGallery/image-20240511104443656.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验五实验报告TyporaGallery/image-20240511104443656.png
--------------------------------------------------------------------------------
/实验五实验报告TyporaGallery/image-20240511105012487.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验五实验报告TyporaGallery/image-20240511105012487.png
--------------------------------------------------------------------------------
/实验五实验报告TyporaGallery/image-20240511105619310.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验五实验报告TyporaGallery/image-20240511105619310.png
--------------------------------------------------------------------------------
/实验五实验报告TyporaGallery/image-20240511110917937.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验五实验报告TyporaGallery/image-20240511110917937.png
--------------------------------------------------------------------------------
/实验五实验报告TyporaGallery/image-20240511111148837.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验五实验报告TyporaGallery/image-20240511111148837.png
--------------------------------------------------------------------------------
/实验五实验报告TyporaGallery/image-20240511111924580.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验五实验报告TyporaGallery/image-20240511111924580.png
--------------------------------------------------------------------------------
/实验五实验报告TyporaGallery/image-20240511112038324.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验五实验报告TyporaGallery/image-20240511112038324.png
--------------------------------------------------------------------------------
/实验五实验报告TyporaGallery/image-20240511113459404.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验五实验报告TyporaGallery/image-20240511113459404.png
--------------------------------------------------------------------------------
/实验五实验报告TyporaGallery/image-20240511114404019.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验五实验报告TyporaGallery/image-20240511114404019.png
--------------------------------------------------------------------------------
/实验五实验报告TyporaGallery/image-20240511114920687.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验五实验报告TyporaGallery/image-20240511114920687.png
--------------------------------------------------------------------------------
/实验五实验报告TyporaGallery/image-20240511114940468.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验五实验报告TyporaGallery/image-20240511114940468.png
--------------------------------------------------------------------------------
/实验五实验报告TyporaGallery/image-20240511115522863.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验五实验报告TyporaGallery/image-20240511115522863.png
--------------------------------------------------------------------------------
/实验五实验报告TyporaGallery/image-20240511120409621.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验五实验报告TyporaGallery/image-20240511120409621.png
--------------------------------------------------------------------------------
/实验五实验报告TyporaGallery/image-20240511135826875.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验五实验报告TyporaGallery/image-20240511135826875.png
--------------------------------------------------------------------------------
/实验五实验报告TyporaGallery/image-20240511150613053.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验五实验报告TyporaGallery/image-20240511150613053.png
--------------------------------------------------------------------------------
/实验五实验报告TyporaGallery/image-20240511150717362.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验五实验报告TyporaGallery/image-20240511150717362.png
--------------------------------------------------------------------------------
/实验五实验报告TyporaGallery/image-20240511150926038.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验五实验报告TyporaGallery/image-20240511150926038.png
--------------------------------------------------------------------------------
/实验五实验报告TyporaGallery/image-20240511154231264.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验五实验报告TyporaGallery/image-20240511154231264.png
--------------------------------------------------------------------------------
/实验五实验报告TyporaGallery/image-20240512010445918.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验五实验报告TyporaGallery/image-20240512010445918.png
--------------------------------------------------------------------------------
/实验五实验报告TyporaGallery/image-20240512013730506.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验五实验报告TyporaGallery/image-20240512013730506.png
--------------------------------------------------------------------------------
/实验五实验报告TyporaGallery/image-20240512013839361.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验五实验报告TyporaGallery/image-20240512013839361.png
--------------------------------------------------------------------------------
/实验五实验报告TyporaGallery/image-20240512014612713.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验五实验报告TyporaGallery/image-20240512014612713.png
--------------------------------------------------------------------------------
/实验六实验报告TyporaGallery/image-20240513204558707.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验六实验报告TyporaGallery/image-20240513204558707.png
--------------------------------------------------------------------------------
/实验六实验报告TyporaGallery/image-20240513210327468.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验六实验报告TyporaGallery/image-20240513210327468.png
--------------------------------------------------------------------------------
/实验六实验报告TyporaGallery/image-20240514145124769.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验六实验报告TyporaGallery/image-20240514145124769.png
--------------------------------------------------------------------------------
/实验六实验报告TyporaGallery/image-20240514150329687.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验六实验报告TyporaGallery/image-20240514150329687.png
--------------------------------------------------------------------------------
/实验六实验报告TyporaGallery/image-20240514151024191.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验六实验报告TyporaGallery/image-20240514151024191.png
--------------------------------------------------------------------------------
/实验六实验报告TyporaGallery/image-20240514155110805.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验六实验报告TyporaGallery/image-20240514155110805.png
--------------------------------------------------------------------------------
/实验六实验报告TyporaGallery/image-20240514161734542.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验六实验报告TyporaGallery/image-20240514161734542.png
--------------------------------------------------------------------------------
/实验六实验报告TyporaGallery/image-20240514163012037.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验六实验报告TyporaGallery/image-20240514163012037.png
--------------------------------------------------------------------------------
/实验六实验报告TyporaGallery/image-20240514165545163.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验六实验报告TyporaGallery/image-20240514165545163.png
--------------------------------------------------------------------------------
/实验六实验报告TyporaGallery/image-20240514165957341.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验六实验报告TyporaGallery/image-20240514165957341.png
--------------------------------------------------------------------------------
/实验四实验报告TyporaGallery/image-20240414152009354.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验四实验报告TyporaGallery/image-20240414152009354.png
--------------------------------------------------------------------------------
/实验四实验报告TyporaGallery/image-20240414154222504.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验四实验报告TyporaGallery/image-20240414154222504.png
--------------------------------------------------------------------------------
/实验四实验报告TyporaGallery/image-20240414173235814.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验四实验报告TyporaGallery/image-20240414173235814.png
--------------------------------------------------------------------------------
/实验四实验报告TyporaGallery/image-20240415203556350.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验四实验报告TyporaGallery/image-20240415203556350.png
--------------------------------------------------------------------------------
/实验四实验报告TyporaGallery/image-20240415203818866.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验四实验报告TyporaGallery/image-20240415203818866.png
--------------------------------------------------------------------------------
/实验四实验报告TyporaGallery/image-20240415214610657.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验四实验报告TyporaGallery/image-20240415214610657.png
--------------------------------------------------------------------------------
/实验四实验报告TyporaGallery/image-20240415232832072.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验四实验报告TyporaGallery/image-20240415232832072.png
--------------------------------------------------------------------------------
/实验四实验报告TyporaGallery/image-20240416004650958.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验四实验报告TyporaGallery/image-20240416004650958.png
--------------------------------------------------------------------------------
/实验四实验报告TyporaGallery/image-20240416004708664.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoliTa-NET/Sysu_OS_Lab_Homework/HEAD/实验四实验报告TyporaGallery/image-20240416004708664.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 中山大学操作系统实验报告
2 | 此处收集的是本人在中山大学大二期间学习的的操作系统的实验作业,收集了实验二到实验八的实验报告。
3 |
4 | - **版本**:`C++`版操作系统实验
5 | - **实验指导**:https://gitee.com/nelsoncheung/sysu-2021-spring-operating-system/tree/main
6 | - **使用语言**:`C++`与`x86`
7 | - **拥有实验报告数量**:7 / 9
8 |
9 | | 实验序号 | 是否拥有 |
10 | | -------- | -------- |
11 | | 1 | |
12 | | 2 | √ |
13 | | 3 | √ |
14 | | 4 | √ |
15 | | 5 | √ |
16 | | 6 | √ |
17 | | 7 | √ |
18 | | 8 | √ |
19 | | 9 | |
20 |
21 | 本实验报告最后实验得分是全部满分,质量算是有保障吧。
22 |
23 | ## 声明
24 |
25 | 本项目所有代码仅供参考,请使用者尽可能自主完成任务,禁止直接抄袭当作业提交。
26 |
27 | 其中部分代码有不尽人意的地方,请辩证看待。
28 |
29 | 如果该项目有侵权行为,请联系本人删除。
30 |
--------------------------------------------------------------------------------
/07_实验七实验报告.md:
--------------------------------------------------------------------------------
1 | # Assignment 1
2 |
3 | 复现参考代码,实现二级分页机制,并能够在虚拟机地址空间中进行内存管理,包括内存的申请和释放等,截图并给出过程解释。
4 |
5 | ## 1.1 再述二级分页
6 |
7 | 此处主要阐述我对实验报告中二级分页的总结以及一些自己的理解。实验指导内容非常详细,但是少了一些总结性的内容,在此我给出自己的总结。
8 |
9 | 首先,二级分页的寻址如下所示。(过程由CPU自动实现)
10 |
11 | - 传入一个地址,地址包含三个信息:页目录号,页号与业内偏移。
12 | - 通过页目录号在页目录表查找,得到页表地址。页目录表由一个固定的寄存器存储(需要我们手动赋予它这个地址)。
13 | - 通过对应的页表,找到内存中对应的位置,读写数据。
14 |
15 | 由此我们可知:
16 |
17 | - 页目录表必须在操作系统启动时就初始化,并把地址放到某个寄存器中。页表不需要一次性建立,可以需要时建立。
18 | - 在一个空间中,页目录表唯一,页表不唯一。
19 | - 二级分页CPU自带这个功能,需要我们通过硬件操作开启,也就是汇编。
20 |
21 | 此时需要注意,内核和进程都有自己的页目录表和页表。也就是说,**页目录表和页表都是在一个块中起作用的,而不是对全局内存起作用的。**一个块内的物理地址必定是连续的。
22 |
23 | 页的根本目的是为了更高效的内存管理,希望用过表的映射将一个地址映射到一个物理内存上,从而高效地利用内存。
24 |
25 | ### 1.1.1 BitMap和二级分页
26 |
27 | BitMap和二级分页实质上没有很大联系,但是两者作为内存管理的手段很容易混淆之间的关系,有必要阐述清楚各自的功能。
28 |
29 | - BitMap只涉及对内存的操作,二级分页涉及到从内存和硬盘中交换页的过程。
30 | - BitMap是监控内存每一个块是否被使用的数组,本身存贮在内存中。其起作用的时候是在系统需要给一个线程分配内存空间时。
31 | - 二级分页是一种寻址策略,有两个数组:页目录表和页表,也都存贮在内存中。
32 | - BitMap只有在进程需要内存时才会起作用,每一个页都是4KB(本次实验)。二级分页会涉及到虚拟内存,当一个页面需要的地址并不在物理内存上存在时(即二级页表对应的地址是无效的,就是缺页),是由缺页处理程序将二级页表中无效地址替换成有效地址。此时,这种分配空闲地址的行为就需要BitMap支持。
33 |
34 | ### 1.1.2 二级分页后的物理内存排布
35 |
36 | 经过这么多的启动,内存中的东西有点太多了。
37 |
38 | 我们需要清楚内存中内存到底有哪些东西。
39 |
40 | 要搞懂这一点,我们可以观察`boot.inc`和`os_constant.h`的内容。
41 |
42 | 根据最开始的知识,我们知道
43 |
44 | - `0x7c00`:这是装MBR的内存起点。这个内存被装入是CPU自动实现的。
45 | - `0x7e00`:这是BootLoader的内存起点,这是我们自己规定的。
46 | - `0x8880`:这是中断向量表IDT的内存起点,是我们自己规定的。
47 | - `0x20000`:这是我们内核开始的地方,这是我们自己规定的。Bootloader开保护模式之后就进入内核,在这里开始,我们写的C++程序开始发挥作用。
48 |
49 | 然后根据我们这节课的内容,我们得到一些新的内存分布。
50 |
51 | - `0x7c00`:**此处内存在BootLoader运行时被替换成存储我们可用内存的总大小**。因为MBR(BootLoader也是)在启动之后就没有作用了,是我们自己定义的。
52 | - `0x10000`:这是BitMap的位置,是我们自己定义的。
53 | - `0x100000`:这是内核页目录表的位置。根据定义,我们的第一个页表在紧跟着页目录表结尾。
54 |
55 | 保护模式下提供了固定的内存区域,保存数据段、堆栈段、显存段和代码段等等。这些段所占据的内存需要根据其对应的段描述符计算出真正的开头地址。进入保护模式的那一刻,我们所有的操作都在段的保护中,包括代码。只是因为代码段刚好给我们设置成基地址从0开始,才避免了一些麻烦。
56 |
57 | 但这不代表代码就是从物理内存中的0开始的。不要忘了还有段选择子的存在。
58 |
59 | 在`0x0000 0000` 到`0x0010 0000`存放内核。
60 |
61 | 可以看到,内存中现在存储的东西确实十分复杂,需要深刻理解才能在之后的操作中不发生内存冲突的现象。
62 |
63 | ### 1.1.3 段页式内存管理的协作方法
64 |
65 | 我们同时使用段式和页式方法管理内存,自然会有一个疑问:这两个方法是怎么同时运行的?不会搞混吗?
66 |
67 | 有必要给出一个详细的段页式操作过程!
68 |
69 | 假设要访问一个内存地址,以下是详细步骤:
70 |
71 | 1. **段寄存器查找**:
72 | - 例如访问数据段,段选择子在 `DS` 寄存器中。
73 | - 段选择子找到GDT或LDT中的段描述符,假设段基地址为 `0x40000000`。
74 | 2. **段地址计算**:
75 | - 假设要访问的段内偏移为 `0x00003000`。
76 | - 线性地址 = 段基地址 + 段内偏移 = `0x40000000 + 0x00003000 = 0x40003000`。
77 | 3. **分页查找**:
78 | - 假设页目录基地址在CR3寄存器中为 `0x00007000`。
79 | - 线性地址 `0x40003000`分为:
80 | - 页目录索引:`0x40003000` 的高10位
81 | - 页表索引:中间10位
82 | - 页内偏移:低12位
83 | 4. **页表转换**:
84 | - 页目录索引在 `0x00007000` 处找到对应的页目录项(PDE)。
85 | - PDE指向页表基地址,页表索引找到页表项(PTE),PTE提供物理页地址。
86 | - 加上页内偏移得到最终物理地址。
87 |
88 | 为什么要弄得这么复杂?
89 |
90 | - 段的根本目的是为了解决一个程序的地址定位问题。有了段之后,我们就不用担心我们写的程序开头地址不对,也不用担心不同的数据会相互混淆,数据段、代码段、堆栈段等等都分的很明确。
91 | - 页的根本目的是为了更高效的内存管理,希望用过表的映射将一个地址映射到一个物理内存上,从而高效地利用内存。
92 |
93 | 可以看到,段最后会得到一个运算过的**虚拟地址**,然后页会把这个虚拟地址映射到**物理地址**上。
94 |
95 | 也就是,先段,后页。
96 |
97 | 使用页式管理内存时,就可以很轻松规避内存冲突的问题,因为页式管理我们已经实现绕过使用中的内存来获取空内存。
98 |
99 | ## 1.2 内存管理实现
100 |
101 | 本次内存管理时针对物理页的管理。
102 |
103 | 其实,到后面为了实现程序地址不要混乱,我们除了管理物理内存池以外,我们还需要管理虚拟内存池。虚拟内存池的作用我会在后面叙述。
104 |
105 | 现在我们先写一个进程实现内存分配。
106 |
107 | 在`src4`中提供了分配物理页的函数,我们只需要调用这些函数即可。
108 |
109 | 我们在`setup.cpp`中,第一个进程改写成如下代码。
110 |
111 | ```cpp
112 | void first_thread(void *arg)
113 | {
114 | // 第1个线程不可以返回
115 | // stdio.moveCursor(0);
116 | // for (int i = 0; i < 25 * 80; ++i)
117 | // {
118 | // stdio.print(' ');
119 | // }
120 | // stdio.moveCursor(0);
121 |
122 | printf("-----------------------------\n");
123 | char *p1 = (char *)memoryManager.allocatePhysicalPages(AddressPoolType::KERNEL, 1);
124 | char *p2 = (char *)memoryManager.allocatePhysicalPages(AddressPoolType::KERNEL, 10);
125 |
126 | memoryManager.releasePhysicalPages(AddressPoolType::KERNEL, (int)p2, 10);
127 | p2 = (char *)memoryManager.allocatePhysicalPages(AddressPoolType::KERNEL, 100);
128 |
129 |
130 | p2 = (char *)memoryManager.allocatePhysicalPages(AddressPoolType::KERNEL, 10);
131 |
132 | asm_halt();
133 | }
134 | ```
135 |
136 | 同时,我们在定义分配物理页函数的文件`address_pool.cpp`中找到对应的函数,加一些打印函数,以便观察。
137 |
138 | 最后程序结果如下。
139 |
140 | 
141 |
142 | 这个程序很好地展示了功能。
143 |
144 | - 首先,第一次分配,我们知道分配后的内存在`0x0020 0000`处。这是程序找到的第一个没有被用过的物理页的开头地址。此次分配我们设定是只分配一个页。
145 | - 其次,第二次分配,我们观察程序发现内存在`0x0020 1000`处。可知,程序跑出来的结果是一个页的大小是`0x1000Byte`,也就是2的12次方,正好就是`4KB`,符合我们的设计。
146 | - 通过换算,我们知道,这应该是第`0x201`个页。
147 | - 然后释放`p2`。
148 | - 释放完之后我们再分配,发现内存仍在`0x0020 1000`开始,说明我们的释放是真的释放掉了。我们这次分配100个页。
149 | - 然后我们再分配,发现这次变成了`0x 0026 5000`开始,说明中间一共有`0x64`个页被用掉了。`0x64`,刚好就是十进制数的`100`。说明分配成功。
150 |
151 | # Assignment 2
152 |
153 | 参照理论课上的学习的物理内存分配算法如first-fit, best-fit等实现动态分区算法等,或者自行提出自己的算法。
154 |
155 | ## 2.1 算法原理
156 |
157 | 我们这次选择`fitst-fit`算法的变体,也就是,每次申请遇到第二个符合要求的内存空间就放入。
158 |
159 | 虽然`fitst-fit`算法原理简单,但是这是一个速度最快,空间开销最小的算法。
160 |
161 | 如果使用`best-fit`等等算法,要不然每次申请时都遍历一遍空间,有很大的时间开销,要不然需要维护一个剩余空间链表,设计难度不小的同时还可能有很大的空间开销。
162 |
163 | 其实我们在Assignment 1中实现的就是`first-fit`算法。为了不重复,我把第一次符合要求改成了第二次符合要求。我称之为`second-fit`算法。
164 |
165 | 算法只要修改在`BitMap.cpp`中分配算法即可。具体说来,是设一个flag值,初始值为0,第一次适配之后换成1,第二次适配再进行内存分配。
166 |
167 | 实际代码修改如下。为了确认确实是第二次适配,我写了一些打印函数。
168 |
169 | ```cpp
170 | int first_fit = 0;
171 |
172 | if(empty == count && first_fit == 0){
173 | printf("First Find Allocatable size. It is %x\n", start);
174 | first_fit = 1;
175 | ++index;
176 | continue;
177 | }
178 | // 存在连续的count个资源
179 | if (empty == count && first_fit == 1)
180 | {
181 | printf("Second Find Allocatable size. It is %x\n", start);
182 | for (int i = 0; i < count; ++i)
183 | {
184 | set(start + i, true);
185 | }
186 |
187 | return start;
188 | }
189 | ```
190 |
191 | ## 2.2 运行结果
192 |
193 | 最后运行的结果如下。
194 |
195 | 
196 |
197 | 可以看到,最后申请内存到的地方都是第二次适配才进行申请。
198 |
199 | 而且仔细观察,我们可以发现有些页在中间其实是被使用的。这也印证了物理页分配算法的正确性。
200 |
201 | 这个`second-fit`算法只是展现一种自己设计的算法防止和Assignment1重复,其本身优点不多。
202 |
203 | # Assignment 3
204 |
205 | 参照理论课上虚拟内存管理的页面置换算法如FIFO、LRU等,实现页面置换,也可以提出自己的算法。
206 |
207 | ## 3.1 页面置换算法的目的
208 |
209 | 页面置换算法的目标是为了解决**缺页**。
210 |
211 | 缺页的产生是这样的。
212 |
213 | **进程访问虚拟地址**:
214 |
215 | - 进程尝试访问某个虚拟地址。此时,处理器将虚拟地址转换为相应的物理地址。
216 |
217 | **页表查找**:
218 |
219 | - 处理器检查页表,确定该虚拟地址是否已被映射到物理内存。如果页表项有效,则说明该虚拟页在物理内存中,处理器继续访问。
220 |
221 | **缺页检测**:
222 |
223 | - 如果页表项无效(即该虚拟页未被映射到物理内存),处理器会触发缺页异常,并将控制权交给操作系统内核。
224 |
225 | 因此我们要做的事情就很明了了。
226 |
227 | **当我们发现某个地址没有分配物理页的时候,就启动页面置换算法。**
228 |
229 | ## 3.2 算法原理
230 |
231 | 本次实验实现FIFO算法。
232 |
233 | 考虑到我们并没有实现实现交换空间,也没有实现拓展空间中从磁盘读取内容交换到内存上,我们也不能直接操作CACHE。
234 |
235 | 因此我们通过实现好的虚拟页算法先手动划定三个空间,然后手动划定页号,然后根据此来实现FIFO算法。
236 |
237 | 算法的核心,是从分配好的页中找是否有符合我们需求的页。如果没有,我们就需要找到最先进来的项然后替换掉。
238 |
239 | 如何确定哪个项是最先进来的?很简单,我们初始化时所有也页都是空页,因此第一个必缺页。我们设一个整型变量`flag`,从第一个开始,每次缺页就将`flag`加一模三,就能始终将指针指在最开始的页中。
240 |
241 | 我们假设,我们有八个页要置换,这八个页的页号是`7,3,3,7,2,1,9,8`,我们假设0是空页。
242 |
243 | 最后我们实现的代码如下。我将这个程序放在了第一个进程中。
244 |
245 | ```cpp
246 | void first_thread(void *arg)
247 | {
248 | // 第1个线程不可以返回
249 | // stdio.moveCursor(0);
250 | // for (int i = 0; i < 25 * 80; ++i)
251 | // {
252 | // stdio.print(' ');
253 | // }
254 | // stdio.moveCursor(0);
255 |
256 | printf("--------------------------------------\n");
257 | char *p1 = (char *)memoryManager.allocatePages(AddressPoolType::KERNEL, 100);
258 | char *p2 = (char *)memoryManager.allocatePages(AddressPoolType::KERNEL, 10);
259 | char *p3 = (char *)memoryManager.allocatePages(AddressPoolType::KERNEL, 100);
260 |
261 | printf("GOT 3 PAGES: -0x%x- -0x%x- -0x%x-\n", p1, p2, p3);
262 |
263 | int pagelist[8] = {7,3,3,7,2,1,9,8};
264 | int plist[3] = {0, 0, 0};
265 | int first = 0;
266 | int miss = 0;
267 | int hit = 0;
268 | int got = -1;
269 | char *pp;
270 |
271 | for(int i=0; i<8; i++){
272 | printf("------------checking page %d...------------\n", pagelist[i]);
273 | int flag = 0;
274 |
275 | for(int j = 0; j<3; j++){
276 | if(plist[j] == pagelist[i]) flag = 1,got = i;
277 | };
278 |
279 | if(flag == 0){
280 | printf("-PAGE FAULT- ");
281 | plist[first] = pagelist[i];
282 | printf("Change. -0x%x=%d- -0x%x=%d- -0x%x=%d-\n", p1, plist[0], p2, plist[1], p3, plist[2]);
283 | first = (first+1)%3;
284 | miss++;
285 | }
286 | else{
287 | if(got == 0) pp = p1;
288 | if(got == 1) pp = p2;
289 | if(got == 2) pp = p3;
290 | printf("-HIT- the page is 0x%x\n", pp);
291 | hit++;
292 | }
293 | }
294 | printf("----------------------------\n");
295 | printf("FINISH. MISS=%d HIT=%d", miss, hit);
296 | asm_halt();
297 | }
298 | ```
299 |
300 | ## 3.3 实现效果
301 |
302 | 
303 |
304 | 观看虚拟器显示屏,可以看到流程是这样的。
305 |
306 | 1. 初始化三个页,用作FIFO模拟。
307 | 2. PAGE 7检查,缺页,替换到第1个页中。
308 | 3. PAGE 3检查,缺页,替换到第2个页中。
309 | 4. PAGE 3检查,命中,在第2个页中。
310 | 5. PAGE 7检查,命中,在第1个页中。
311 | 6. PAGE 2检查,缺页,替换到第3个页中。
312 | 7. PAGE 1检查,缺页,替换到第1个页中。
313 | 8. PAGE 9检查,缺页,替换到第2个页中。
314 | 9. PAGE 8检查,缺页,替换到第3个页中。
315 | 10. 结束,缺页6次,命中2次。
316 |
317 | 可以看到,整个流程是成功的,符合FIFO的规则。
318 |
319 | # Assignment 4
320 |
321 | 复现“虚拟页内存管理”一节的代码,完成如下要求。
322 |
323 | - 结合代码分析虚拟页内存分配的三步过程和虚拟页内存释放。
324 | - 构造测试例子来分析虚拟页内存管理的实现是否存在bug。如果存在,则尝试修复并再次测试。否则,结合测例简要分析虚拟页内存管理的实现的正确性。
325 |
326 | ## 4.1 再述虚拟页分配
327 |
328 | 我在前面分析了物理页之间的关系,再述虚拟页,就方便许多。
329 |
330 | ### 4.1.1 虚拟页和物理页
331 |
332 | 物理页涉及对内存的直接操作。物理页的目的是为了更高效利用内存。
333 |
334 | 那为什么还需要虚拟页?
335 |
336 | 实验指导中说,虚拟页是因为虚拟地址到物理页之间有一个映射,这意味着我们的防止内存冲突的管理都是只针对物理页而不是针对虚拟地址的。一个程序中有很多对虚拟地址的操作,有可能虚拟地址会发生混乱。为了防止这种情况,所以才增设虚拟页。
337 |
338 | 这么说是可信的,但是我认为虚拟页有一种用途更为重要。**虚拟页可以为每一个进程提供一个从0开始且空间足够大的的虚拟页目录表**。
339 |
340 | - 思考一下,我们的页目录表,在用户空间中可是只有一个的。如果有大量进程来参与页的使用的话,如果我们直接用物理页的话,程序能分配到的页的序号是完全不可控的,这意味着,一个程序没有办法有一个轻松的方式检查自己用了哪些页,在进程退出的时候,我们也没有办法检查程序是否释放掉了所有页才退出。
341 |
342 | - 但如果每个进程都有一个虚拟页表的话就很简单了,只要退出的时候把页目录表和页表中所有使用过的页全部释放掉就可以了。
343 |
344 | - 更近一步,虚拟页表就能将一部分的页映射到一个虚拟地址空间中。当页中没有这个地址的时候,我们就能将这个页从磁盘中读进来,实现内存扩张。虽然想要实现这种功能还要很多硬盘交换知识,但是这是一个正确的方向。
345 |
346 | 我们还没有实现这种扩张虚拟空间的操作。就现在实现的内容说来,虚拟页和物理页是一个简单的一一映射关系。
347 |
348 | 但是我们先需要搞清楚,虚拟页的分配是怎么样的。
349 |
350 | - 首先分配一串连续的虚拟页。
351 | - 对每一个虚拟页,找到一个分配好的物理页,得到一个物理地址(注意,这个物理地址其实也是“虚拟”的,只不过把这个地址直接传入CPU能识别到正确的页)。
352 | - 计算出虚拟地址对应的页目录项,页表项。
353 | - 如果计算出来的页目录项是空的,需要分配一个新的页表,然后让页目录项指向这个新分配的页表。
354 | - 我们把页表项与这个物理地址划等号。
355 |
356 | 到这一步,我们发现,其实寻址过程是从虚拟地址,到虚拟页目录表,再到虚拟页表,虚拟页表存的内容是一个物理页表存的内容,最后成功寻址。
357 |
358 | 这意味着,虚拟页寻址是一套独立的系统,只有到了最后才会把虚拟页映射到一个物理页上。**而在这次实验中,因为只实现了内核中的虚拟页分配,实验指导就是把虚拟页目录表当成了实际页目录表放在cr3中的。**
359 |
360 | 刚开始的时候我以为虚拟页表寻完就到物理页目录表了...让我废了好大劲去理解...
361 |
362 | ## 4.2 虚拟页内存分配过程
363 |
364 | ### 4.2.1 虚拟页的初始化
365 |
366 | 我们也没有实现用户进程,因此不需要考虑每个进程的页表存在哪里。我们只需要在内核中再建一个虚拟页表即可。
367 |
368 | 那程序中,把这个虚拟页表放在哪里了?
369 |
370 | 我们可以看`os_constant.h`的内容。
371 |
372 | ```cpp
373 | #define PAGE_DIRECTORY 0x100000
374 | #define KERNEL_VIRTUAL_START 0xc0100000
375 | ```
376 |
377 | 很明显啊,示例程序中把这个虚拟页表放在了`0xc0100000`的位置中。
378 |
379 | 为什么要放在这里?
380 |
381 | 我们在最开始分配虚拟地址空间的时候,就把3~4GB的空间划给了共享内存区。注意,我们现在在内核中建立虚拟页表,然后我们希望所有进程都能共享内核分配过来的内存。因此我们把页表位置加了`0xc000 0000`。
382 |
383 | 之后初始化的过程也是和先前物理页目录表一样。此时我们还没有建立映射。
384 |
385 | ### 4.2.2 虚拟页的分配
386 |
387 | 实验指导中对这一部分的描述非常详细。我会将指导中的核心总结,帮助大家整理思路时使用。
388 |
389 | #### 4.2.2.1 **从虚拟地址池中分配若干连续的虚拟页**
390 |
391 | 这一步操作,和物理页一模一样。不要忘记,虚拟页表也是一个完整的页表,只是是在物理页表之上又叠加了一层页表而已。
392 |
393 | #### 4.2.2.2 **从物理地址池中分配1页**
394 |
395 | 这里是直接对物理地址操作,我们拿到一个新的物理页之后,就来到了重点:建立虚拟页和物理页的联系。
396 |
397 | #### 4.2.2.3 **虚拟页内的地址经过分页机制变换到物理页**
398 |
399 | 此处的两个关键是`pde`和`pte`。
400 |
401 | - `pde`(Page Directory Entry):页目录项,存在页目录表里。
402 | - `pte`(Page Table Entry):页表项,存在页表里。
403 |
404 | 具体的构造方法已经在实验指导中非常详细地给出了。
405 |
406 | 问题就在于:为什么是这么构造的?打开二级分页机制了所以呢?很有必要再次阐明这一点。
407 |
408 | 打开二级分页机制,从根本上,意味着虚拟地址被拆成三部分。
409 |
410 | - 这是CPU自动拆的,自动先找页目录表,再找页表,最后找地址。页目录表的地址是存在`cr3`中的。
411 |
412 | 但是我们想要为一个虚拟地址分配页。**我们需要直接监测页目录项和页表项的内容之后再决定我们的操作。**
413 |
414 | 这是实验指导中没有提及的。当时我想破了脑袋也不知道为什么寻址还要额外构造页目录项。
415 |
416 | 因此**我们需要直接拿到我们需要的页目录项的虚拟地址,和我们需要的页表项的地址。**
417 |
418 | **这才是为什么我们需要对虚拟地址做变换,而不是因为我们使用的是虚拟地址所以要做变换。**实验指导的讲述具有误导性。
419 |
420 | 虽然实验指导在这方面有些许不足,但是关于程序怎么做讲述的可谓是非常非常详细,观察实验报告就能理解程序的要义了。
421 |
422 | ## 4.3 测试例子
423 |
424 | 我们使用如下的测试例子,来判断释放是否正确,以及分配是否正确。
425 |
426 | 首先我们改写测试样例。
427 |
428 | ```cpp
429 | void first_thread(void *arg)
430 | {
431 | // 第1个线程不可以返回
432 | // stdio.moveCursor(0);
433 | // for (int i = 0; i < 25 * 80; ++i)
434 | // {
435 | // stdio.print(' ');
436 | // }
437 | // stdio.moveCursor(0);
438 |
439 | printf("--------------------------------------");
440 | char *p1 = (char *)memoryManager.allocatePages(AddressPoolType::KERNEL, 100);
441 | char *p2 = (char *)memoryManager.allocatePages(AddressPoolType::KERNEL, 10);
442 | char *p3 = (char *)memoryManager.allocatePages(AddressPoolType::KERNEL, 100);
443 |
444 | memoryManager.releasePages(AddressPoolType::KERNEL, (int)p2, 10);
445 | p2 = (char *)memoryManager.allocatePages(AddressPoolType::KERNEL, 100);
446 |
447 | p2 = (char *)memoryManager.allocatePages(AddressPoolType::KERNEL, 10);
448 |
449 |
450 | asm_halt();
451 | }
452 | ```
453 |
454 | 然后我们在`managecpp`中加入一些打印函数,观察是否正确。
455 |
456 | ```cpp
457 | int MemoryManager::allocatePages(enum AddressPoolType type, const int count)
458 | {
459 | // 第一步:从虚拟地址池中分配若干虚拟页
460 | int virtualAddress = allocateVirtualPages(type, count);
461 | if (!virtualAddress)
462 | {
463 | return 0;
464 | }
465 |
466 | bool flag;
467 | int physicalPageAddress;
468 | int vaddress = virtualAddress;
469 |
470 | // 依次为每一个虚拟页指定物理页
471 | for (int i = 0; i < count; ++i, vaddress += PAGE_SIZE)
472 | {
473 | flag = false;
474 | // 第二步:从物理地址池中分配一个物理页
475 | physicalPageAddress = allocatePhysicalPages(type, 1);
476 | if (physicalPageAddress)
477 | {
478 | //printf("allocate physical page 0x%x\n", physicalPageAddress);
479 |
480 | // 第三步:为虚拟页建立页目录项和页表项,使虚拟页内的地址经过分页机制变换到物理页内。
481 | flag = connectPhysicalVirtualPage(vaddress, physicalPageAddress);
482 | }
483 | else
484 | {
485 | flag = false;
486 | }
487 |
488 | // 分配失败,释放前面已经分配的虚拟页和物理页表
489 | if (!flag)
490 | {
491 | // 前i个页表已经指定了物理页
492 | releasePages(type, virtualAddress, i);
493 | // 剩余的页表未指定物理页
494 | releaseVirtualPages(type, virtualAddress + i * PAGE_SIZE, count - i);
495 | return 0;
496 | }
497 | }
498 | printf("New Virtual Page ALLOCATE SUCCESS. Virtual:0x%x Physical:0x%x\n", virtualAddress, physicalPageAddress);
499 | return virtualAddress;
500 | }
501 | ```
502 |
503 | ```cpp
504 | void MemoryManager::releasePages(enum AddressPoolType type, const int virtualAddress, const int count)
505 | {
506 | int vaddr = virtualAddress;
507 | int *pte;
508 | for (int i = 0; i < count; ++i, vaddr += PAGE_SIZE)
509 | {
510 | // 第一步,对每一个虚拟页,释放为其分配的物理页
511 | releasePhysicalPages(type, vaddr2paddr(vaddr), 1);
512 |
513 | // 设置页表项为不存在,防止释放后被再次使用
514 | pte = (int *)toPTE(vaddr);
515 | *pte = 0;
516 | }
517 |
518 | // 第二步,释放虚拟页
519 | releaseVirtualPages(type, virtualAddress, count);
520 | printf("-Virtual:0x%x- REALISE.\n", virtualAddress);
521 | }
522 | ```
523 |
524 | 最后运行的结果如下。
525 |
526 | 
527 |
528 | 可以看到,分配过程是正确的。为什么呢?
529 |
530 | - 因为p2释放掉之后又申请了一个100的空间,但是前面q3占掉了后面的空间。因此p2释放完之后原地只剩了10的空间。再次分配时一定是在p3之后。
531 | - 而在最后,我们申请了10的空间,刚好释放掉的空间符合。因此,理想状况下最后一个申请到的页地址应该是前面释放掉的地址。观看程序,这是正确的!说明程序真的正确执行了。
532 |
--------------------------------------------------------------------------------
/02_实验2实验报告.md:
--------------------------------------------------------------------------------
1 | # Assignment1 MBR
2 |
3 | ## 1.1 Example1复现
4 |
5 | Example1中详细讲述了如何利用开机时自启动进入的Master Boost Record编写一个简单的Hello World程序。本次复现的实验报告中会展示我编写的MBR代码,以及补充实验报告未明确说明的注意事项。
6 |
7 | ### 1.1.1 MBR代码编写
8 |
9 | 在该次实验中,需要注意如下的细节:
10 |
11 | - BIOS自检完成之后会将控制权自动交给MBR运行,MBR的地址是`0x7c00`,但是不同的编译器设定的默认代码段起始位置很可能是不一样的,且很可能不符合我们的期望。因此在程序开头我们需要使用伪指令`org`使得编译器以MBR起始地址为代码段地址来编译代码。
12 | - 本次实验只要求实现MBR中编写程序,进一步的bootloader不做要求,所以最后使用`times`伪指令做无限循环,让程序停在MBR中。
13 | - 计算机会检查MBR是否具有有效内容,判断方法是检查MBR最后两个字节是否是`0x55`和`0xaa`,符合才会开始运行程序。因此MBR中实际可用空间只有510个字节,且编写完程序之后必须保证程序大小为514个字节,可以用`times`伪指令来填充实际代码中剩余空白部分,最后再填充两个校验字节。
14 |
15 | - 在屏幕上显示字符时,显示矩阵的映射范围是`0xB8000~0xBFFFF`处,这一段地址称为显存地址。因此我们如果要显示地址,只用在程序中把对应的内存地址做修改就能显示文字了。
16 |
17 | 最后编写的MBR代码如下。
18 |
19 | ```assembly
20 | org 0x7c00
21 | [bits 16]
22 |
23 | xor ax,ax
24 | mov ds,ax
25 | mov ss,ax
26 | mov es,ax
27 | mov fs,ax
28 | mov gs,ax
29 |
30 | mov sp,0x7c00
31 | mov ax,0xb800
32 | mov gs,ax
33 |
34 | mov ah,0x01
35 | mov al,'H'
36 | mov [gs:2*0],ax
37 |
38 | mov al,'e'
39 | mov [gs:2*1],ax
40 |
41 | mov al,'l'
42 | mov [gs:2*2],ax
43 |
44 | mov al,'l'
45 | mov [gs:2*3],ax
46 |
47 | mov al,'o'
48 | mov [gs:2*4],ax
49 |
50 | mov al,' '
51 | mov [gs:2*5],ax
52 |
53 | mov al,'W'
54 | mov [gs:2*6],ax
55 |
56 | mov al,'o'
57 | mov [gs:2*7],ax
58 |
59 | mov al,'r'
60 | mov [gs:2*8],ax
61 |
62 | mov al,'l'
63 | mov [gs:2*9],ax
64 |
65 | mov al,'d'
66 | mov [gs:2*10],ax
67 |
68 | jmp $
69 |
70 | times 510-($-$$) db 0
71 | db 0x55,0xaa
72 | ```
73 |
74 | ### 1.1.2 装载MBR
75 |
76 | 首先我们当然要把写好的文件转化为二进制可执行文件。
77 |
78 | ```bash
79 | nasm -f bin mbr.asm -o mbr.bin
80 | ```
81 |
82 | 我们在第一次实验已经装配好了相应的内核。现在我们为虚拟机开一个虚拟硬盘,`qemu`给我们提供了相应的指令。我们开一个名字为`hd`,大小为10Mbit的硬盘。
83 |
84 | ```bash
85 | qemu-img create hd.img 10m
86 | ```
87 |
88 | 最后把二进制可执行文件写入这个硬盘的首512个字节中。
89 |
90 | ```bash
91 | dd if=mbr.bin of=hd.img bs=512 count=1 seek=0 conv=notrunc
92 | ```
93 |
94 | 最后启动虚拟机。
95 |
96 | ```bash
97 | qemu-system-i386 -hda hd.img -serial null -parallel stdio
98 | ```
99 |
100 | 虚拟机运行界面如下。
101 |
102 | 
103 |
104 | 可以看到有如下情况:
105 |
106 | - 成功显示出了蓝色的`Hello World`
107 | - 虚拟机一只卡在`Booting from Hard Disk`中。这也可以理解,因为我们的`mdr`中没有写进一步的操作。
108 |
109 | ## 1.2 Example1修改
110 |
111 | 题目要求我们在`(12,12)`处开始输出学号,且要求景色与背景色不同。
112 |
113 | **只要修改`ah`改变背景颜色和字体颜色,`gs`改变显示位置即可。**
114 |
115 | 屏幕的显示矩阵大小是`25x80`。因此位置计算公式如下。
116 | $$
117 | Address=\texttt{0xB8000}+2\times(80x+y)
118 | $$
119 | 我们把背景颜色改为绿色,字体改成黄色。即`al=0010 1110`即可。
120 |
121 | 最后编写的代码如下。
122 |
123 | ```assembly
124 | org 0x7c00
125 | [bits 16]
126 |
127 | xor ax,ax
128 | mov ds,ax
129 | mov ss,ax
130 | mov es,ax
131 | mov fs,ax
132 | mov gs,ax
133 |
134 | mov sp,0x7c00
135 | mov ax,0xb800
136 | mov gs,ax
137 |
138 | mov ah,00101110b
139 | mov al,'2'
140 | mov [gs:2*80*12+2*12],ax
141 |
142 | mov al,'2'
143 | mov [gs:2*80*12+2*13],ax
144 |
145 | mov al,'3'
146 | mov [gs:2*80*12+2*14],ax
147 |
148 | mov al,'0'
149 | mov [gs:2*80*12+2*15],ax
150 |
151 | mov al,'5'
152 | mov [gs:2*80*12+2*16],ax
153 |
154 | mov al,'0'
155 | mov [gs:2*80*12+2*17],ax
156 |
157 | mov al,'5'
158 | mov [gs:2*80*12+2*18],ax
159 |
160 | mov al,'3'
161 | mov [gs:2*80*12+2*19],ax
162 |
163 | jmp $
164 |
165 | times 510-($-$$) db 0
166 | db 0x55,0xaa
167 | ```
168 |
169 | 最后程序运行结果如下。
170 |
171 | 
172 |
173 | (不得不提代码最后写死循环不是一个好写法,进程被一直占用导致电脑很卡)
174 |
175 | # Assignment2 实模式中断
176 |
177 | ## 2.1 光标中断实现位置获取与移动
178 |
179 | 系统已经提供了相应的功能号让我们使用中断例程来实现程序。
180 |
181 | | 功能 | 功能号 | 参数 | 返回值 |
182 | | ---------------- | ------ | ------------------------------------------ | -------------------------------------------------------- |
183 | | 设置光标位置 | 02H | BH=页码,DH=行,DL=列 | 无 |
184 | | 获取光标位置形状 | 03H | BX=页码 | AX=0,CH=行扫描开始,CL=行扫描结束,DH=行位置,DL=列位置 |
185 | | 在当前位置写字符 | 09H | AL=字符,BH=页码,BL=颜色,CX=输出字符个数 | 无 |
186 |
187 | 为了方便调试,先给出几个常用指令:
188 |
189 | - 查看寄存器:`i r`
190 | - 单步执行:`si`
191 | - 设置断点:`b`
192 | - 继续连续实行函数:`c`
193 | - 显示下一步指令:`set disassemble-next-line on`
194 | - 以intel风格显示汇编指令:`set disassembly-flavor intel`
195 |
196 | 根据上述条件,我期望写一个程序,第一步获取光标位置,第二步移动光标位置,最终使用远程调试一步一步进行看看是否达成目标。
197 |
198 | ### 2.1.1 MBR代码
199 |
200 | ```assembly
201 | org 0x7c00
202 | [bits 16]
203 |
204 | xor ax,ax
205 | mov ds,ax
206 | mov ss,ax
207 | mov es,ax
208 | mov fs,ax
209 | mov gs,ax
210 |
211 | mov sp,0x7c00
212 | mov ax,0xb800
213 | mov gs,ax
214 |
215 | mov ah,03h
216 | int 10h
217 |
218 | mov ah,02h
219 | add dl,1
220 | int 10h
221 |
222 | mov ah,03h
223 | int 10h
224 |
225 | jmp $
226 |
227 | times 510-($-$$) db 0
228 | db 0x55,0xaa
229 | ```
230 |
231 | ### 2.2.2 逐步执行观察效果
232 |
233 | 编译这个文件。
234 |
235 | ```bash
236 | nasm -f bin mbr_2_1.asm -o mbr_2_1.bin -g
237 | ```
238 |
239 | 写入文件。
240 |
241 | ```bash
242 | dd if=mbr_2_1.bin of=hd.img bs=512 count=1 seek=0 conv=notrunc
243 | ```
244 |
245 | 以debug模式启动虚拟机。
246 |
247 | ```bash
248 | qemu-system-i386 -hda hd.img -serial null -parallel stdio -s -S
249 | ```
250 |
251 | 我使用gdb进行远程调试。连接成功之后,我们设置断点。
252 |
253 | ```bash
254 | b *0x7c00
255 | ```
256 |
257 | 然后用c命令执行到相应位置,并设置好语言格式,如下图所示。
258 |
259 | 
260 |
261 | 此时我们可以看到红圈处,这是此时光标的位置(正在不断闪烁)。我们来看看这个位置是多少。
262 |
263 | 首先我们一步步执行到这一部分。
264 |
265 | ```assembly
266 | mov ah,03h
267 | int 10h
268 | ```
269 |
270 | 然后通过打断点并继续执行的办法跳过`int 10h`函数,最后使用`i r`查看寄存器内容。gdb界面如下。
271 |
272 | 
273 |
274 | 可以看到,此时`DX`寄存器的值是`0x2048`,二进制是`0010 0000 0100 1000`
275 |
276 | **这说明这个光标的位置是在32行72列处**。看得出来这个位置和我们看上去的位置并不是一一对应的关系。
277 |
278 | 然后我们把光标右移,也就是把`DL`加1。
279 |
280 | 将如下代码运行。
281 |
282 | ```assembly
283 | mov ah,02h
284 | add dl,1
285 | int 10h
286 | ```
287 |
288 | 最后截图如下。
289 |
290 | 
291 |
292 | 可以看到,光标真的向右移了一位。我们的程序正确运行了。
293 |
294 | ## 2.2 光标中断输出代码
295 |
296 | 我们可以修改`1.2`的程序了。
297 |
298 | ### 2.2.1 实现思路
299 |
300 | 只要我们先获取光标的位置,然后把`dl`和`dh`都加12,然后再在当前位置写一次,再在移动,不断以此下去写八次即可。
301 |
302 | ### 2.2.2 实现代码
303 |
304 | ```assembly
305 | org 0x7c00
306 | [bits 16]
307 |
308 | xor ax,ax
309 | mov ds,ax
310 | mov ss,ax
311 | mov es,ax
312 | mov fs,ax
313 | mov gs,ax
314 |
315 | mov sp,0x7c00
316 | mov ax,0xb800
317 | mov gs,ax
318 |
319 | mov ah,03h
320 | int 10h
321 |
322 | mov ah,02h
323 | add dl,12
324 | add dh,12
325 | int 10h
326 |
327 | mov al,'2'
328 | mov bl,00101110b
329 | mov cx,2
330 | mov ah,9h
331 | int 10h
332 | add dl,2
333 | mov ah,2h
334 | int 10h
335 |
336 | mov al,'3'
337 | mov bl,01001110b
338 | mov cx,1
339 | mov ah,9h
340 | int 10h
341 | add dl,1
342 | mov ah,2h
343 | int 10h
344 |
345 | mov al,'0'
346 | mov bl,10001010b
347 | mov cx,1
348 | mov ah,9h
349 | int 10h
350 | add dl,1
351 | mov ah,2h
352 | int 10h
353 |
354 | mov al,'5'
355 | mov bl,01101010b
356 | mov cx,1
357 | mov ah,9h
358 | int 10h
359 | add dl,1
360 | mov ah,2h
361 | int 10h
362 |
363 | mov al,'0'
364 | mov bl,00001100b
365 | mov cx,1
366 | mov ah,9h
367 | int 10h
368 | add dl,1
369 | mov ah,2h
370 | int 10h
371 |
372 | mov al,'5'
373 | mov bl,01100110b
374 | mov cx,1
375 | mov ah,9h
376 | int 10h
377 | add dl,1
378 | mov ah,2h
379 | int 10h
380 |
381 | mov al,'3'
382 | mov bl,11100110b
383 | mov cx,1
384 | mov ah,9h
385 | int 10h
386 | add dl,1
387 | mov ah,2h
388 | int 10h
389 |
390 | jmp $
391 |
392 | times 510-($-$$) db 0
393 | db 0x55,0xaa
394 | ```
395 |
396 | 我们这次就不进调试模式了,直接虚拟机,启动!
397 |
398 | 
399 |
400 | 可以看到程序执行情况还是很良好的。
401 |
402 | ## 2.3 从键盘输入并回显
403 |
404 | 通过查询相关资料可知,键盘I/O中断有三个功能号012,中断码是`16h`。
405 |
406 | - 0号实现从键盘读入字符并送往`AL`。
407 | - 1号查询键盘缓冲区,对键盘扫描但不等待,如果有按键操作就置`ZF`为0,否则为1.
408 | - 2号用来检测键盘各个功能键的状态,并把状态送给`AL`。
409 |
410 | 显然我们只用0号就足够了。
411 |
412 | ### 2.3.1 实现思路
413 |
414 | 我们写一个循环。先从键盘读入,再显示到屏幕上,再移动一个光标,再循环回去,即可实现目的。
415 |
416 | ### 2.3.2 实现代码
417 |
418 | ```assembly
419 | org 0x7c00
420 | [bits 16]
421 |
422 | xor ax,ax
423 | mov ds,ax
424 | mov ss,ax
425 | mov es,ax
426 | mov fs,ax
427 | mov gs,ax
428 |
429 | mov sp,0x7c00
430 | mov ax,0xb800
431 | mov gs,ax
432 |
433 | mov ah,03h
434 | int 10h
435 |
436 | proc mov ah,0
437 | int 16h
438 |
439 | mov bl,00001100b
440 | mov cx,1
441 | mov ah,9h
442 | int 10h
443 | mov ah,02h
444 | add dl,1
445 | int 10h
446 | jmp proc
447 |
448 |
449 | jmp $
450 |
451 | times 510-($-$$) db 0
452 | db 0x55,0xaa
453 | ```
454 |
455 | 最后在虚拟机上跑一跑,截图如下
456 |
457 | 
458 |
459 | 我们敲入几个字看看
460 |
461 | 
462 |
463 | 可以看到程序很好地执行了功能。不过回车和回退都没有办法检测到,略显可惜。
464 |
465 | # Assignment3 汇编
466 |
467 | 在此之前,为了在后续编写汇编程序时方便,我先找出了几个常用的指令的用法。
468 |
469 | | 指令 | 用法 |
470 | | ---- | ------------------------------------------------------------ |
471 | | loop | 所有循环指令都是**短转移**。`CX`存储循环次数,每到loop一次减一,检测到变零跳出循环。 |
472 | | jmp | 直接跳转到对应位置 |
473 | | add | 加 |
474 | | sub | 减 |
475 | | mul | 乘 |
476 | | div | 用法是`div s`,运算方法是`eax/s`,商存入`eax`,余数存入`edx`。无符号除法,`idiv`是有符号除法。 |
477 | | neg | 取负 |
478 | | inc | 自增 |
479 | | dec | 自减 |
480 | | and | 与 |
481 | | or | 或 |
482 | | not | 非 |
483 | | xor | 异或 |
484 | | shl | 左移 |
485 | | shr | 右移 |
486 | | mov | 数据转移 |
487 |
488 | `cmp`判断指令有些复杂,在下列表解释。
489 |
490 | | CMP无符号数比较结果 | ZF | CF |
491 | | ------------------- | ---- | ---- |
492 | | < | 0 | 1 |
493 | | > | 0 | 0 |
494 | | = | 1 | 0 |
495 |
496 | | CMP有符号数比较结果 | 响应 |
497 | | ------------------- | ---------- |
498 | | < | `SF != OF` |
499 | | > | `SF == OF` |
500 | | = | `ZF == 1` |
501 |
502 | 各种跳转指令的用法如下。
503 |
504 | | 跳转指令 | 跳转条件 |
505 | | -------- | --------------- |
506 | | jz | zf=1 |
507 | | jnz | zf=0 |
508 | | jc | cf=1 |
509 | | jnc | cf=0 |
510 | | jo | of=1 |
511 | | jno | of=0 |
512 | | js | sf=1 |
513 | | jns | sf=0 |
514 | | jp | pf=1 |
515 | | jnp | pf=0 |
516 | | je | 两数相等 |
517 | | jne | 两数不等 |
518 | | jcxz | cx=0 |
519 | | jecxz | ecx=0 |
520 | | ja/jnbe | 无符号数 多于 |
521 | | jae/jnb | 无符号数 不少于 |
522 | | jb/jnae | 无符号数 少于 |
523 | | jbe/jna | 无符号数 不多于 |
524 |
525 | ## 3.1 分支逻辑
526 |
527 | ```assembly
528 | your_if:
529 | ; put your implementation here
530 | mov eax,[a1]
531 | cmp eax,12
532 | jb pro1
533 | cmp eax,24
534 | jb pro2
535 | jmp pro3
536 | pro1 shr eax,1
537 | inc eax
538 | mov edx,[if_flag]
539 | mov edx,eax
540 | jmp exit1
541 | pro2 mov edx,24
542 | sub edx,eax
543 | mov eax,edx
544 | mov ebx,[a1]
545 | mul ebx
546 | mov [if_flag],edx
547 | jmp exit1
548 | pro3 mov edx,eax
549 | shl edx,4
550 | exit1 mov [if_flag],edx
551 | ```
552 |
553 | ## 3.2 存入字符
554 |
555 | ```assembly
556 | your_while:
557 | ; put your implementation here
558 | mov ecx,[a2]
559 | pro4 cmp ecx,12
560 | jb exit2
561 | call my_random
562 | mov edi,ecx
563 | sub edi,12
564 | add edi,[while_flag]
565 | mov [edi],eax
566 | sub ecx,1
567 | jmp pro4
568 | exit2 mov [a2],ecx
569 | ```
570 |
571 | ## 3.3 函数调用实现
572 |
573 | ```assembly
574 | your_function:
575 | ; put your implementation here
576 |
577 | push ecx
578 | push edi
579 | push edx
580 | mov ecx,0
581 | mov edi,[your_string]
582 | pro5
583 | cmp byte [edi],'\0'
584 | jz exit3
585 | pushad
586 | mov eax,[edi]
587 | push eax
588 | call print_a_char
589 | pop eax
590 | popad
591 | add edi,1
592 | jmp pro5
593 | exit3 pop edx
594 | pop edi
595 | pop ecx
596 | ret
597 | ```
598 |
599 | ## 3.4 实现截图
600 |
601 | 
602 |
603 | 可以看到,即使我不知道函数中部分值的内容,因为函数写得正确,程序正确地执行了。
604 |
605 | # Assignment4 弹射程序
606 |
607 | ******
608 |
609 | **警告:该报告中的思路与代码有瑕疵,请谨慎使用。**
610 |
611 | *******
612 |
613 | ## 4.1 算法思路
614 |
615 | 因为有反射弹回的设计,因此我们需要两个变量,用来记录此时字符的位置。当字符的位置超过屏幕边界时就将变量从加变减。从而实现字符位置的”弹射“。
616 |
617 | 同时在算法实现的过程中,我发现在载入字符时遇到了一些麻烦,我通过将边界改为屏幕边长的两倍,每次增减2才能得到正确的答案。为什么会出现这个现象?仍然需要进一步的研究。
618 |
619 | 不过本次实验使用上述方法能够正确运行。
620 |
621 | 同时,nasm编译器的语法规则与DOSBox的编译器的语法规则稍有不同,有些指令需要查阅nasm标准文档来排查语法错误。
622 |
623 | 比如,`mov WORD PTR [num],1`不被允许,因为nasm编译器中不支持`PTR`语法,需要更改成`mov WORD [num],1`
624 |
625 | 再比如,`[gs:ax]`的寻址模式不被允许,偏移地址必须接受32位寄存器,需要更改成`[gs:eax]`
626 |
627 | 还有非常多的变量不允许直接操作的问题。
628 |
629 | 种种语法不匹配的问题以及屏幕输出的问题是本次实验中最困难的部分,让我折腾了相当长的一段时间。
630 |
631 | ## 4.2 代码展示
632 |
633 | 源代码如下。
634 |
635 | ```assembly
636 | org 0x7c00
637 | [bits 16]
638 |
639 | section .data
640 | num db 0
641 | string db 48
642 | color dw 00000010b
643 | xAddr dw 2
644 | yAddr dw 0
645 | xMax dw 80*2-1
646 | yMax dw 25*2-2
647 | xflag db 1
648 | yflag db 1
649 | delay_time dw 0xFFFF
650 |
651 | _start:
652 | xor ax,ax
653 | mov ds,ax
654 | mov ss,ax
655 | mov es,ax
656 | mov fs,ax
657 | mov gs,ax
658 |
659 | mov sp,0x7c00
660 | mov ax,0xb800
661 | mov gs,ax
662 |
663 | ;load number
664 | proc:
665 | ;mod fuction
666 | add byte [num],1
667 | mov al,[num]
668 | cmp al,10
669 | jne next1
670 | mov al,0
671 | mov byte [num],al
672 | next1: add word [color], 00100010b
673 |
674 | ;change ASCII
675 | add al,'0'
676 | mov byte [string],al
677 |
678 | ;count address
679 | jmp count_address
680 | proc1:
681 | ;GS address
682 | mov eax,0
683 | mov ebx,0
684 | mov ax, [yAddr]
685 | mov bx,80
686 | mul bx
687 | add ax, [xAddr]
688 | mov bx,ax
689 |
690 | ;All in!
691 | mov eax,0
692 | mov ax,bx
693 | mov dh,[color]
694 | mov dl,[string]
695 | mov WORD [gs:eax], dx
696 |
697 | ;delay time
698 | mov cx,delay_time
699 | delay_loop:
700 | nop
701 | mov bx,delay_time
702 | loop2:
703 | dec bx
704 | cmp bx,0
705 | nop
706 | jne loop2
707 | nop
708 | loop delay_loop
709 | jmp proc
710 |
711 |
712 | count_address:
713 | cmp byte [xflag], 1
714 | je add_value_x
715 | jmp sub_value_x
716 | add_value_x:
717 | inc word [xAddr]
718 | inc word [xAddr]
719 | mov ax,[xAddr]
720 | cmp ax, [xMax]
721 | jle y_count
722 | mov byte [xflag],0
723 | jmp y_count
724 | sub_value_x:
725 | dec word [xAddr]
726 | dec word [xAddr]
727 | cmp word [xAddr],2
728 | jge y_count
729 | mov byte [xflag],1
730 | jmp y_count
731 | y_count:
732 | cmp byte [yflag], 1
733 | je add_value_y
734 | jmp sub_value_y
735 | add_value_y:
736 | inc word [yAddr]
737 | inc word [yAddr]
738 | mov ax,[yAddr]
739 | cmp ax, [yMax]
740 | jle proc1
741 | mov byte [yflag],0
742 | jmp proc1
743 | sub_value_y:
744 | dec word [yAddr]
745 | dec word [yAddr]
746 | cmp word [yAddr],0
747 | jge proc1
748 | mov byte [yflag],1
749 | jmp proc1
750 |
751 | times 510-($-$$) db 0
752 | db 0x55,0xaa
753 | ```
754 |
755 | ## 4.3 运行
756 |
757 | 编译这个文件。
758 |
759 | ```bash
760 | nasm -f bin mbr_2_4.asm -o mbr_2_4.bin -g
761 | ```
762 |
763 | 写入文件。
764 |
765 | ```bash
766 | dd if=mbr_2_4.bin of=hd.img bs=512 count=1 seek=0 conv=notrunc
767 | ```
768 |
769 | 以debug模式启动虚拟机。
770 |
771 | ```bash
772 | qemu-system-i386 -hda hd.img -serial null -parallel stdio
773 | ```
774 |
775 | 运行的结果如下:
776 |
777 | 
778 |
779 | 
780 |
--------------------------------------------------------------------------------
/06_实验六实验报告.md:
--------------------------------------------------------------------------------
1 | # Assignment 1
2 |
3 | ## 1.1 代码复现
4 |
5 | 我会基于“消失的芝士汉堡”这个问题阐述锁的思路。
6 |
7 | 芝士汉堡问题只有两个进程,因此信号量问题其实会退化成自旋锁问题。
8 |
9 | ### 1.1.1 自旋锁
10 |
11 | 自旋锁的根本目的是为了**让一个进程不要因为中断,让另一个进程改变了这个进程需要用的值。**
12 |
13 | 因此一个自旋锁的用法应该如下。
14 |
15 | ```cpp
16 | void thread(void*){
17 | //可以被打断的部分
18 | locK();
19 | //不可被打断的部分
20 | unlock();
21 | }
22 | ```
23 |
24 | 我们这次不用关中断的方法来实现,而是用**忙等待**来实现自旋锁。
25 |
26 | 因此很自然我们需要一个全局变量,这个变量被改为1之后其他想进入临界区进程不得运行。
27 |
28 | 我们还需要一个局部变量,这个局部变量为0的进程允许进入临界区。
29 |
30 | **这里就要求必须保证这些局部变量中只有一个局部变量为0,否则就会出现冲突。**
31 |
32 | 思路是较为简单的。
33 |
34 | ```cpp
35 | void SpinLock::lock()
36 | {
37 | uint32 key = 1;
38 |
39 | do
40 | {
41 | asm_atomic_exchange(&key, &bolt);
42 | //printf("pid: %d\n", programManager.running->pid);
43 | } while (key);
44 | }
45 | ```
46 |
47 | ```asm
48 | asm_atomic_exchange:
49 | push ebp
50 | mov ebp, esp
51 | pushad
52 |
53 | mov ebx, [ebp + 4 * 2] ; register
54 | mov eax, [ebx] ;
55 | mov ebx, [ebp + 4 * 3] ; memory
56 | xchg [ebx], eax ;
57 | mov ebx, [ebp + 4 * 2] ; memory
58 | mov [ebx], eax ;
59 |
60 | popad
61 | pop ebp
62 | ret
63 | ```
64 |
65 | 解除自旋锁一句代码就能解决。
66 |
67 | ```cpp
68 | void SpinLock::unlock()
69 | {
70 | bolt = 0;
71 | }
72 | ```
73 |
74 | **但是上述的自旋锁是一个有条件的原子操作。**在实际使用中,这种操作是原子的,在实验指导中指明了这种绝对性。
75 |
76 | 在1.2中我将这种自旋锁改正成了真正的原子操作,且也不是靠`LOCK`实现的,时间上不会有损耗。
77 |
78 | 我们键入这两个进程。
79 |
80 | ```cpp
81 | void a_mother(void *arg)
82 | {
83 | aLock.lock2();
84 | int delay = 0;
85 |
86 | printf("mother: start to make cheese burger, there are %d cheese burger now\n", cheese_burger);
87 | // make 10 cheese_burger
88 | cheese_burger += 10;
89 |
90 | printf("mother: oh, I have to hang clothes out.\n");
91 | // hanging clothes out
92 | delay = 0xfffffff;
93 | while (delay)
94 | --delay;
95 | // done
96 |
97 | printf("mother: Oh, Jesus! There are %d cheese burgers\n", cheese_burger);
98 | aLock.unlock();
99 | }
100 |
101 | void a_naughty_boy(void *arg)
102 | {
103 | aLock.lock2();
104 | printf("boy : Look what I found!\n");
105 | // eat all cheese_burgers out secretly
106 | cheese_burger -= 10;
107 | // run away as fast as possible
108 | aLock.unlock();
109 | }
110 | ```
111 |
112 | 如果没有自旋锁,最终结果会是`a_naughty_boy`进程会先执行完,最后打印出来的汉堡数量会是0。
113 |
114 | 但是加入自旋锁之后结果变成如下。
115 |
116 | 
117 |
118 | 自旋锁起了作用。
119 |
120 | ### 1.1.2 信号量
121 |
122 | 我们有了自旋锁之后,就能保证很多操作都可以是原子操作了。
123 |
124 | 这样我们就能实现更为复杂的信号量。
125 |
126 | 信号量相比自旋锁最有优势的地方在于其能够分配多个资源。
127 |
128 | - 没有资源时进程可以压进`BLOCK`阻塞队列中。
129 | - 虽然只有一个CPU,但是`RUN`可以和`READY`不断换进换出,实现时间片流转算法。
130 | - 有资源时,就可以把阻塞进程压进`READY`队列中参与时间片轮转算法。
131 |
132 | 因此,P操作和V操作不是单纯的锁和放的过程,我们还要对五个状态队列做维护。
133 |
134 | ```cpp
135 | void Semaphore::P()
136 | {
137 | PCB *cur = nullptr;
138 |
139 | while (true)
140 | {
141 | semLock.lock();
142 | if (counter > 0)
143 | {
144 | --counter;
145 | semLock.unlock();
146 | return;
147 | }
148 |
149 | cur = programManager.running;
150 | waiting.push_back(&(cur->tagInGeneralList));
151 | cur->status = ProgramStatus::BLOCKED;
152 |
153 | semLock.unlock();
154 | programManager.schedule();
155 | }
156 | }
157 | ```
158 |
159 | ```cpp
160 | void Semaphore::V()
161 | {
162 | semLock.lock();
163 | ++counter;
164 | if (waiting.size())
165 | {
166 | PCB *program = ListItem2PCB(waiting.front(), tagInGeneralList);
167 | waiting.pop_front();
168 | semLock.unlock();
169 | programManager.MESA_WakeUp(program);
170 | }
171 | else
172 | {
173 | semLock.unlock();
174 | }
175 | }
176 | ```
177 |
178 | 在芝士汉堡问题中,信号量问题退化成为自旋锁问题。因此我们可以简单的将`lock()`和`unlock()`替换成`PV`操作。结果和上面的截图是一样的。
179 |
180 | ## 1.2 改进自旋锁
181 |
182 | 原本的实现方法中,锁进程的函数是一个“伪”原子操作。
183 |
184 | 这种伪原子操作的根本原因在于,**key值发生更改之后,没有办法阻止其他进程也修改自身的key值。**
185 |
186 | 因此,改进的思路就在于,**保证bolt值改变在key前面,并且把bolt值判断跳转放在key修改之前。**这样就能保证,即使锁的过程被打断,一旦bolt值改为1,那么其他进程没有办法绕过bolt值判断修改key值,只有原进程才能修改自己的key值。
187 |
188 | 但是,bolt值的判断要尽可能最短,越简单的操作越好验证操作是否为原子操作,最好是一步搞定。
189 |
190 | **`bts`指令就是这样一个符合我们期望的指令。**
191 |
192 | - `bts`的作用是:检查一个位是不是为1。
193 | - 如果是1,CF位置1,原位不改变。
194 | - 如果是0,CF位置0,原位被改变为1。
195 | - `bts`可以对内存直接操作
196 | - 前面的自旋锁中,出现冲突的原因还是因为没有办法直接将两个内存的内容进行置换。这个指令能直接操作内存且是靠硬件实现的,这样我们就能**真正意义上的原子操作修改bolt值。**
197 |
198 | 新的自旋锁的代码如下。
199 |
200 | ```asm
201 | asm_atomic_ex2:
202 | push ebp
203 | mov ebp, esp
204 | pushad
205 |
206 | mov ebx, [ebp + 4 * 3]
207 | bts dword [ebx], 0
208 | jc asm_atomic_ex2_exit
209 | mov ebx, [ebp + 4 * 2]
210 | mov dword [ebx], 0
211 |
212 | asm_atomic_ex2_exit:
213 | popad
214 | pop ebp
215 | ret
216 | ```
217 |
218 | - `bts`指令之前,是把bolt值放进`ebx`中,**最关键的修改操作是原子的,可以保证无论怎么打断都不会出现其他进程可以修改自己的key值。**
219 | - 之后就是修改key值。
220 |
221 | 这样的代码就是**真正意义上的原子操作。且不是靠禁用中断实现的,而是靠逻辑实现。**
222 |
223 | 我们修改一下lock函数调用的代码,从`asm_atomic_exchange`改为`asm_atomic_ex2`,把该补上的声明和头文件语句都加上。
224 |
225 | 最后执行代码结果如下。
226 |
227 | 
228 |
229 | 可以看到,运行的结果并没有发生改变。这个自旋锁是可行的。
230 |
231 | # Assignment 2
232 |
233 | ## 2.1 一个生产者-消费者问题
234 |
235 | 我们考虑这样一个问题:
236 |
237 | - 有两个生产者,有两个消费者,但是只有一个放资源的位置。
238 | - 创建进程时,先创建两个生产者,再创建两个消费者。
239 | - 要求运行时流程如下:
240 | - 一号生产者先花不止一个时间片的时间产生一个资源1。
241 | - 一号消费者取出其产生的资源。(取出0代表取出失败)
242 | - 二号生产者在一号生产者产生完资源,且一号消费者取出资源之后再产生一个资源2。
243 | - 二号消费者取出其产生的资源。取出资源的消费者没有要求,但是要求资源是按照1,2的顺序被取出来的。
244 |
245 | ## 2.2 不使用信号量的结果
246 |
247 | 根据上述要求,如果我们不考虑锁的话,写成的代码应该是这样的。
248 |
249 | ```cpp
250 | void consumer1(void *arg){
251 | printf("consumer1 get first resourse, it is %d\n", resourceList);
252 | }
253 |
254 | void consumer2(void *arg){
255 | printf("consumer2 get first resourse, it is %d\n", resourceList);
256 | }
257 |
258 | void producer1(void *arg){
259 | int delay = 0;
260 | printf("producer1 is making first resourse, the resourse is 1.\n");
261 | // hanging clothes out
262 | delay = 0xfffffff;
263 | while (delay)
264 | --delay;
265 | resourceList = 1;
266 | printf("resourse 1 is done.\n");
267 | }
268 |
269 | void producer2(void *arg){
270 | int delay = 0;
271 | printf("producer2 is making second resourse, the resourse is 2.\n");
272 | delay = 0xfffffff;
273 | while (delay)
274 | --delay;
275 | resourceList = 2;
276 | printf("resourse 2 is done.\n");
277 | }
278 | ```
279 |
280 | 在第一个线程中调用的顺序按照题目要求如下。
281 |
282 | ```cpp
283 | void first_thread(void *arg)
284 | {
285 | // 第1个线程不可以返回
286 | stdio.moveCursor(0);
287 | for (int i = 0; i < 25 * 80; ++i)
288 | {
289 | stdio.print(' ');
290 | }
291 | stdio.moveCursor(0);
292 |
293 | cheese_burger = 0;
294 |
295 | programManager.executeThread(producer1, nullptr, "second thread", 1);
296 | programManager.executeThread(producer2, nullptr, "second thread", 1);
297 | programManager.executeThread(consumer1, nullptr, "third thread", 1);
298 | programManager.executeThread(consumer2, nullptr, "third thread", 1);
299 |
300 | asm_halt();
301 | }
302 | ```
303 |
304 | 我们直接执行代码的话,会发现得到如下结果。
305 |
306 | 
307 |
308 | 我们发现
309 |
310 | - 第一个生产者还没生产完第二个消费者就继续生产了。
311 | - 第一个消费者和第二个消费者都在生成资源时尝试取出资源,都取出失败。
312 |
313 | 这显然是不合理的。
314 |
315 | ## 2.3 使用信号量的结果
316 |
317 | 为了避免这样的情况出现,我们引入两个信号量。
318 |
319 | - `NumberOfPlace`:空位剩余的数量。
320 | - `NumberOfRes`:产生资源的数量。
321 |
322 | 我们给生产者限定如下:
323 |
324 | - 检查是否有空位,有空位才进入生产。
325 | - 生产结束之后,资源数量加一,在资源被取出之前不让出空位。
326 |
327 | 我们给消费者限定如下:
328 |
329 | - 检查是否有资源,有资源才取出资源。
330 | - 让生产这个资源的生产者让出空位,即空位加一。
331 |
332 | 根据定义,我们知道,最开始空位为1,资源为0。因此初始化如下。
333 |
334 | ```cpp
335 | NumberOfPlace.initialize(1);
336 | NumberOfRes.initialize(0);
337 | ```
338 |
339 | 将四个生产者的代码更改如下。
340 |
341 | ```cpp
342 | void consumer1(void *arg){
343 | NumberOfRes.P();
344 | printf("consumer1 get first resourse, it is %d\n", resourceList);
345 | NumberOfPlace.V();
346 | }
347 |
348 | void consumer2(void *arg){
349 | NumberOfRes.P();
350 | printf("consumer2 get second resourse, it is %d\n", resourceList);
351 | NumberOfPlace.V();
352 | }
353 |
354 | void producer1(void *arg){
355 | NumberOfPlace.P();
356 | int delay = 0;
357 | printf("producer1 is making first resourse, the resourse is 1.\n");
358 | // hanging clothes out
359 | delay = 0xfffffff;
360 | while (delay)
361 | --delay;
362 | resourceList = 1;
363 | printf("resourse 1 is done.\n");
364 | NumberOfRes.V();
365 | }
366 |
367 | void producer2(void *arg){
368 | NumberOfPlace.P();
369 | int delay = 0;
370 | printf("producer2 is making second resourse, the resourse is 2.\n");
371 | delay = 0xfffffff;
372 | while (delay)
373 | --delay;
374 | resourceList = 2;
375 | printf("resourse 2 is done.\n");
376 | NumberOfRes.V();
377 | }
378 | ```
379 |
380 | 最后运行的结果如下。
381 |
382 | 
383 |
384 | 可以看到,程序正确的运行了。
385 |
386 | 为了检验这个程序是正确的,我们把生成消费者的过程放在前面,让消费者先运行,看看信号量此时还起不起作用。
387 |
388 | 同时我们在进程中加入打印字符串的功能。
389 |
390 | ```cpp
391 | void first_thread(void *arg)
392 | {
393 | // 第1个线程不可以返回
394 | stdio.moveCursor(0);
395 | for (int i = 0; i < 25 * 80; ++i)
396 | {
397 | stdio.print(' ');
398 | }
399 | stdio.moveCursor(0);
400 |
401 | cheese_burger = 0;
402 | NumberOfPlace.initialize(1);
403 | NumberOfRes.initialize(0);
404 |
405 | programManager.executeThread(consumer1, nullptr, "forth thread", 1);
406 | printf("----consumer1 CREATE----\n");
407 | programManager.executeThread(consumer2, nullptr, "fifth thread", 1);
408 | printf("----consumer2 CREATE----\n");
409 | programManager.executeThread(producer1, nullptr, "second thread", 1);
410 | printf("----producer1 CREATE----\n");
411 | programManager.executeThread(producer2, nullptr, "third thread", 1);
412 | printf("----producer2 CREATE----\n");
413 |
414 |
415 | asm_halt();
416 | }
417 | ```
418 |
419 | 最后运行结果如下。
420 |
421 | 
422 |
423 | 可以看到,即使生产者进程是在后面被调用的,消费者还是等生产者生产完之后再取出资源,生产者2还是等取出资源之后才进行。
424 |
425 | # Assignment 3
426 |
427 | ## 3.1 使用信号量应对哲学家问题
428 |
429 | 哲学家问题中,我们给每个筷子编号的话,那么每个筷子都是一个信号量,大小为1。
430 |
431 | 我们假设每个哲学家都会取自己编号与前一个编号的正数模的筷子。
432 |
433 | 比如,哲学家一号会取五号筷子和一号筷子,哲学家二号会取一号筷子和二号筷子。
434 |
435 | 同时,哲学家思考和吃饭都要时间,因此可以加入`delay`来等待。
436 |
437 | 我们就可以根据此写代码。因为五个哲学家的代码高度雷同,因此我只给出一号哲学家的代码,其他哲学家的代码几乎完全相同。
438 |
439 | ```cpp
440 | void Phy1(void *arg){
441 | int delay = 0;
442 | printf("Phy1 is thinking...\n");
443 | delay = 0xfffffff;
444 | while (delay)
445 | --delay;
446 | cho5.P();
447 | cho1.P();
448 | printf("--Phy1 is EATING!!--\n");
449 | printf("-Now Chopstic- %d - %d - %d - %d - %d\n", cho1.getCounter(), cho2.getCounter(), cho3.getCounter(), cho4.getCounter(), cho5.getCounter());
450 | delay = 0xfffffff;
451 | while (delay)
452 | --delay;
453 | cho5.V();
454 | cho1.V();
455 | printf("--Phy1 finish eating.--\n");
456 |
457 | printf("-Now Chopstic- %d - %d - %d - %d - %d\n", cho1.getCounter(), cho2.getCounter(), cho3.getCounter(), cho4.getCounter(), cho5.getCounter());
458 |
459 | }
460 | ```
461 |
462 | 为了大致观察每一个哲学家进餐时的筷子数量,我写了打印函数观察。
463 |
464 | 首先,为了得到信号量的大小,我们得在类里新写一个返回信号量的函数。
465 |
466 | ```cpp
467 | int Semaphore::getCounter(){
468 | return int(this->counter);
469 | }
470 | ```
471 |
472 | 最后打印出来的结果如下。
473 |
474 | 
475 |
476 | 可以看到,所有哲学家都进餐了,最后所有筷子也都归位了。但是**进餐过程中的筷子变化很有意思。我们来分析一下。**
477 |
478 | - 看第三行筷子,也就是`phy1`进餐完毕之后的筷子数目
479 |
480 | 本来应该是这样的。
481 |
482 | ```
483 | 1 - 0 - 0 - 1 - 1
484 | ```
485 |
486 | 但是最后我们发现四号位的筷子不见了。
487 |
488 | 从后面五号哲学家进餐,可以知道,这个消失的筷子**其实是给五号哲学家拿走了。**
489 |
490 | - 同理,第四行筷子中的第一个筷子也不翼而飞,其实是给二号哲学家拿走了。
491 |
492 | 这些哲学家在他们的进程中都拿走一个筷子,然后等待另一个筷子。
493 |
494 | 这种占着茅坑不拉屎的行为最终会导致**死锁的形成**。
495 |
496 | ## 3.2 死锁情况
497 |
498 | 根据上面的分析,要制造出死锁非常简单。
499 |
500 | **只需要在每个哲学家取完第一个筷子之后加一个足够长的延迟,这样经过五次切换,每个哲学家都只拿到一个筷子,就会进入死锁状态。**
501 |
502 | 将每个哲学家代码更改成如下。
503 |
504 | ```cpp
505 | void Phy1(void *arg){
506 | int delay = 0;
507 | printf("Phy1 is thinking...\n");
508 | delay = 0xfffffff;
509 | while (delay)
510 | --delay;
511 | cho5.P();
512 | delay = 0xfffffff;
513 | while (delay)
514 | --delay;
515 | cho1.P();
516 | printf("--Phy1 is EATING!!--\n");
517 | printf("-Now Chopstic- %d - %d - %d - %d - %d\n", cho1.getCounter(), cho2.getCounter(), cho3.getCounter(), cho4.getCounter(), cho5.getCounter());
518 | delay = 0xfffffff;
519 | while (delay)
520 | --delay;
521 | cho5.V();
522 | cho1.V();
523 | printf("--Phy1 finish eating.--\n");
524 |
525 | printf("-Now Chopstic- %d - %d - %d - %d - %d\n", cho1.getCounter(), cho2.getCounter(), cho3.getCounter(), cho4.getCounter(), cho5.getCounter());
526 |
527 | }
528 | ```
529 |
530 | 最后运行的截图如下。
531 |
532 | 
533 |
534 | 会发现运行卡在这一步了。
535 |
536 | 为了更方便观察,我们将打印函数做一些修正。
537 |
538 | ```cpp
539 | void Phy1(void *arg){
540 | int delay = 0;
541 | printf("Phy1 is thinking...\n");
542 | delay = 0xfffffff;
543 | while (delay)
544 | --delay;
545 | cho5.P();
546 | printf("--Phy1 got cho5--%d - %d - %d - %d - %d\n", cho1.getCounter(), cho2.getCounter(), cho3.getCounter(), cho4.getCounter(), cho5.getCounter());
547 | delay = 0xfffffff;
548 | while (delay)
549 | --delay;
550 | cho1.P();
551 | printf("--Phy1 got cho1--%d - %d - %d - %d - %d\n", cho1.getCounter(), cho2.getCounter(), cho3.getCounter(), cho4.getCounter(), cho5.getCounter());
552 | printf("--Phy1 is EATING!!--\n");
553 | delay = 0xfffffff;
554 | while (delay)
555 | --delay;
556 | cho5.V();
557 | printf("--Phy1 released cho5--%d - %d - %d - %d - %d\n", cho1.getCounter(), cho2.getCounter(), cho3.getCounter(), cho4.getCounter(), cho5.getCounter());
558 | cho1.V();
559 | printf("--Phy1 released cho1--%d - %d - %d - %d - %d\n", cho1.getCounter(), cho2.getCounter(), cho3.getCounter(), cho4.getCounter(), cho5.getCounter());
560 | printf("--Phy1 finish eating.--\n");
561 | }
562 | ```
563 |
564 | 最后运行的结果如下。
565 |
566 | 
567 |
568 | 可以看到,每一个哲学家都只拿了一个筷子,造成了死锁。
569 |
570 | ## 3.3 解决死锁
571 |
572 | 解决死锁有一个简单的思路。
573 |
574 | **检测到下一根筷子拿不到的话就不拿当前的筷子了。**
575 |
576 | 因为每次拿筷子都是从左手开始起手,因此只要留有筷子,一定有筷子能够利用到这根筷子。
577 |
578 | 具体说来是写一个循环,如果下一根筷子拿不到就一直卡在那里等待。
579 |
580 | 只需要一行代码。
581 |
582 | ```cpp
583 | while(!cho1.getCounter()) {;}
584 | ```
585 |
586 | 因此,一号哲学家的代码更正成如下。
587 |
588 | ```cpp
589 | void Phy1(void *arg){
590 | int delay = 0;
591 | printf("Phy1 is thinking...\n");
592 | delay = 0xfffffff;
593 | while (delay)
594 | --delay;
595 | while(!cho1.getCounter()) {;} //更正处
596 | cho5.P();
597 | printf("--Phy1 got cho5--%d - %d - %d - %d - %d\n", cho1.getCounter(), cho2.getCounter(), cho3.getCounter(), cho4.getCounter(), cho5.getCounter());
598 | delay = 0xfffffff;
599 | while (delay)
600 | --delay;
601 | cho1.P();
602 | printf("--Phy1 got cho1--%d - %d - %d - %d - %d\n", cho1.getCounter(), cho2.getCounter(), cho3.getCounter(), cho4.getCounter(), cho5.getCounter());
603 | printf("--Phy1 is EATING!!--\n");
604 | delay = 0xfffffff;
605 | while (delay)
606 | --delay;
607 | cho5.V();
608 | printf("--Phy1 released cho5--%d - %d - %d - %d - %d\n", cho1.getCounter(), cho2.getCounter(), cho3.getCounter(), cho4.getCounter(), cho5.getCounter());
609 | cho1.V();
610 | printf("--Phy1 released cho1--%d - %d - %d - %d - %d\n", cho1.getCounter(), cho2.getCounter(), cho3.getCounter(), cho4.getCounter(), cho5.getCounter());
611 | printf("--Phy1 finish eating.--\n");
612 | }
613 | ```
614 |
615 | 对每一个进程都做这样的修改,最后运行结果如下(没有运行完)。
616 |
617 | 
618 |
619 | 可以看到,在第一次流转中,五号哲学家检测到取了筷子也无法保证另一个筷子能被取到,因此放弃取筷子。
620 |
621 | 这样四号哲学家成为了唯一一个能够吃上饭的哲学家。
622 |
623 | 四号吃完之后,释放的资源又能给三号吃饭,如此往复。
624 |
625 | 等到运行结束之后结果如下。
626 |
627 | 
628 |
629 | 可以看到吃饭顺序是`4-3-2-1-5`,符合我们的预期,其中五号哲学家在最后才拿到第一根筷子。
630 |
631 | 混口饭吃可真难啊。
632 |
--------------------------------------------------------------------------------
/03_实验3实验报告.md:
--------------------------------------------------------------------------------
1 | # Assignment1
2 |
3 | ## 1.1 Example1复现
4 |
5 | 我会在此次复现中讲述自己对课件的理解和具体复现的结果。
6 |
7 | ### 1.1.1 内存安排
8 |
9 | 因为在内核启动的时候,操作系统没有这么智能,能自动分别配好MBR和bootloader的空间和地址。因此,我们需要自己安排bootloader的大小和地址。
10 |
11 | 因为MBR大小是512字节。
12 |
13 | 所以bootloader的起始地址就是MBR后移512,具体是`0x7e00`。
14 |
15 | bootloader的作用是拓展MBR不足的内存,以执行大小更大的程序。bootloader本质上是一个启动之后第一个载入的硬盘,因此我们需要用读取硬盘的逻辑去安排bootloader的内存。
16 |
17 | 我们这次实验中就留5个扇区的空间即可。一个扇区大小和MBR一样,通常是512字节。
18 |
19 | ### 1.1.2 加载Bootloader
20 |
21 | 根据前面的知识,操作系统启动的顺序是`MBR->Bootloader`。因此我们要写两个程序:
22 |
23 | - 修改MBR,让其能载入Bootloader
24 | - 编写Bootloader实现相应的功能
25 |
26 | ### 1.1.3 修改MBR
27 |
28 | 再次明确,Bootloader的读取逻辑是硬盘的读取逻辑。
29 |
30 | 根据课件我们要做的事情有四件:
31 |
32 | 1. 使用`out`汇编指令,将第一个扇区号写入内核设定好的硬盘端口中。
33 | 2. 将读取扇区的数量写入内核设定好的端口中。
34 | 3. 向相应的端口请求硬盘读。
35 | 4. 编写相应代码,让硬盘完成其他操作之后再开始本次读操作,防止冲突。
36 |
37 | 在x86中,LBA相关的端口如下。
38 |
39 | 1. **数据端口(Data Port)**:用于传输读取或写入的数据。对于主硬盘(Primary Hard Disk),数据端口是`0x1F0`;对于从硬盘(Secondary Hard Disk),数据端口是`0x170`。
40 | 2. **扇区计数端口(Sector Count Port)**:用于指定要读取或写入的扇区数量。对于主硬盘,扇区计数端口是`0x1F2`;对于从硬盘,扇区计数端口是`0x172`。
41 | 3. **LBA扇区号端口(LBA Low端口、LBA中端口、LBA高端口)**:用于指定要读取或写入的扇区号。LBA地址是32位的,因此通常需要使用四个端口来传输。对于主硬盘,这四个端口是`0x1F3`、`0x1F4`、`0x1F5`、`0x1F6`;对于从硬盘,这四个端口是`0x173`、`0x174`、`0x175`、`0x176`。
42 | 4. **命令端口(Command Port)**:用于发送命令给硬盘控制器,指示要执行的操作。对于主硬盘,命令端口是`0x1F7`;对于从硬盘,命令端口是`0x177`。
43 | 5. **状态端口(Status Port)**:用于读取硬盘控制器的状态。对于主硬盘,状态端口是`0x1F7`;对于从硬盘,状态端口是`0x177`。
44 |
45 | 最后我写的代码如下:
46 |
47 | - MBR
48 |
49 | ```assembly
50 | org 0x7c00
51 | [bits 16]
52 | xor ax,ax
53 | mov ds,ax
54 | mov ss,ax
55 | mov es,ax
56 | mov fs,ax
57 | mov gs,ax
58 |
59 | mov sp,0x7c00
60 | mov ax,1
61 | mov cx,0
62 | mov bx,0x7e00
63 | load_bootloader:
64 | call asm_read_hard_disk
65 | inc ax
66 | cmp ax,5
67 | jle load_bootloader
68 | jmp 0x0000:0x7e00
69 |
70 | jmp $
71 |
72 | asm_read_hard_disk:
73 | mov dx,0x1f3
74 | out dx,al
75 | inc dx
76 | mov al,ah
77 | out dx,al
78 | mov ax,cx
79 | inc dx
80 | out dx,al
81 | inc dx
82 | mov al,ah
83 | and al,0x0f
84 | or al,0xe0
85 | out dx,al
86 |
87 | mov dx,0x1f2
88 | mov al,1
89 | out dx,al
90 | mov dx,0x1f7
91 | mov al,0x20
92 | out dx,al
93 |
94 | .waits:
95 | in al,dx
96 | and al,0x88
97 | cmp al,0x08
98 | jnz .waits
99 |
100 | mov cx,256
101 | mov dx,0x1f0
102 | .readw:
103 | in ax,dx
104 | mov [bx],ax
105 | add bx,2
106 | loop .readw
107 | ret
108 |
109 | times 510 - ($ - $$) db 0
110 | db 0x55,0xaa
111 | ```
112 |
113 | - Bootloader
114 |
115 | ```assembly
116 | org 0x7e00
117 | [bits 16]
118 |
119 | mov ax,0xb800
120 | mov gs,ax
121 | mov ah,0x03
122 | mov ecx,bootloader_tag_end - bootloader_tag
123 | xor ebx,ebx
124 | mov esi,bootloader_tag
125 | output_bootloader_tag:
126 | mov al,[esi]
127 | mov word[gs:bx],ax
128 | inc esi
129 | add ebx,2
130 | loop output_bootloader_tag
131 | jmp $
132 |
133 | bootloader_tag db 'run bootloader'
134 | bootloader_tag_end:
135 | ```
136 |
137 | 我们编译bootloader。
138 |
139 | ```bash
140 | nasm -f bin bootloader.asm -o bootloader.bin
141 | ```
142 |
143 | ```bash
144 | dd if=bootloader.bin of=hd.img bs=512 count=5 seek=1 conv=notrunc
145 | ```
146 |
147 | mbr
148 |
149 | ```bash
150 | nasm -f bin mbr.asm -o mbr.bin
151 | ```
152 |
153 | ```bash
154 | dd if=mbr.bin of=hd.img bs=512 count=1 seek=0 conv=notrunc
155 | ```
156 |
157 | 最后使用qemu运行
158 |
159 | ```bash
160 | qemu-system-i386 -hda hd.img -serial null -parallel stdio
161 | ```
162 |
163 | 最后运行的结果如下图所示。
164 |
165 | 
166 |
167 | ## 1.2 CHS读取硬盘
168 |
169 | CHS是一种读取硬盘的方式,需要给出三个位置:**磁头,扇区和柱面**。
170 |
171 | 其中的取值如下。
172 |
173 | 1. 柱面号C:0-总柱面数
174 | 2. 磁头号H:0-(总磁头数-1)
175 | 3. 扇区号S:1-每磁道扇区数
176 |
177 | LBA只需要给出逻辑块的位置。
178 |
179 | 为了将CHS模式转化为LBA模式读取,需要有对应的转化公式。
180 |
181 | 一种比较简单的转换方式是通过以下公式进行计算:
182 |
183 | - 扇区号 = (LBA % 每磁道扇区数) + 1
184 | - 磁头号 = (LBA / 每柱面扇区数) % 磁头总数
185 | - 柱面号 = (LBA / 每磁道扇区数) / 磁头总数
186 |
187 | 这个公式是一种通用的转化方法,不过在具体使用的时候不同的硬盘可能使用的公式不一样。
188 |
189 | 使用CHS读取硬盘时,BIOS已经提供了相应的中断实现这个功能。
190 |
191 | 功能号是13。
192 |
193 | 其中传参要求如下:
194 |
195 | - ah:对硬盘的操作。2是读取扇区。
196 | - al:读取扇区的个数。
197 | - ch:柱面号。
198 | - cl:扇区号(低六位)。
199 | - dh:磁头号。
200 | - dl:硬盘号,0x80表示第一个硬盘。
201 | - bx:存储读取数据的内存地址。
202 |
203 | 该次读取能一次性读取完一个扇区的大小,因此不再需要手动做位移。
204 |
205 | 其中,在虚拟硬盘中,有一些重要参数如下。
206 |
207 | | 参数 | 数值 |
208 | | -------------------------------- | ---- |
209 | | 驱动器号(DL寄存器) | 80h |
210 | | 每磁道扇区数 | 63 |
211 | | 每柱面磁头数(每柱面总的磁道数) | 18 |
212 | | 每柱面扇区数 | 1134 |
213 |
214 | 在这次实验中,我们的逻辑扇区的LBA是1。
215 |
216 | 因此我们可以换算如下。
217 |
218 | - 扇区号 = (1%63)+1 = 2
219 | - 磁头号 = (1/1134)%1(假设为1)= 0
220 | - 柱面号 = 0
221 |
222 | 我们得到了CHS的表达式,可以编写初步的代码了。
223 |
224 | ```assembly
225 | mov ah, 0x02 ; 读取扇区
226 | mov al, 1 ; 读取1个扇区
227 | mov ch, 0 ; 柱面号
228 | mov cl, 2 ; 扇区号
229 | mov dh, 0 ; 磁头号
230 | mov dl, 0x80 ; 硬盘号
231 | mov bx, 0x7e00 ; 存储读取数据的内存地址,当然是bootloader的地址。
232 | int 0x13 ; 调用BIOS中断
233 | ```
234 |
235 | 最后我们将这段初步代码装入mbr代码中。
236 |
237 | ```assembly
238 | org 0x7c00
239 | [bits 16]
240 | xor ax,ax
241 | mov ds,ax
242 | mov ss,ax
243 | mov es,ax
244 | mov fs,ax
245 | mov gs,ax
246 |
247 | mov sp,0x7c00
248 | mov ax,1
249 | mov cx,0
250 | mov bx,0x7e00
251 | load_bootloader:
252 | call asm_read_hard_disk
253 | inc ax
254 | cmp ax,5
255 | jle load_bootloader
256 | jmp 0x0000:0x7e00
257 |
258 | jmp $
259 |
260 | asm_read_hard_disk:
261 | mov ah, 0x02
262 | mov al, 1
263 | mov ch, 0
264 | mov cl, 2
265 | mov dh, 0
266 | mov dl, 0x80
267 | int 0x13
268 | add bx, 128
269 | add bx, 128
270 | add bx, 128
271 | add bx, 128 ;读取完数据之后bx加512。我分四次相加是怕报错(不知道直接加行不行)
272 | ret
273 |
274 | times 510 - ($ - $$) db 0
275 | db 0x55,0xaa
276 | ```
277 |
278 | 最后按照上述流程跑一边,截图如下。
279 |
280 | 
281 |
282 | 结果一样,但是可以从截图看出,我使用的是BIOS中断来载入bootloader数据。
283 |
284 | # Assignment2
285 |
286 | ## 2.1 内存安排
287 |
288 | 保护模式,指所有的程序都在固定的段内运行,保证程序不会越界。
289 |
290 | 但是操作系统不会自动帮你定义段描述符数组(就是**GDT**)。我们需要自己划空间。
291 |
292 | 我们这次试验有五个扇区,因此bootloader中一共有`0x0A00`个字节。
293 |
294 | 所以GDT只要接在bootloader后即可。
295 |
296 | | Name | Start | Length | End |
297 | | ---------- | ------ | --------------- | ------ |
298 | | MBR | 0x7c00 | 0x200(512B) | 0x7e00 |
299 | | bootloader | 0x7e00 | 0xa00(512B * 5) | 0x8800 |
300 | | GDT | 0x8800 | 0x80(8B * 16) | 0x8880 |
301 |
302 | 实验指南使用一个独立的文件来存放常量。事实上,这种模块化的方法非常有利于后续实验中不断搭建更多新功能时修改代码。
303 |
304 | 我们先写一些马上就要用到的常量:加载器被加载地址和GDT起始位置
305 |
306 | 后续再加东西上去。
307 |
308 | ```assembly
309 | LOADER_SECTOR_COUNT equ 5
310 | LOADER_START_SECTOR equ 1
311 | LOADER_START_ADDRESS equ 0x7e00
312 | ;GDT
313 | GDT_START_ADDRESS equ 0x8800
314 | ```
315 |
316 | ## 2.2 打开保护模式
317 |
318 | 保护模式的打开是在bootloader中。
319 |
320 | 因此我们需要修改对应的bootloader文件,做好四件事:
321 |
322 | - 加载GDTR(段描述符表的大小)
323 | - 打开第21根地址线。(PS:这根地址线需要经过南校芯片端口打开)
324 | - 打开保护模式(设置CR0为1)
325 | - 远跳转到对应的段,进入保护模式
326 |
327 | 首先需要知道,保护模式下,在每个段中运行程序时没有实模式启动这样,操作系统没有设置任何段描述符(比如数据段)。也就是说,我们需要自己设定各个段描述符,并自己在程序中调用自己设置的段描述符才能保证程序正确运行。
328 |
329 | 我们有显示字符的需求,因此我们需要设置以下的段:
330 |
331 | - 代码段描述符
332 | - 数据段描述符
333 | - 栈段描述符
334 | - 视频段描述符(就是显示映射)
335 |
336 | 在保护模式中,我们的程序给出的都是偏移地址,偏移地址和段线性基地址得到线性地址。
337 |
338 | 如果我们开启了分页机制的话,那么线性地址不一定是物理地址,需要变换。
339 |
340 | 但是此时没有开启分页,物理地址就是线性地址。
341 |
342 | 一个段地址空间有足足4GB,因此完全可以把所有段描述符放进一个段中。更近一步,我们可以把段的线性基地址都改成0,这样甚至偏移地址就是线性地址了,简化了编程逻辑。这种内存访问方式称为**平坦模式**。
343 |
344 | > PS:c/c++编译链接得到的二进制程序默认运行在平坦模式下。
345 |
346 | ### 2.2.1 编写GDT部分
347 |
348 | 我们需要知道段描述符的表达格式。
349 |
350 | 
351 |
352 | 这是个64位的描述符,因此需要分成两个字(四个字节)来存。
353 |
354 | - **段基地址**。段基地址共32位,是段的起始地址,**被拆分成三部分放置**(计算的时候不要漏了)。
355 | - **G位**。G表示粒度, G=0表示段界限以字节为单位, G=1表示段界限以4KB为单位。
356 | - **D/B位**。D/B位是默认操作数的大小或默认堆栈指针的大小,在保护模式下,该位置为1,表示32位。
357 | - **L位**。L位是 64 位代码段标志,由于这里我们使用的是32位的代码,所以L置0。
358 | - **AVL**。AVL位是保留位。
359 | - **段界限**。段界限表示段的偏移地址范围,我们在后面详细讨论这个问题。
360 | - **P位**。P位是段存在位, P=1表示段存在, P=0表示段不存在。
361 | - **DPL**。DPL指明访问该段必须有的最低优先级,优先级从0-3依次降低,即0拥有最高优先级,3拥有最低优先级。
362 | - **S位**。S位是描述符类型。S=0表示该段是系统段,S=1表示该段位代码段或数据段。
363 | - **TYPE**。TYPE指示代码段或数据段的类型。
364 |
365 | 需要注意:GDT的第0个描述符必须是全0的描述符。
366 |
367 | 因此我们有5个段,编号从0到4。
368 |
369 | - 第一个:数据段,这个段需要包含所有数据,因此把段界限设到最大,粒度开到最大。
370 | - 第二个:保护模式下的堆栈段,因为是堆栈段,因此我们可以同时把基地址设0,界限设0设为栈顶。但是这就提出一个要求,**千万要小心规划其他段的内存防止出现溢出**。
371 | - 第三个:保护模式下的视频段。我们设置为`0x000B8000`,因为高32位的尾部有一个b所以也要计算进去。
372 | - 第四个:保护模式下平坦模式代码段。段基地址设置为0。这个情况下,数据段和代码段是冲突的。但是**代码会在编译的过程中会对所有变量进行重定向**,编译器会给数据段和代码段乃至堆栈段分配不同的虚拟地址。
373 |
374 | ```assembly
375 | %include "boot.inc"
376 | org 0x7e00
377 | [bits 16]
378 |
379 | ;第零个:空描述符
380 | mov dword [GDT_START_ADDRESS+0x00],0x00
381 | mov dword [GDT_START_ADDRESS+0x04],0x00
382 |
383 | ;第一个:数据段描述符
384 | mov dword [GDT_START_ADDRESS+0x08],0x0000ffff ; 基地址为0,段界限为0xFFFFF
385 | mov dword [GDT_START_ADDRESS+0x0c],0x00cf9200 ; 粒度为4KB,存储器段描述符
386 |
387 | ;第二个:保护模式下的堆栈段描述符
388 | mov dword [GDT_START_ADDRESS+0x10],0x00000000 ; 基地址为0,界限0x0
389 | mov dword [GDT_START_ADDRESS+0x14],0x00409600 ; 粒度为1个字节
390 |
391 | ;第三个:保护模式下的显存描述符
392 | mov dword [GDT_START_ADDRESS+0x18],0x80007fff ; 基地址为0x000B8000,界限0x07FFF
393 | mov dword [GDT_START_ADDRESS+0x1c],0x0040920b ; 粒度为字节
394 |
395 | ;第四个:保护模式下平坦模式代码段描述符
396 | mov dword [GDT_START_ADDRESS+0x20],0x0000ffff ; 基地址为0,段界限为0xFFFFF
397 | mov dword [GDT_START_ADDRESS+0x24],0x00cf9800
398 | ```
399 |
400 | 一共有5个段,所以GDTR的界限是39字节。
401 |
402 | 接下来我们根据顺序,结合低三位都为0,设置段选择子。
403 |
404 | ```assembly
405 | ;平坦模式数据段选择子
406 | DATA_SELECTOR equ 0x8
407 | ;平坦模式栈段选择子
408 | STACK_SELECTOR equ 0x10
409 | ;平坦模式视频段选择子
410 | VIDEO_SELECTOR equ 0x18
411 | VIDEO_NUM equ 0x18
412 | ;平坦模式代码段选择子
413 | CODE_SELECTOR equ 0x20
414 | ```
415 |
416 | 最后,我们只需要打开21号地址线,并远跳转加载所有段即可。
417 |
418 | 最后我们需要在原来程序上的四个位置打断点:
419 |
420 | - 初始化GDTR
421 | - 打开A20
422 | - 设置PE位
423 | - 加载完所有选择子之后打断点查看寄存器内容
424 |
425 | ### 2.2.2 断点结果
426 |
427 | 我们编译bootloader。
428 |
429 | ```bash
430 | nasm -f bin bootloader.asm -o bootloader.bin
431 | ```
432 |
433 | ```bash
434 | dd if=bootloader.bin of=hd.img bs=512 count=5 seek=1 conv=notrunc
435 | ```
436 |
437 | mbr
438 |
439 | ```bash
440 | nasm -f bin mbr.asm -o mbr.bin
441 | ```
442 |
443 | ```bash
444 | dd if=mbr.bin of=hd.img bs=512 count=1 seek=0 conv=notrunc
445 | ```
446 |
447 | 最后使用qemu,使用debug运行
448 |
449 | ```bash
450 | qemu-system-i386 -hda hd.img -serial null -parallel stdio -s -S
451 | ```
452 |
453 | 我们使用GDB远程连接,就能够开始调试了。
454 |
455 | 我们拥有以下的指令,方便我们进行调试:
456 |
457 | 显示下一步指令
458 |
459 | ```bash
460 | set disassemble-next-line on
461 | ```
462 |
463 | 以intel风格显示汇编指令
464 |
465 | ```bash
466 | set disassembly-flavor intel
467 | ```
468 |
469 | 查看接下来的五十条指令的汇编代码
470 |
471 | ```bash
472 | x/50i $pc
473 | ```
474 |
475 | 查看寄存器
476 |
477 | ```bash
478 | i r
479 | ```
480 |
481 | 我们可以在程序内部使用nop指令标记好位置(因为编译器会改变我们的程序代码)。我们在上述四个关键部位做好标记,方便之后打断点。
482 |
483 | 
484 |
485 | 之后我们编译并启动虚拟机,远程连接。我们先在`0x7e00`打断点,以便于我们一步执行到bootloader。
486 |
487 | 
488 |
489 | 然后我们查看后面的70条指令。
490 |
491 | 
492 |
493 | 我们找到了当初我们写的三个nop。第一个nop没有找到,但是我们可以发现`in al,0x92`的指令。因此我们只要在这里打断点,相当于完成给GDTR赋值的任务。
494 |
495 | 因此我们在这四个地方打断点。
496 |
497 | - `0x7e8a`
498 | - `0x7e90`
499 | - `0x7e9c`
500 | - `0x7ebc`
501 |
502 | 
503 |
504 | 到达第一个断点,我们查看寄存器。
505 |
506 | 
507 |
508 | 我们暂时查看不了gdtr的内容。这可能是权限问题导致的。我们先看第二个断点的情况。
509 |
510 | 
511 |
512 | 我们看eax的内容。
513 |
514 | - 观察eax的低2位(十六进制),可以发现,内容是`0x02`。这是把二进制的第一位设置成了1,说明这个寄存器存储了打开A20的机器码。
515 | - 这句代码的上一句是`out 0x92,al`。说明这个时候`0x92`端口,也就是A20端口被写了`0x02`,说明此时第21根地址线打开了,我们现在可以访问cr0了。
516 |
517 | 我们现在来看第三个断点的情况。为了观察cr0是否真的改变了。我们改成逐步执行。
518 |
519 | 
520 |
521 | 将cr0的值赋给eax,eax的内容为`0x10`。
522 |
523 | 
524 |
525 | eax的值变成了`0x11`传回给了cr0。说明此时保护模式被真正打开了。
526 |
527 | 我们运行到最后一个断点,观察所有寄存器的值。
528 |
529 | 下面六个寄存器便是段寄存器。我们在代码中把数据段选择子`0x08`放进了ds,es中,把堆栈段`0x10`放进了ss中,最后把显存段`0x18`放进了gs中。
530 |
531 | 可以看到对应的寄存器确实是我们期望的数值。
532 |
533 | # Assignment3
534 |
535 | 为了确保这是在保护模式下运行的而不是我直接把第二次实验的截图贴上去,我们要保留输出”run bootloader“和"enter protect mode"。同时,移植程序时。不再需要填补字节了,因此代码最后需要修改。
536 |
537 | 除此之外,因为前面的程序已经做好了各种段的初始化,因此段初始化也不再需要。
538 |
539 | 多运用变量编写程序而不是直接操作内存的写代码方式让程序变得便于移植!
540 |
541 | 移植之后的bootloader程序源代码如下。
542 |
543 | ```assembly
544 | %include "boot.inc"
545 | org 0x7e00
546 | [bits 16]
547 | mov ax, 0xb800
548 | mov gs, ax
549 | mov ah, 0x03 ;青色
550 | mov ecx, bootloader_tag_end - bootloader_tag
551 | xor ebx, ebx
552 | mov esi, bootloader_tag
553 | output_bootloader_tag:
554 | mov al, [esi]
555 | mov word[gs:bx], ax
556 | inc esi
557 | add ebx,2
558 | loop output_bootloader_tag
559 |
560 | ;空描述符
561 | mov dword [GDT_START_ADDRESS+0x00],0x00
562 | mov dword [GDT_START_ADDRESS+0x04],0x00
563 |
564 | ;创建描述符,这是一个数据段,对应0~4GB的线性地址空间
565 | mov dword [GDT_START_ADDRESS+0x08],0x0000ffff ; 基地址为0,段界限为0xFFFFF
566 | mov dword [GDT_START_ADDRESS+0x0c],0x00cf9200 ; 粒度为4KB,存储器段描述符
567 |
568 | ;建立保护模式下的堆栈段描述符
569 | mov dword [GDT_START_ADDRESS+0x10],0x00000000 ; 基地址为0x00000000,界限0x0
570 | mov dword [GDT_START_ADDRESS+0x14],0x00409600 ; 粒度为1个字节
571 |
572 | ;建立保护模式下的显存描述符
573 | mov dword [GDT_START_ADDRESS+0x18],0x80007fff ; 基地址为0x000B8000,界限0x07FFF
574 | mov dword [GDT_START_ADDRESS+0x1c],0x0040920b ; 粒度为字节
575 |
576 | ;创建保护模式下平坦模式代码段描述符
577 | mov dword [GDT_START_ADDRESS+0x20],0x0000ffff ; 基地址为0,段界限为0xFFFFF
578 | mov dword [GDT_START_ADDRESS+0x24],0x00cf9800 ; 粒度为4kb,代码段描述符
579 |
580 | ;初始化描述符表寄存器GDTR
581 | mov word [pgdt], 39 ;描述符表的界限
582 | lgdt [pgdt]
583 | nop
584 |
585 | in al,0x92 ;南桥芯片内的端口
586 | or al,0000_0010B
587 | out 0x92,al ;打开A20
588 | nop
589 |
590 | cli ;中断机制尚未工作
591 | mov eax,cr0
592 | or eax,1
593 | mov cr0,eax ;设置PE位
594 | nop
595 |
596 | ;以下进入保护模式
597 | jmp dword CODE_SELECTOR:protect_mode_begin
598 |
599 | ;16位的描述符选择子:32位偏移%include "boot.inc"
600 | org 0x7e00
601 | [bits 16]
602 | mov ax, 0xb800
603 | mov gs, ax
604 | mov ah, 0x03 ;青色
605 | mov ecx, bootloader_tag_end - bootloader_tag
606 | xor ebx, ebx
607 | mov esi, bootloader_tag
608 | output_bootloader_tag:
609 | mov al, [esi]
610 | mov word[gs:bx], ax
611 | inc esi
612 | add ebx,2
613 | loop output_bootloader_tag
614 |
615 | ;空描述符
616 | mov dword [GDT_START_ADDRESS+0x00],0x00
617 | mov dword [GDT_START_ADDRESS+0x04],0x00
618 |
619 | ;创建描述符,这是一个数据段,对应0~4GB的线性地址空间
620 | mov dword [GDT_START_ADDRESS+0x08],0x0000ffff ; 基地址为0,段界限为0xFFFFF
621 | mov dword [GDT_START_ADDRESS+0x0c],0x00cf9200 ; 粒度为4KB,存储器段描述符
622 |
623 | ;建立保护模式下的堆栈段描述符
624 | mov dword [GDT_START_ADDRESS+0x10],0x00000000 ; 基地址为0x00000000,界限0x0
625 | mov dword [GDT_START_ADDRESS+0x14],0x00409600 ; 粒度为1个字节
626 |
627 | ;建立保护模式下的显存描述符
628 | mov dword [GDT_START_ADDRESS+0x18],0x80007fff ; 基地址为0x000B8000,界限0x07FFF
629 | mov dword [GDT_START_ADDRESS+0x1c],0x0040920b ; 粒度为字节
630 |
631 | ;创建保护模式下平坦模式代码段描述符
632 | mov dword [GDT_START_ADDRESS+0x20
633 | ;清流水线并串行化处理器
634 | [bits 32]
635 | protect_mode_begin:
636 |
637 | mov eax, DATA_SELECTOR ;加载数据段(0..4GB)选择子
638 | mov ds, eax
639 | mov es, eax
640 | mov eax, STACK_SELECTOR
641 | mov ss, eax
642 | mov eax, VIDEO_SELECTOR
643 | mov gs, eax
644 | nop
645 |
646 | mov ecx, protect_mode_tag_end - protect_mode_tag
647 | mov ebx, 80 * 2
648 | mov esi, protect_mode_tag
649 | mov ah, 0x3
650 | output_protect_mode_tag:
651 | mov al, [esi]
652 | mov word[gs:ebx], ax
653 | add ebx, 2
654 | inc esi
655 | loop output_protect_mode_tag
656 |
657 | ;load number
658 | proc:
659 | ;mod fuction
660 | add byte [num],1
661 | mov al,[num]
662 | cmp al,10
663 | jne next1
664 | mov al,0
665 | mov byte [num],al
666 | next1: add word [color], 00100010b
667 |
668 | ;change ASCII
669 | add al,'0'
670 | mov byte [string],al
671 |
672 | ;count address
673 | jmp count_address
674 | proc1:
675 | ;GS address
676 | mov eax,0
677 | mov ebx,0
678 | mov ax, [yAddr]
679 | mov bx,80
680 | mul bx
681 | add ax, [xAddr]
682 | mov bx,ax
683 |
684 | ;All in!
685 | mov eax,0
686 | mov ax,bx
687 | mov dh,[color]
688 | mov dl,[string]
689 | mov WORD [gs:eax], dx
690 |
691 | ;delay time
692 | mov cx,delay_time
693 | delay_loop:
694 | nop
695 | mov bx,delay_time
696 | loop2:
697 | dec bx
698 | cmp bx,0
699 | nop
700 | jne loop2
701 | nop
702 | loop delay_loop
703 | jmp proc
704 |
705 |
706 | count_address:
707 | cmp byte [xflag], 1
708 | je add_value_x
709 | jmp sub_value_x
710 | add_value_x:
711 | inc word [xAddr]
712 | inc word [xAddr]
713 | mov ax,[xAddr]
714 | cmp ax, [xMax]
715 | jle y_count
716 | mov byte [xflag],0
717 | jmp y_count
718 | sub_value_x:
719 | dec word [xAddr]
720 | dec word [xAddr]
721 | cmp word [xAddr],2
722 | jge y_count
723 | mov byte [xflag],1
724 | jmp y_count
725 | y_count:
726 | cmp byte [yflag], 1
727 | je add_value_y
728 | jmp sub_value_y
729 | add_value_y:
730 | inc word [yAddr]
731 | inc word [yAddr]
732 | mov ax,[yAddr]
733 | cmp ax, [yMax]
734 | jle proc1
735 | mov byte [yflag],0
736 | jmp proc1
737 | sub_value_y:
738 | dec word [yAddr]
739 | dec word [yAddr]
740 | cmp word [yAddr],0
741 | jge proc1
742 | mov byte [yflag],1
743 | jmp proc1
744 |
745 | pgdt dw 0
746 | dd GDT_START_ADDRESS
747 |
748 | bootloader_tag db 'run bootloader'
749 | bootloader_tag_end:
750 |
751 | protect_mode_tag db 'enter protect mode'
752 | protect_mode_tag_end:
753 |
754 |
755 | num db 0
756 | string db 48
757 | color dw 00000010b
758 | xAddr dw 2
759 | yAddr dw 0
760 | xMax dw 80*2-1
761 | yMax dw 25*2-2
762 | xflag db 1
763 | yflag db 1
764 | delay_time dw 0xFFFF
765 | ```
766 |
767 | 最后我们编译运行虚拟机,得到如下截图:
768 |
769 | 
770 |
771 | 通过截图上的虚拟机,这个程序确实是在进入了保护模式之后运行的。而且也正常地弹窗了。
772 |
773 | 还是很棒的完成了任务!
774 |
--------------------------------------------------------------------------------
/08_实验8实验报告.md:
--------------------------------------------------------------------------------
1 | # Assignment 1
2 |
3 | 编写一个系统调用,然后在进程中调用之,根据结果回答以下问题。
4 |
5 | - 展现系统调用执行结果的正确性,结果截图并并说说你的实现思路。
6 | - 请根据gdb来分析执行系统调用后的栈的变化情况。
7 | - 请根据gdb来说明TSS在系统调用执行过程中的作用。
8 |
9 | ## 1.1 编写一个系统调用
10 |
11 | 我们使用`src1`的代码。
12 |
13 | 在实验指导详细而层序渐进的讲述中,系统调用的逻辑变得非常清晰易懂。
14 |
15 | 想要自己编写一个系统调用,根据已经实现好的函数,过程也变得非常容易。
16 |
17 | 首先,我们把我们想要的中断函数在`setup`中写好。
18 |
19 | ```cpp
20 | int syscall_1(int first, int second, int third, int forth, int fifth)
21 | {
22 | printf("systerm call 1: SoliTa Interrupt.\n");
23 | return 114514;
24 | }
25 | ```
26 |
27 | 然后我们根据系统调用的逻辑将这个中断函数添加到中断向量表中。
28 |
29 | - 首先我们把这个函数声明添加进`syscall`的头文件中。
30 |
31 | - 然后,我们已经实现好了这个添加向量表的函数,在`setup`中调用即可。
32 |
33 | ```cpp
34 | systemService.setSystemCall(1, (int)syscall_1);
35 | ```
36 |
37 | - 最后,我们在`main`函数中,在新建第一个线程之前,调用中断1。这个中断调用函数是我们之前就实现好的了。
38 |
39 | ```cpp
40 | ret = asm_system_call(1);
41 | ```
42 |
43 | 最后编译运行,可以看到结果如下。
44 |
45 | 
46 |
47 | 可以看到,在最后一行中,我们自己写的中断1被成功地调用执行了。
48 |
49 | 然后我们使用gdb,来看看这个过程中发生了什么。
50 |
51 | ## 1.2 栈的变化情况
52 |
53 | 我们以第一次调用中断为跟踪目标,也就是跟踪实验指导中实现的中断0。
54 |
55 | 我们先明确有关栈的寄存器。
56 |
57 | - **ESP/RSP**:栈指针寄存器,指向当前栈顶。
58 |
59 | - **EBP/RBP/FP**:基址指针寄存器,指向当前栈帧的基址。
60 |
61 | 因此,根据我们的追踪要求(即观察栈地址),我们追踪基地址`esp`。
62 |
63 | 首先,我们先编写`gdbinit`,这样我们启动`debug`时就能一步到位开始调试了。
64 |
65 | ```bash
66 | target remote:1234
67 | file ../build/kernel.o
68 | set disassembly-flavor intel
69 | b setup_kernel
70 | c
71 | b 66
72 | c
73 | ```
74 |
75 | 启动虚拟机,开始调试。
76 |
77 | ```bash
78 | make debug
79 | ```
80 |
81 | 
82 |
83 | 在调用中断前,中断向量表中已经存储好了我们函数的位置。因此在这个系统调用函数中,函数做的事情只有
84 |
85 | 保护现场,调用`int 80`和还原现场。
86 |
87 | 我们重点观察`int 80`后发生了什么。
88 |
89 | 首先,根据我们的预测,因为我们此时处在内核环境中,因此我们现在的特权级别是0,也就是最高。即使我们转到了中断寄存器,我们的特权级别依然没变,因此栈的地址,**理应来说并不会发生改变**。
90 |
91 | 为了验证我们的猜想,我们停到那,然后用`i r`观察此时的`ebp`寄存器。
92 |
93 | 
94 |
95 | 可以看到,此时的`esp`是`0x7b98`。
96 |
97 | 我们执行到`int 80`之后直接`s`单步进入,发现我们进入了`asm_system_call_handler`函数。这是合理的。因为我们在`syscall`的初始化时,把`0x80`的中断处理函数替换成了它。
98 |
99 | ```cpp
100 | void SystemService::initialize()
101 | {
102 | memset((char *)system_call_table, 0, sizeof(int) * MAX_SYSTEM_CALL);
103 | // 代码段选择子默认是DPL=0的平坦模式代码段选择子,DPL=3,否则用户态程序无法使用该中断描述符
104 | interruptManager.setInterruptDescriptor(0x80, (uint32)asm_system_call_handler, 3);
105 | }
106 | ```
107 |
108 | 然后我们继续。
109 |
110 | 单步执行,再进行`i r`,我们得到了如下结果。
111 |
112 | 
113 |
114 | 可以发现,`esp`因为压了一个栈所以往低地址延展了而且只延展了一个`0x10`。根据代码前面压了一个十六位的寄存器值,我们可以得出结论:跳转时栈顶寄存器内容没有发生改变。
115 |
116 | 这正好符合我们的猜想。`esp`没有改变。
117 |
118 | 那如果是用户空间呢?用户空间的话就会改变吗?
119 |
120 | 为了证实这一点,我们将这个系统调用移植到`src3`中。
121 |
122 | ### 1.2.1 在实现了用户进程中的代码中编写系统调用
123 |
124 | 移植的过程和`src1`的处理方法如出一辙。只是我们不再在内核中调用,而是在进程中调用。
125 |
126 | 进程也是帮忙实现好的了,调用也较为简单。
127 |
128 | ```cpp
129 | void second_process()
130 | {
131 | asm_system_call(1);
132 | asm_halt();
133 | }
134 |
135 | void first_thread(void *arg)
136 | {
137 | printf("start process\n");
138 | programManager.executeProcess((const char *)first_process, 1);
139 | programManager.executeProcess((const char *)first_process, 1);
140 | programManager.executeProcess((const char *)first_process, 1);
141 | programManager.executeProcess((const char *)second_process, 1);
142 | asm_halt();
143 | }
144 | ```
145 |
146 | 最后运行的结果如下。可以看到符合我们的需求。
147 |
148 | 
149 |
150 | ### 1.2.2 观察`esp`是否发生改变
151 |
152 | 我们再度追踪。
153 |
154 | 我们知道,想要切换到另一个进程,需要等待时间片流转算法切换进程过去才能开始执行。
155 |
156 | 我们此次并不追踪进程的实现过程,因此我们直接在`first_process`打断点,从那里开始追踪。
157 |
158 | 因此需要将原先的`gdbinit`的`b 66`修改成子函数所在的行数再运行。
159 |
160 | ```bash
161 | target remote:1234
162 | file ../build/kernel.o
163 | set disassembly-flavor intel
164 | b setup_kernel
165 | c
166 | b 37
167 | c
168 | ```
169 |
170 | 编译运行之后,再度追踪到`int 80`的位置,观察此时的`ebp`。
171 |
172 | 
173 |
174 | 这个时候的`esp`是`0x8048fb8`。因为我们是在用户进程中,用户进程的特权级是3而中断后的特权级是0,这一次`esp`一定会发生改变。
175 |
176 | 我们执行到下一步再看寄存器的内容。
177 |
178 | 
179 |
180 | 跳转结束之后,`esp`的值骤变成`0xc0025748`。很显然,这不是原地址压了一个16位寄存器的结果,这是栈指针直接发生了改变。
181 |
182 | 这也符合我们的预测,`esp`跳转到了内核空间中的栈。
183 |
184 | 而且仔细观察这个栈的位置,是不是有点熟悉?
185 |
186 | 没错,这个栈指针正好位于虚拟地址的3~4GB的虚拟地址空间中,这正好也是内核空间的共享地址!据此我们可以完全确定,此时就是从特权级为3的内核空间跳转到了特权级为0的内核空间,内核接管了操作系统。
187 |
188 | 据此我们已经了解了栈的变化了。
189 |
190 | ## 1.3 TSS的功能
191 |
192 | 从上述的描述,我们已经知道,TSS能帮助CPU读取到特权值为0的栈。
193 |
194 | 那TSS是怎么被CPU读取到的?
195 |
196 | 其实我们手动创建了一个TSS,然后我们把TSS的地址传到了一个叫TR的寄存器中。
197 |
198 | TSS的修改都是在时钟中断发生时,中断处理函数切换进程的同时修改的。
199 |
200 | 我们没有办法直接观察TR寄存器的内容,但是gdb调试可以直接打印tss的内容,十分方便。因此我们可以追踪一下TSS的变化情况。
201 |
202 | 我们把`gdbinit`修改成如下。
203 |
204 | ```bash
205 | target remote:1234
206 | file ../build/kernel.o
207 | set disassembly-flavor intel
208 | b ProgramManager::activateProgramPage
209 | c
210 | ```
211 |
212 | 运行到`tss.esp0`发生改变的地方,我们观察到
213 |
214 | 
215 |
216 | 可以发现,`tss.esp0`的地址发生了改变。
217 |
218 | 我们看一看这个数字`0xc0025760`。正好是上面我们监测栈变化时附近的指针!
219 |
220 | 所以可以预见到,`tss`的读取是CPU自带的,而`tss`的更改则是我们自己给他进行赋值的。而为什么`tss.esp0`会发生这样的改变?因为我们给进程设计的PCB中就规定了进程第一页后面跟着特权值为0的栈!
221 |
222 | 这样就解释通了`tss`发挥作用的全流程。
223 |
224 | # Assignment 2
225 |
226 | 实现fork函数,并回答以下问题。
227 |
228 | - 请根据代码逻辑和执行结果来分析fork实现的基本思路。
229 | - 从子进程第一次被调度执行时开始,逐步跟踪子进程的执行流程一直到子进程从`fork`返回,根据gdb来分析子进程的跳转地址、数据寄存器和段寄存器的变化。同时,比较上述过程和父进程执行完`ProgramManager::fork`后的返回过程的异同。
230 | - 请根据代码逻辑和gdb来解释fork是如何保证子进程的`fork`返回值是0,而父进程的`fork`返回值是子进程的pid。
231 |
232 | ## 2.1 `fork`实现思路
233 |
234 | `fork`函数的实现,本身就是回答实验指导上的四个问题:
235 |
236 | 1. 如何实现父子进程的代码段共享?
237 | 2. 如何使得父子进程从相同的返回点开始执行?
238 | 3. 除代码段外,进程包含的资源有哪些?
239 | 4. 如何实现进程的资源在进程之间的复制?
240 |
241 | 现在在此做出回答:
242 |
243 | - 第一个问题,我们回想一下,每个进程的虚拟页表中,3~4GB空间都是用来和内核共享的。这意味着,进程与进程之间也可以通过这部分空间共享。而在上次的实验中,我们把代码段放在了这个共享空间中!那么这段空间就是父子进程的共享代码空间,这是天然共享的,源于巧妙的代码设计。
244 |
245 | - 第二个问题,我们先要理清楚整个创建子进程的流程。
246 |
247 | - 首先父进程暂停。
248 | - 父进程将数据段、栈段等其他资源复制到子进程中。
249 | - 进程创建完毕之后,如果我们要立刻执行子进程,那么先执行子进程。
250 | - 子进程执行完之后,返回给父进程暂停位置接手,继续执行。
251 |
252 | 关键在第四点,子进程怎么知道父进程在哪里暂停?
253 |
254 | 其实也很简单。关注第二点,**父进程将数据段、栈段等其他资源复制到子进程中**。这意味着,父进程的所有栈内容都原样复制过去了,只要子进程不清空栈,那么子进程执行完之后,栈剩余的内容就是父进程的内容了。
255 |
256 | 然后我们在Assignment 1中也提到了,进程切换时,3特权级栈保存了父进程在执行`int 0x80`后的逐步返回的返回地址。
257 |
258 | 因此,只要子进程不清空栈,无论是先执行父进程还是执行子进程完再返回父进程,**栈顶都存着父进程的逐次返回地址。**返回时是天然的返回到相同的返回点!
259 |
260 | - 第三个问题,根据第二个问题的分析我们也明白了,进程包含着栈(包括所有级别的栈段),PCB、虚拟地址池、页目录表、页表及其指向的物理页。
261 | - 第四个问题,考虑到进程与进程之间没有办法通过直接寻址找到另一个进程数据的物理地址(因为二级分页制度的存在,我们没法准确定位实际的物理页在哪里,只有CPU自动寻址),我们就在内核空间开一个页作为中转站,依靠这个页在父进程和子进程之间复制资源。
262 |
263 | 四个问题都解决完毕了,那么理解实验指导的代码也就变得轻松了起来。现在我们来跟踪整个创建流程。
264 |
265 | ## 2.2 跟踪子进程的创建流程
266 |
267 | 为了搞清楚fork是如何保证子进程的`fork`返回值是0,而父进程的`fork`返回值是子进程的pid,我们从建立进程开始追踪。
268 |
269 | 我们把函数定在进程调用`fork()`的地方。同时,我们在真正执行`fork()`的进程管理器中打上断点。如下所示。
270 |
271 | 
272 |
273 | 我们遇到一个函数就进入一个函数,这样一直运行下去。直到这一步。
274 |
275 | 
276 |
277 | 这一步是在`asm_system_call_handler`阶段。前面我们传入了函数号,因此这一步开始就进入了`syscall`部分。
278 |
279 | 
280 |
281 | 我们可以看到,这里就是调用进程管理器的`fork()`函数。我们进入这个函数继续追踪。
282 |
283 | 在函数内经历了这样的顺序。
284 |
285 | - 判断父进程是线程还是进程。如果是线程就拒绝执行。
286 | - 创建子进程,分配新的PCB。
287 |
288 | 然后我们来到了关键的复制函数。
289 |
290 | 
291 |
292 | 根据代码,这个复制进程干了如下几件事。
293 |
294 | - 复制0级栈
295 |
296 | - **手动将子进程的寄存器集合中的`eax`设置成0**。
297 |
298 | 
299 |
300 | 这是非常关键的一点,这意味着**我们把子进程的返回值设置成了0。至于为什么我会在后面讲述。**
301 |
302 | - 将子进程的栈按照启动进程的顺序放入,这一步是为了时间片流转算法切换的时候,能够跳转到启动进程的函数正常启动进程。
303 |
304 | - 将子进程的PCB的状态、优先级、进程名字都复制过去。注意,**我们还复制了父进程的PID**。这一步也是关键,为原路返回父进程提供的帮助。
305 |
306 | 
307 |
308 | - 复制用户虚拟地址池。
309 |
310 | - 将父进程的页表都复制过去。注意,这里也相当于把其他等级的栈也给继承过去了。
311 |
312 | - 结束
313 |
314 | 我们跳到函数的最后,看看退出之后到了哪里。
315 |
316 | 
317 |
318 | 返回到了`fork`中,我们注意!**这里我们返回了一个pid**。这是什么?
319 |
320 | 观察我们前面的语句,我们发现
321 |
322 | 
323 |
324 | **这就是为什么父进程会返回子进程的pid**。
325 |
326 | 我们运行完,再看看到哪里了。
327 |
328 | 
329 |
330 | 一路返回,到了`asm_system_call_handler`中。
331 |
332 | 逐步运行,直到我们看到这条指令。
333 |
334 | 
335 |
336 | 我们已经知道,`iret`指令完成以下操作:
337 |
338 | 1. 从栈中弹出返回地址(包含CS和EIP寄存器,或CS和IP寄存器,取决于处理器模式),将其放入程序计数器(EIP/IP)和代码段寄存器(CS)中。
339 | 2. 如果处理器在保护模式下运行,还会从栈中弹出标志寄存器(EFLAGS)的内容,并将其恢复到标志寄存器中。这包括中断标志(IF),它决定是否允许后续中断。
340 | 3. 如果在任务切换过程中,`iret`还会处理任务状态段(TSS)切换。
341 |
342 | 因此,`iret`这条指令不仅仅只是返回,还将切换优先级。
343 |
344 | 这里结束一步步退出,就完成了一次子进程的创建。
345 |
346 | 在父进程中,我们得到的子进程的`pid`,然后一路一路传下来,最终就得到了父进程的pid。
347 |
348 | 现在我们追踪子进程的执行过程。
349 |
350 | ## 2.3 跟踪子进程的执行流程
351 |
352 | 此时要切换到子进程,是靠时间中断函数切换的。
353 |
354 | 因此我们修改`gdbinit`,堵在`ProgramManager::schedule`门口,再度调试,运行到子进程切换过来的地步。
355 |
356 | 
357 |
358 | 我们连续两次`c`过去,然后关注下面这一个函数,开始追踪这一个函数。
359 |
360 | 
361 |
362 | (之所以连续两次`c`过去,因为我在前面的实验报告中提到过,这个时间片轮转调度是存在巨大的BUG的,只是恰好在本实验的内容中可以正常运行罢了。整个BUG的产生原因,以及实际运行时产生的偏差在我之前的实验报告中有很详细的讨论。)
363 |
364 | 进入函数运行完之后,我们很自然地切换到了`asm_start_process`函数中。
365 |
366 | 复习一下,我们的子进程0栈中有什么东西?
367 |
368 | 
369 |
370 | 我们现在知道了,这是把父进程的`ProcessStartStack`继承过去了。`ProcessStartStack`的设计是严格依照函数压栈的顺序放成员的,因此可以执行正确的复制。
371 |
372 | 然后我们再想想,这个时候的`eax`是什么?这个时候的栈指针是什么?
373 |
374 | **没错,到这里逻辑就通畅了。子进程此时继承的是运行到`fork`断点的父进程,我们没有清空父进程代码也没有清空子进程的栈。我们最开始更改了`eax`的值为0,这个寄存器是存函数返回值的,我们接下来的所有操作都没有操作过`eax`的值。**
375 |
376 | **所以子进程能回到父进程最开始断点一模一样的位置继续运行,所以子进程返回的`pid`是0。**
377 |
378 | 为了验证这一点,我们把`asm_start_process`执行完,看看运行到了哪里。
379 |
380 | 
381 |
382 | 正好是`int 0x80`后面。这是父进程复制完子进程之后返回的地方。在这里,除了`eax`的值是被人工改成了0,所有寄存器的值都是一模一样,包括返回地址。
383 |
384 | 因此,运行完这里,函数一定会返回到`setup`中的判断`pid`是否为0的函数中。
385 |
386 | 我们执行完看一看。
387 |
388 | 
389 |
390 | 没错,返回到了这里。
391 |
392 | 至此整个`fork`的流程都很清晰地展示了。
393 |
394 | # Assignment 3
395 |
396 | 实现wait函数和exit函数,并回答以下问题。
397 |
398 | - 请结合代码逻辑和具体的实例来分析exit的执行过程。
399 | - 请分析进程退出后能够隐式地调用exit和此时的exit返回值是0的原因。
400 | - 请结合代码逻辑和具体的实例来分析wait的执行过程。
401 | - 如果一个父进程先于子进程退出,那么子进程在退出之前会被称为孤儿进程。子进程在退出后,从状态被标记为`DEAD`开始到被回收,子进程会被称为僵尸进程。请对代码做出修改,实现回收僵尸进程的有效方法。
402 |
403 | ## 3.1 `exit`的实现原理
404 |
405 | 我在上述过程详细地讲述了如何在内核态和用户态切换,解释了一个进程的构造,完善了其内容中的各个特级栈段和页目录表页表,还创造出了一个方法新建一个子进程。
406 |
407 | 现在想要结束一个进程就比构造简单多了。我们只需要释放掉其所有占用的物理页内存和占用的虚拟地址池空间,只保留一个PCB并把进程设置成`DEAD`即可。
408 |
409 | 而且进程调用在本次实验中,是一旦结束,立刻释放,立刻切换下一个进程。
410 |
411 | 为了证实这一点,我们在中断处理函数中加入打印函数,然后观察该进程的效果。
412 |
413 | ```cpp
414 | int syscall_0(int first, int second, int third, int forth, int fifth)
415 | {
416 | printf("systerm call 0: %d, %d, %d, %d, %d\n",
417 | first, second, third, forth, fifth);
418 | return first + second + third + forth + fifth;
419 | }
420 |
421 | void first_process()
422 | {
423 | int pid = fork();
424 |
425 | if (pid == -1)
426 | {
427 | printf("can not fork\n");
428 | asm_halt();
429 | }
430 | else
431 | {
432 | if (pid)
433 | {
434 | printf("I am father\n");
435 | asm_halt();
436 | }
437 | else
438 | {
439 | printf("I am child, exit\n");
440 | }
441 | }
442 | }
443 |
444 | void second_thread(void *arg) {
445 | printf("thread exit\n");
446 | exit(0);
447 | }
448 |
449 | void first_thread(void *arg)
450 | {
451 |
452 | printf("start process\n");
453 | programManager.executeProcess((const char *)first_process, 1);
454 | programManager.executeThread(second_thread, nullptr, "second", 1);
455 | asm_halt();
456 | }
457 | ```
458 |
459 | 最后我们得到如下结果。
460 |
461 | 
462 |
463 | 我们仔细观察一下这些打印函数。
464 |
465 | - 理应来说,每次调用都应该先中断,再管理,最后打印线程,如第一个方括号。
466 | - 但是关注下面两个圆括号。一个是线程的显示退出,另一个是子进程的隐式退出。我们发现,每次一退出,**Schedule立刻被调用,之后才切换到中断。(正如之前的报告所说,切换到Schedule之所以没有立刻分配到新的进程是实验代码中实现的时间片流转算法本身存在BUG)**
467 |
468 | 符合我们的预期,退出之后立刻切换到了进程管理函数中。
469 |
470 | 要怎么实现隐式调用`exit`呢?
471 |
472 | 原理终究都是从栈里调用。执行一次`ret`决定返回到哪里的都是栈顶的元素决定的。也就是说,只要我们在一个进程开始之前,先往里按照调用进程的顺序压进去一个返回函数,那么当进程结束的时候就可以实现返回啦。
473 |
474 | 那怎么`ret`是0呢?观察`exit`,我们没有发现有对`ret`的赋值啊?
475 |
476 | 还是一个道理,根本上来看,一个函数的传参是由函数之后紧跟的几个栈的变量值决定的。
477 |
478 | 我们观察在加载进程时,把`exit`压进栈的时候还压了什么东西进去。
479 |
480 | 
481 |
482 | 没错,我们默认的传参就是0。
483 |
484 | 因此自然得到的返回值`ret`也是0。
485 |
486 | ## 3.2 `wait`的实现原理
487 |
488 | 通过前面的学习我们知道了,只要我们想让函数停在某个地方,调用`ProgramManager::schedule`立刻把下一个进程切换上来即可。
489 |
490 | `fork`通过这个将父进程停留在原本的位置,同样的,`wait`遇到了没有运行完的子进程时,我们在这个时候切换进程,就能把控制权给到下一个进程,让后面所有的子进程继续运行,直到回到父进程时,我们写了一个`while`来让寻找可释放的DEAD子进程不断重复。
491 |
492 | 同时我来解释一下`wait`中的判断逻辑。
493 |
494 | - 如果有一个子进程可以释放,那么直接释放掉,然后直接返回pid。
495 | - 如果没有子进程释放,我们需要判断是没有子进程了还是子进程都在运行,这是根据`flag`判断的。
496 | - 如果没有子进程了,就返回-1。
497 | - 如果都在运行,就切换进程。
498 |
499 | 具体体现在这个地方。
500 |
501 | 
502 |
503 | 而等待到所有进程结束的判断就写在`setup`中。我们在上面只有一种情况会返回`-1`。
504 |
505 | 
506 |
507 | 这样整个流程都打通了。我们使用一个判例进行测试。
508 |
509 | ```cpp
510 | int syscall_0(int first, int second, int third, int forth, int fifth)
511 | {
512 | printf("systerm call 0: %d, %d, %d, %d, %d\n",
513 | first, second, third, forth, fifth);
514 | return first + second + third + forth + fifth;
515 | }
516 |
517 | void first_process()
518 | {
519 | int pid = fork();
520 | int retval;
521 |
522 | if (pid)
523 | {
524 | pid = fork();
525 | if (pid)
526 | {
527 | while ((pid = wait(&retval)) != -1)
528 | {
529 | printf("wait for a child process, pid: %d, return value: %d\n", pid, retval);
530 | }
531 |
532 | printf("all child process exit, programs: %d\n", programManager.allPrograms.size());
533 |
534 | asm_halt();
535 | }
536 | else
537 | {
538 | uint32 tmp = 0xffffff;
539 | while (tmp)
540 | --tmp;
541 | printf("-CHILD 2 EXIT- pid: %d\n", programManager.running->pid);
542 | exit(123934);
543 | }
544 | }
545 | else
546 | {
547 | uint32 tmp = 0xffffff;
548 | while (tmp)
549 | --tmp;
550 | printf("-CHILD 1 EXIT- pid: %d\n", programManager.running->pid);
551 | exit(-123);
552 | }
553 | }
554 |
555 | void second_thread(void *arg)
556 | {
557 | printf("thread exit\n");
558 | //exit(0);
559 | }
560 |
561 | void first_thread(void *arg)
562 | {
563 |
564 | printf("start process\n");
565 | programManager.executeProcess((const char *)first_process, 1);
566 | programManager.executeThread(second_thread, nullptr, "second", 1);
567 | asm_halt();
568 | }
569 | ```
570 |
571 | 我们来解释一下这个代码运行起来的逻辑。
572 |
573 | - 首先我们第一次`fork`,产生第一个子进程。
574 | - 然后父进程进入判断,判断成功,再产生第二个子进程。
575 | - 父进程执行`wait`,开始等待。
576 | - `wait`函数检查到所有子进程都没结束,立刻切换到下一个进程。
577 | - 切换到第一个子进程,从最外层的判断开始(回忆一下,子进程可是能从父进程相同的返回点开始执行的),判断失败,进入释放部分。
578 | - 切换到第二个进程,从内层判断开始,判断失败,进入释放部分。
579 | - 父进程接手时两个子进程都没执行完,所以继续等待。
580 | - 两个子进程最后都执行完毕结束。
581 | - 父进程接手时发现可以回收,开始回收第一个,回收完之后判断还有一个,继续回收,最后退出`wait`,打印结束语句。
582 |
583 | 最后运行成功后结果如下。
584 |
585 | 
586 |
587 | 与我们的推理一致。
588 |
589 | ## 3.3 回收僵尸进程
590 |
591 | 我们先构造出会出现僵尸进程的情况。
592 |
593 | ```cpp
594 | void first_process()
595 | {
596 | int pid = fork();
597 | int retval;
598 |
599 | if (pid)
600 | {
601 | printf("MAIN EXIT.");
602 | }
603 | else
604 | {
605 | uint32 tmp = 0xffffff;
606 | while (tmp)
607 | --tmp;
608 | printf("-CHILD 1 EXIT- pid: %d\n", programManager.running->pid);
609 | exit(-123);
610 | }
611 | }
612 | ```
613 |
614 | 运行结果如下。
615 |
616 | 
617 |
618 | 可以看到主进程确实在子进程之前释放,造成了僵尸进程。
619 |
620 | 现在我们来回收僵尸进程。
621 |
622 | 首先既然是僵尸进程,那么`wait`的判断条件就放宽了,只要检测到PCB还有**非零**父亲信息就判定成还有子进程。
623 |
624 | 为了方便起见,我将`wait`函数直接修改成了一个专用于释放僵尸进程的函数。
625 |
626 | ```cpp
627 | int ProgramManager::wait(int *retval)
628 | {
629 | PCB *child;
630 | ListItem *item;
631 | bool interrupt, flag;
632 |
633 | while (true)
634 | {
635 | interrupt = interruptManager.getInterruptStatus();
636 | interruptManager.disableInterrupt();
637 |
638 | item = this->allPrograms.head.next;
639 |
640 | // 查找子进程
641 | flag = true;
642 | while (item)
643 | {
644 | child = ListItem2PCB(item, tagInAllList);
645 | if (child->parentPid)
646 | {
647 | flag = false;
648 | if (child->status == ProgramStatus::DEAD)
649 | {
650 | break;
651 | }
652 | }
653 | item = item->next;
654 | }
655 |
656 | if (item) // 找到一个可返回的子进程
657 | {
658 | if (retval)
659 | {
660 | *retval = child->retValue;
661 | }
662 |
663 | int pid = child->pid;
664 | releasePCB(child);
665 | interruptManager.setInterruptStatus(interrupt);
666 | printf("-RECYCLE ZOMBIE CHILD SUCCESS- pid=%d",pid);
667 | return pid;
668 | }
669 | else
670 | {
671 | if (flag) // 子进程已经返回
672 | {
673 |
674 | interruptManager.setInterruptStatus(interrupt);
675 | return -1;
676 | }
677 | else // 存在子进程,但子进程的状态不是DEAD
678 | {
679 | interruptManager.setInterruptStatus(interrupt);
680 | schedule();
681 | }
682 | }
683 | }
684 | }
685 | ```
686 |
687 | 加一个打印函数,验证是否成功回收。
688 |
689 | 然后我们自己新开一个线程,用来执行这个回收函数。毕竟父进程直接没了,我们自然要新开一个线程回收。
690 |
691 | ```cpp
692 | void second_thread(void *arg)
693 | {
694 | int retval;
695 | wait(&retval);
696 | }
697 | ```
698 |
699 | 然后我们运行观察结果。
700 |
701 | 
702 |
703 | 可以看到,最后僵尸进程回收成功了,且`pid`是对应的上的。
704 |
705 | # 结语
706 |
707 | 我们最终完成了所有实验内容。
708 |
709 | 然后我注意到,还有一个`src7`没有使用到。这个实验代码封装了一个`Shell`用来在进程内实现打印函数。
710 |
711 | 原理也是依靠利用中断切换优先级从而调用显存。
712 |
713 | 我执行了一下这段实验代码。
714 |
715 | 
716 |
717 | 这是一个`YatSenOS`的欢迎界面。看着这个界面感慨颇多,实验指导的制作者依靠一个`Shell`完成了这次实验课程颇有仪式感的谢幕。
718 |
719 | 在本次实验课程中,我学习到了很多操作系统的底层原理,学会了非常熟练地编写汇编代码,学会了很多编写大项目时所需要具备的知识,包括联合编译。当然最重要的是给了我一个入门操作系统的机会。
720 |
721 | 当然,操作系统是一个非常庞大的系统,本次实验也只是将最基本的内容传授了给我。即便如此,每一次的实验都带给我非常大的挑战,我需要投入大量的时间才能彻底理解每一行代码的含义。本次实验课程也许是我投入时间与精力最多的课程了。在学习的过程中,我能感受到实验指导的优秀超乎想象。
722 |
723 | 实验指导编写的内容非常非常详尽,每一个实验指导都是近乎两万字的超级教程,虽然存在很少的逻辑问题和代码问题,但是也正是因为指导老师和2019级学长们极度认真的态度,每一个复杂的实验才会变得更加易懂,也帮助我少走特别多弯路。每一个可能出现的细枝末节的问题,实验指导中都尽可能详细地给出了他们的解释。
724 |
725 | 考虑到参与者中大部分都是学生,我更加敬佩他们的努力与实力,更加感激他们做出的贡献。
726 |
727 | 在此致谢参与编写`YatSenOS`的指导老师和所有学长!
728 |
729 |
--------------------------------------------------------------------------------
/05_实验五实验报告.md:
--------------------------------------------------------------------------------
1 | # Assignment 1
2 |
3 | ## 1.1 使用宏定义的好处
4 |
5 | 在开始实验前我先补充一些个人思考。
6 |
7 | 在实验报告中,对于可变参数的相关函数,题中使用了**宏**定义。
8 |
9 | ```c
10 | #ifndef STDARG_H
11 | #define STDARG_H
12 |
13 | typedef char *va_list;
14 | #define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))
15 | #define va_start(ap, v) (ap = (va_list)&v + _INTSIZEOF(v))
16 | #define va_arg(ap, type) (*(type *)((ap += _INTSIZEOF(type)) - _INTSIZEOF(type)))
17 | #define va_end(ap) (ap = (va_list)0)
18 |
19 | #endif
20 | ```
21 |
22 | 我们在平常写小体量代码时很少使用宏定义,因此我花了些时间思考宏定义的优点。
23 |
24 | - **使代码逻辑更清晰**
25 |
26 | 宏,本质上,就是一段代码替换。
27 |
28 | 比如取一个宏做例子。
29 |
30 | ```c
31 | #define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))
32 | ```
33 |
34 | 当我们在代码中这么用时
35 |
36 | ```c
37 | char b = 'f';
38 | int a = _INTSIZEOF(b);
39 | ```
40 |
41 | 经过编译器之后,这个代码就变成了这个样子。
42 |
43 | ```c
44 | char b = 'f';
45 | int a = ((sizeof(b) + sizeof(int) - 1) & ~(sizeof(int) - 1));
46 | ```
47 |
48 | 这个用法看着和函数非常相似。那么宏相比函数有什么优越性呢?
49 |
50 | - **改变变量更加方便**
51 |
52 | 在使用函数时,为了达到在函数里修改变量的效果,一般都要传指针进去。
53 |
54 | 在c++中,出现了`&`这个符号,传参相对更加方便。
55 |
56 | 这么做是为了保护变量不改变。但是缺点是很容易搞混乱。
57 |
58 | **宏的优点正在于,因为是直接替换代码,因此变量修改不会有任何保护。也就不用绕路子写代码。**
59 |
60 | 具体说来,看一下这个宏。
61 |
62 | ```c
63 | #define va_arg(ap, type) (*(type *)((ap += _INTSIZEOF(type)) - _INTSIZEOF(type)))
64 | ```
65 |
66 | - 这个宏用于获取可变参数列表中的下一个参数值。`ap` 是指向参数列表的指针,`type` 是参数的类型。
67 | - 它先将 `ap` 指针按照参数 `type` 的大小向后移动,然后再减去 `type` 的大小,这样就得到了当前参数的地址,然后将这个地址转换成 `type` 类型的指针,并通过 `*` 运算符获取该地址处的值。
68 |
69 | `ap`是直接发生修改了。如果要用函数写的话,就得传`ap`指针的指针进去了,麻烦不少。
70 |
71 | 宏的使用能让代码变得更可读,且逻辑更简单清晰,不会像使用函数这样复杂。
72 |
73 | **因为宏的保护性不足,因此宏定义一般都很短。目的只是为了减少重复代码。**
74 |
75 | ## 1.2 完善printf函数
76 |
77 | ### 1.2.1 改正原来printf函数中不规范代码
78 |
79 | 在原来的`printf`代码中,有一段这样的`case`。
80 |
81 | ```c
82 | case 'd':
83 | case 'x':
84 | int temp = va_arg(ap, int);
85 |
86 | if (temp < 0 && fmt[i] == 'd')
87 | {
88 | counter += printf_add_to_buffer(buffer, '-', idx, BUF_LEN);
89 | temp = -temp;
90 | }
91 |
92 | temp = itos(number, temp, (fmt[i] == 'd' ? 10 : 16));
93 |
94 | for (int j = temp - 1; j >= 0; --j)
95 | {
96 | counter += printf_add_to_buffer(buffer, number[j], idx, BUF_LEN);
97 | }
98 | break;
99 | ```
100 |
101 | 这个代码有一处不规范的地方。
102 |
103 | ```c
104 | int temp = va_arg(ap, int);
105 | ```
106 |
107 | `temp`作为一个暂时性地变量,我们必须保证,在我们不再使用这个变量时,这个变量就释放掉。
108 |
109 | 因为很有可能,我们在后面会要再用到`temp`,但是我们忘记自己声明过了,从而报错。
110 |
111 | 
112 |
113 | 然而不仅如此,这段代码是在`switch`代码段里的。这就会出现一个更大的问题:
114 |
115 | 如果我在接下来的cases要用到temp变量
116 |
117 | - 如果我声明,编译器报错重复声明。
118 | - 如果我不声明,**temp是在case x中声明的,这意味着,如果不执行x,temp就不存在。如果执行x,那么我要执行我的case时就必须先执行x,直到声明完temp直接跳到我的case中执行。**这在c里面也是不被允许的。
119 |
120 | 
121 |
122 | 这个错误被称为**跨作用域错误**。
123 |
124 | 修改这个代码也不难,**只要给case x用大括号划定一个作用域,编译器就会在执行完这段作用域后自动释放掉temp。**
125 |
126 | 用大括号划定作用域是使用暂时性变量的良好手段。
127 |
128 | 修改后的代码如下:
129 |
130 | ```cpp
131 | case 'd':
132 | case 'x':{
133 | int temp = va_arg(ap, int);
134 |
135 | if (temp < 0 && fmt[i] == 'd')
136 | {
137 | counter += printf_add_to_buffer(buffer, '-', idx, BUF_LEN);
138 | temp = -temp;
139 | }
140 |
141 | itos(number, temp, (fmt[i] == 'd' ? 10 : 16));
142 |
143 | for (int j = 0; number[j]; ++j)
144 | {
145 | counter += printf_add_to_buffer(buffer, number[j], idx, BUF_LEN);
146 | }
147 | break;
148 | }
149 | ```
150 |
151 | ### 1.2.2 添加八进制数输出功能
152 |
153 | 写法如同十进制数和十六进制数的输出。因为已经完成了大部分的功能,因此代码逻辑非常清晰。
154 |
155 | 这个八进制数并不能输出负数。
156 |
157 | 只要在原来的`printf`函数段中的`switch`语句段中添加多一个case即可。注意暂时性变量的作用域。
158 |
159 | ```cpp
160 | case 'o':{
161 | int temp = va_arg(ap, int);
162 | itos(number, temp, 8);
163 | for(int j=0; number[j]; ++j){
164 | counter += printf_add_to_buffer(buffer, number[j], idx, BUF_LEN);
165 | }
166 | break;
167 | }
168 | ```
169 |
170 | 在主函数中我们添加如下测试代码。
171 |
172 | ```cpp
173 | printf("SoliTa_Test: \"Original->10 Octal-> %o\"", 10);
174 | ```
175 |
176 | 这一个代码我们期望输出`SoliTa_Test: "Original->10 Octal-> 12"`。
177 |
178 | 最后运行代码即可。为了方便测试,我将`makefile`中的`run`部分做了一些修改。
179 |
180 | ```makefile
181 | run:
182 | make
183 | qemu-system-i386 -hda $(RUNDIR)/hd.img -serial null -parallel stdio -no-reboot
184 | ```
185 |
186 | 这样我们只要打开`makefile`所在位置的终端,输入一句就能把编译和运行搞定。
187 |
188 | ```bash
189 | make run
190 | ```
191 |
192 | 最后虚拟机截图如下。
193 |
194 | 
195 |
196 | 可以看到结果很好地执行了。
197 |
198 | # Assignment 2
199 |
200 | ## 2.1 使用enum的好处
201 |
202 | `enum`作为枚举类型,其本质不过是一串标识,功能可以用一个`int`类型变量来替代。
203 |
204 | 使用`enum`最大的好处是代码变得直观可读。这种写法在一个需要多人接手的大项目中很有作用。
205 |
206 | ## 2.2 自行实现PCB
207 |
208 | 我添加了一个优先级,这个优先级是一个数字,能够反映这个进程的重要程度。
209 |
210 | ```cpp
211 | int priority;
212 | ```
213 |
214 | 为了反映这个优先级,我修改了一下程序,给出三个线程。
215 |
216 | ```cpp
217 | void third_thread(void *arg) {
218 | printf("PRIORITY--%d--pid %d name \"%s\": Hello World!\n",programManager.running->priority, programManager.running->pid, programManager.running->name);
219 |
220 | }
221 | void second_thread(void *arg) {
222 | printf("PRIORITY--%d--pid %d name \"%s\": Hello World!\n",programManager.running->priority, programManager.running->pid, programManager.running->name);
223 | }
224 |
225 | void first_thread(void *arg)
226 | {
227 | // 第1个线程不可以返回
228 | printf("PRIORITY--%d--pid %d name \"%s\": Hello World!\n",programManager.running->priority, programManager.running->pid, programManager.running->name);
229 | if (!programManager.running->pid)
230 | {
231 | programManager.executeThread(second_thread, nullptr, "second thread", 2);
232 | programManager.executeThread(third_thread, nullptr, "third thread", 2);
233 | }
234 | asm_halt();
235 | }
236 | ```
237 |
238 | 除此以外,为了反映出0进程以外的所有进程都运行完了,我适当修改了`ProgramManager`的代码,加入了一个判断条件。
239 |
240 | ```cpp
241 | List allPrograms; // 所有状态的线程/进程的队列
242 | List readyPrograms; // 处于ready(就绪态)的线程/进程的队列
243 | PCB *running; // 当前执行的线程
244 | int ProgramManagerflag; // 决定要不要打印停止命令
245 | ```
246 |
247 | 对`schedule`的`if (readyPrograms.size() == 0)`部分做适当补充。
248 |
249 | ```cpp
250 | if (readyPrograms.size() == 0)
251 | {
252 | interruptManager.setInterruptStatus(status);
253 | if(ProgramManagerflag == 0){
254 | printf("All thread (without pid 0) has been finished. System is sleep...\n");
255 | ProgramManagerflag = 1;
256 | }
257 | return;
258 | }
259 | ```
260 |
261 | 最后我们运行程序观察一下是否有打印出我们新添加的优先级。
262 |
263 | 
264 |
265 | 可以看到程序很好的运行了。
266 |
267 | # Assignment 3
268 |
269 | ## 3.1 运行原理
270 |
271 | 在系统进入保护模式之后,整个程序的运行流程如下。
272 |
273 | 
274 |
275 | 代码中实现的是时间片轮转算法。因此,触发中断后会运行一次中断处理函数。整个切换是这样执行的。
276 |
277 | 
278 |
279 | **整个中断执行的流程是自动的。因此,我们不能只追踪setup_kernel函数,我们还得追踪schedule函数才能理解整个进程切换。**
280 |
281 | 而中断处理函数是用来进行进程切换的。因此我们添加一个打印函数,指示进程切换的时间,方便调试。
282 |
283 | ```cpp
284 | // ProgramManager::schedule()中添加
285 | printf("pid %d is stop, and pid %d is continue...\n", cur->pid, next->pid);
286 | ```
287 |
288 | 然后我们在`setup_kernel`新添加两个进程,分别放在进程一中。
289 |
290 | ```cpp
291 | void third_thread(void *arg) {
292 | printf("pid %d name \"%s\": Hello World!\n", programManager.running->pid, programManager.running->name);
293 | while(1) {
294 |
295 | }
296 | }
297 | void second_thread(void *arg) {
298 | printf("pid %d name \"%s\": Hello World!\n", programManager.running->pid, programManager.running->name);
299 | }
300 |
301 | void first_thread(void *arg)
302 | {
303 | // 第1个线程不可以返回
304 | printf("pid %d name \"%s\": Hello World!\n", programManager.running->pid, programManager.running->name);
305 | if (!programManager.running->pid)
306 | {
307 | programManager.executeThread(second_thread, nullptr, "second thread", 1);
308 | programManager.executeThread(third_thread, nullptr, "third thread", 1);
309 | }
310 | asm_halt();
311 | }
312 | ```
313 |
314 | ## 3.2 追踪setup_kernel()
315 |
316 | 接下来我们追踪`setup`函数。
317 |
318 | 将`gdbinit`改成如下形式。
319 |
320 | ```bash
321 | target remote:1234
322 | file ../build/kernel.o
323 | set disassembly-flavor intel
324 | b setup_kernel
325 | c
326 | ```
327 |
328 | 在终端中打开debug。
329 |
330 | ```bash
331 | make debug
332 | ```
333 |
334 | 启动的界面如下。
335 |
336 | 
337 |
338 | 我们一直运行函数到倒数第二行,会发现,屏幕没有任何输出。
339 |
340 | 
341 |
342 | 再运行一步可以看到如下输出。
343 |
344 | 
345 |
346 | 可以看到,进程现在的切换为:进程1已经执行完毕,内存被释放了,只剩下进程0(不可释放)和进程2(里面写了无限循环函数)在不断切换。
347 |
348 | 这说明一件事:中断的产生是硬件产生的,我们没办法只追踪setup_kernel了解运作全貌。
349 |
350 | ## 3.3 追踪c_time_interrupt_handler()
351 |
352 | 我们修改`gdbinit`成如下代码。
353 |
354 | ```bash
355 | target remote:1234
356 | file ../build/kernel.o
357 | set disassembly-flavor intel
358 | b c_time_interrupt_handler
359 | c
360 | ```
361 |
362 | 进入debug。因为我们追踪的是中断处理函数,因此进入这个函数时一定发生了中断。
363 |
364 | 
365 |
366 | 可以看到,中断第一次发生的时候,进程一已经创建好了。在本次实验代码中,我们设定过十次才切换一次代码。
367 |
368 | 我们打好断点,不断继续运行,直到运行到分支判断中。
369 |
370 | 
371 |
372 | 进入函数。进行到这一步时
373 |
374 | ```cpp
375 | asm_switch_thread(cur, next); //断点位置
376 | printf("pid %d is stop, and pid %d is continue...\n", cur->pid, next->pid);
377 | ```
378 |
379 | 再进行一步,发现函数没有进行下去,而是直接跳到了第二次中断(没有打印切换函数)此时的输出结果如下。
380 |
381 | 
382 |
383 | 为了搞清楚这一步发生了什么,我们重新追踪,并且进入`asm_switch_thread()`函数,运行到末尾,开始观察。
384 |
385 | 
386 |
387 | 再运行一步,我们发现运行到了第二个函数中。这意味着线程切换成功。
388 |
389 | 
390 |
391 | 运行完这个函数之后,我们发现,运行直接跳转到了终止函数里。
392 |
393 | 
394 |
395 | 这是怎么做到的?
396 |
397 | 这个设计非常巧妙。
398 |
399 | ### 3.3.1 从进程跳转到终止函数的原理
400 |
401 | 我们已经知道x86芯片对栈是从高地址向低地址延展的。
402 |
403 | 而`ret`命令,根本上是栈指针往后退一位,然后读取内存中存储的地址,再跳转到对应的地址。
404 |
405 | 每一个c语言函数运行完之后,转化为汇编语言,最后都有一个`ret`指令。
406 |
407 | 然后我们回想一下,当初我们创建线程的时候,我们压了什么东西进栈里?
408 |
409 | ```cpp
410 | thread->stack = (int *)((int)thread + PCB_SIZE);
411 | thread->stack -= 7;
412 | thread->stack[0] = 0;
413 | thread->stack[1] = 0;
414 | thread->stack[2] = 0;
415 | thread->stack[3] = 0;
416 | thread->stack[4] = (int)function;
417 | thread->stack[5] = (int)program_exit;
418 | thread->stack[6] = (int)parameter;
419 | ```
420 |
421 | 没错,在函数后面的,就是终止函数`program_exit`!
422 |
423 | 因此我们在运行完function之后就会自动跳转到终止函数里。
424 |
425 | ### 3.3.2 继续追踪
426 |
427 | 终止函数中,我们设计如下:先把进程状态设置成`DEAD`,如果监测到该进程可以返回,就重新进入`schedule`函数中运行。
428 |
429 | 因此,运行终止函数之后,我们又进入了`schedule`函数中。
430 |
431 | 
432 |
433 | 这个函数里集成了释放内存的操作。
434 |
435 | 至此,线程1就被彻底杀死了,链表中这个线程被剔除掉,只剩下了线程0和线程2。
436 |
437 | 可以知道,此时的时钟中断还没到来,因为我们还没有再次触发进入中断的函数。
438 |
439 | 之后,和上面的流程一样,线程2接管,但是线程2是一个无线循环。
440 |
441 | 
442 |
443 | 进行到这一步就定死在这里,正在等待时钟中断。
444 |
445 | 不断运行下去,就等到了时钟中断。
446 |
447 | **这里其实反映了代码里存在的一个严重的逻辑问题。可以思考一下,这个看似正常运行的代码有什么隐患?我会在这一节的最后说明。**
448 |
449 | 
450 |
451 | **这就是为什么最后那个切换进程没有打印出来。因为函数最后进到了线程2,线程2进入了死循环(即一直运行不退出),直到下一次中断的到来,直接从头开始进行中断流程。**
452 |
453 | 再次运行到asm_switch_thread(),再缓步运行下去,我们发现,程序运行到了下一步。也就是说,`ret`指令返回到的地址就是打印函数的地方。
454 |
455 | 
456 |
457 | 可是,为什么这次就会返回到下一条指令呢?
458 |
459 | ### 3.3.2 能够执行完中断函数的原理
460 |
461 | 这个命令,是从线程2切换到线程0。
462 |
463 | 因此理应来说,函数返回之后,应该是跳到线程0中继续运行才对。
464 |
465 | **但是我们思考一下,当时线程0被换下去的时候,保存在栈里的东西到底是什么?**
466 |
467 |
468 |
469 | 观察这个图,我们知道:
470 |
471 | - 返回函数时,首先esp会把栈里的四个寄存器的值读进来。
472 | - 然后`ret`再指令读return value,得到下一条指令的运行值。
473 |
474 | 因此,无论是第一次创建的进程,还是被中断的进程,都是靠栈顶的第五个元素来判断。
475 |
476 | 问题来了,**线程0被换下来的时候,程序运行到哪里了?**
477 |
478 | 这就是整个设计最巧妙(**但也是最疑惑的地方,因为这个设计造成了上述提到的严重的逻辑错误**)的地方。
479 |
480 | - 首次执行时,我们切换进程的时候,**进入汇编函数时,编译器帮我们把最后执行的那一个指令的地址压进的栈。**
481 | - **那一条指令正好就是`asm_switch_thread`**
482 | - 因此,下一次再载入这个函数的时候,**这个进程直接从下一条指令开始运行。**
483 |
484 | 因此就能达到直接从下一条指令开始运行。
485 |
486 | **但是这个处理方法逻辑是很奇怪的。**
487 |
488 | - 第一点,这意味着,我们首次建立进程的时候,**中断函数没有执行完就被打断了。**我马上就会阐明这一点的危害性。
489 | - 第二点,这是否意味着,不仅是首次,每次**进程被奇数次调用时,中断都是被打断的?**我们马上就会来验证这一点。
490 |
491 | 这两点的存在,让整个代码的正确性得不到保证。
492 |
493 | ### 3.3.4 示例代码中存在的严重逻辑问题
494 |
495 | #### 3.3.4.1 时钟中断没有被屏蔽
496 |
497 | 回到3.3.2中的疑问。代码中存在什么问题吗?
498 |
499 | 也许你已经发现了。**首次创建进程2的时候,程序从中断一路执行到进程2的无限循环。但是中间没有任何代码打开了中断。**
500 |
501 | 这意味着,从原则上来说,**因为中断被禁止,进程2本来是不可能被切换出去的。**
502 |
503 | **但是实验结果就是切换出去了。**
504 |
505 | 这有严重的矛盾。唯一的解释是,**CLI指令没有关闭时钟中断。**
506 |
507 | 我重写了关中断的代码,为了验证是否是在关闭中断之后仍然响应了中断。最后我通过debug指令得到了如下的截图。
508 |
509 | 
510 |
511 | 符合我的猜想,**程序的关中断没有影响时钟中断。即使已经关掉了中断,中断还是发生了。**
512 |
513 | 虚拟机的时钟信号很长,因此这样的中断不影响代码正常运行。
514 |
515 | **但是如果程序的原子指令足够长,那么此时的时钟中断会直接切断原子指令的运行,可能造成程序崩溃。**
516 |
517 | 但是实验给我的程序有一个搞笑的地方:这个程序是建立在这个错误上才能运行的...如果我把时钟中断也给关了,那么线程调度就卡死了...
518 |
519 | 一种可行的解决方法是这样的:
520 |
521 | - 在调度函数中把时钟中断也给关掉
522 | - 然后在每一个线程最开始再把时钟中断打开
523 |
524 | #### 3.3.4.2 中断函数被打断
525 |
526 | 第一个问题说明中断没有被屏蔽。
527 |
528 | 那么,这说明,进程刚被创建的时候,**中断函数是被打断的。**
529 |
530 | 这意味着,压入栈的值,**不是这个函数运行到的地方,而是当时中断函数被打断的地方!**
531 |
532 | 然后之后的每次线程中断调度都是**从中断被打断的地方开始运行到程序->从程序被打断的地方开始运行中断程序然后中断被打断**。这样来回往复运行。
533 |
534 | 这显然和我们的设计不符,只是因为很凑巧,这个函数没有造成大问题罢了。
535 |
536 | 我们来修改原程序的代码来观察这一点。为了观察中断开始和中断结束,我们在schedule部分加上打印语句。
537 |
538 | ```cpp
539 | extern "C" void c_time_interrupt_handler()
540 | {
541 | PCB *cur = programManager.running;
542 |
543 | if (cur->ticks)
544 | {
545 | --cur->ticks;
546 | ++cur->ticksPassedBy;
547 | }
548 | else
549 | {
550 | printf("SCHEDULE-Interrupt is START.\n");
551 | programManager.schedule();
552 | printf("SCHEDULE-Interrupt is END.\n");
553 | }
554 | }
555 | ```
556 |
557 | 这样我们就能观察,时间片用完之后,中断到底执行完没有。
558 |
559 | 如果START的数量和END的数量是相同的,说明每一次中断都执行完了,反之,就说明有问题。
560 |
561 | 使用debug模式在中断函数打上断点,运行十次结果如下。
562 |
563 | 
564 |
565 | 问题已经很明显了:**只有一次START,但是没有END。说明中断真的被打断了。**
566 |
567 | 
568 |
569 | 再运行十次,我们发现:**START了两次,才有了一次END。根据我们前面的推理,这是0号线程的中断按顺序执行完了。这意味着,相邻的START和END是匹配不上的。**
570 |
571 | 多运行几次结果如下。
572 |
573 | 
574 |
575 | 无论怎么运行,START的数量永远比END多。这意味着,永远有一个中断处于被打断状态。这不是我们想要的结果。
576 |
577 | 其危害性在于,**上一个问题我们提到时钟中断没法被关闭,这个问题已经很严重了。**
578 |
579 | **但是在这里问题将变得更加严重。**
580 |
581 | #### 3.3.4.3 中断永远被关闭
582 |
583 | **仔细思考一下,如果一个中断没有被执行完,根据示例程序里线程管理程序的写法,是不是意味着,中断在此期间是一直关闭的?**
584 |
585 | (因为在程序中,关中断是直接CLI关闭的,但是恢复是靠一个变量state,存储着关闭前的状态,如果关闭之前是CLOSE(比如说,如果之前有一个中断是被打断的),那么即使这个中断运行完了,那么恢复之后的结果仍然是CLOSE。)
586 |
587 | **如果一直有一个中断被打断,就意味着,中断永远没有办法被打开!**
588 |
589 | 要验证这一点也很简单。我们在`schedule`函数的最后添加如下代码。
590 |
591 | ```cpp
592 | if(status == false) printf("Interrupt is still CLOSE.\n");
593 | if(status == true) printf("Interrupt has been OPEN.\n");
594 | ```
595 |
596 | 理应来说,如果中断都是正常运行的,那么在这个函数运行完毕之后,中断理应是打开的。
597 |
598 | 也就是一定都是OPEN才对。因为程序中没有对中断函数嵌套调用。
599 |
600 | 但是我们的运行结果是这样的。
601 |
602 | 
603 |
604 | **中断全部都是CLOSE。中断从来没有被打开过。**
605 |
606 | **一方面,时钟中断一直没有被关闭。另一方面,CLI关闭的那些中断永远没有办法被打开。**
607 |
608 | 问题变得非常严重。
609 |
610 | 要彻底改正这个问题需要重写中断程序,工作量不小。
611 |
612 | # Assignment 4
613 |
614 | 本次实验实现先进先出算法。
615 |
616 | ## 4.1 代码原理与展示
617 |
618 | 算法不需要使用时钟中断,因此我们把中断函数内容清空。
619 |
620 | ```cpp
621 | extern "C" void c_time_interrupt_handler()
622 | {
623 | }
624 | ```
625 |
626 | 先进先出算法核心是运行完之后才轮到下一个进程。因此我们只要在每一个进程后面跟一个修改过的`schedule`函数即可。
627 |
628 | `schedule`函数的实现如下。
629 |
630 | ```cpp
631 | void ProgramManager::schedule_2()
632 | {
633 | bool status = interruptManager.getInterruptStatus();
634 | interruptManager.disableInterrupt();
635 |
636 | if(readyPrograms.size() == 0){
637 | printf("All thread has been finished. System is sleep...\n");
638 | asm_halt();
639 | }
640 |
641 | if (running->status == ProgramStatus::DEAD)
642 | {
643 | releasePCB(running);
644 | }
645 |
646 | ListItem *item = readyPrograms.front();
647 | PCB *next = ListItem2PCB(item, tagInGeneralList);
648 | PCB *cur = running;
649 | next->status = ProgramStatus::RUNNING;
650 | running = next;
651 | readyPrograms.pop_front();
652 |
653 | asm_switch_thread(cur, next);
654 | printf("pid %d is finished, and pid %d is loaded and start to run.\n", cur->pid, next->pid);
655 |
656 |
657 | interruptManager.setInterruptStatus(status);
658 |
659 | }
660 | ```
661 |
662 | 这个算法把`if(readyPrograms.size() == 0)`调至最前,而且直接用`asm_halt()`堵死,正是为了让最后一个进程执行完之后就不再继续运行,防止后面的列表操作出现异常。
663 |
664 | ## 4.2 示例运行
665 |
666 | 我们使用如下的示例进程运行。
667 |
668 | ```cpp
669 | void third_thread(void *arg) {
670 | printf("PRIORITY--%d--pid %d name \"%s\": Hello World!\n",programManager.running->priority, programManager.running->pid, programManager.running->name);
671 | programManager.schedule_2();
672 | }
673 | void second_thread(void *arg) {
674 | printf("PRIORITY--%d--pid %d name \"%s\": Hello World!\n",programManager.running->priority, programManager.running->pid, programManager.running->name);
675 | programManager.schedule_2();
676 | }
677 |
678 | void first_thread(void *arg)
679 | {
680 | // 第1个线程不可以返回
681 | printf("PRIORITY--%d--pid %d name \"%s\": Hello World!\n",programManager.running->priority, programManager.running->pid, programManager.running->name);
682 | if (!programManager.running->pid)
683 | {
684 | programManager.executeThread(second_thread, nullptr, "second thread", 2);
685 | programManager.executeThread(third_thread, nullptr, "third thread", 2);
686 | }
687 | programManager.schedule_2();
688 | }
689 | ```
690 |
691 | 此时,进程是线性存在的,因此第0个进程直接消除了也是可以的。
692 |
693 | 运行结果如下。
694 |
695 | 
696 |
697 | 可以看到,程序执行了下来。
698 |
699 | 如果我们把第二个进程设置成永不退出,即
700 |
701 | ```cpp
702 | void second_thread(void *arg) {
703 | printf("PRIORITY--%d--pid %d name \"%s\": Hello World!\n",programManager.running->priority, programManager.running->pid, programManager.running->name);
704 | while(1){};
705 | programManager.schedule_2();
706 | }
707 | ```
708 |
709 | 程序最后运行的结果如下。
710 |
711 | 
712 |
713 | 进程会卡在第二个线程不动了。这也符合我们FIFO的期望,中断并没有干扰程序运行。
714 |
715 | ## 4.3 一点碎碎念
716 |
717 | 我们此时如果在每个进程结束前检查中断情况,即
718 |
719 | ```cpp
720 | void third_thread(void *arg) {
721 | printf("PRIORITY--%d--pid %d name \"%s\": Hello World!\n",programManager.running->priority, programManager.running->pid, programManager.running->name);
722 | if(interruptManager.getInterruptStatus() == true) printf("interrupt is open.\n");
723 | else printf("interrupt still close...");
724 | programManager.schedule_2();
725 | }
726 | void second_thread(void *arg) {
727 | printf("PRIORITY--%d--pid %d name \"%s\": Hello World!\n",programManager.running->priority, programManager.running->pid, programManager.running->name);
728 | if(interruptManager.getInterruptStatus() == true) printf("interrupt is open.\n");
729 | else printf("interrupt still close...");
730 |
731 |
732 | programManager.schedule_2();
733 |
734 | }
735 |
736 | void first_thread(void *arg)
737 | {
738 | // 第1个线程不可以返回
739 | printf("PRIORITY--%d--pid %d name \"%s\": Hello World!\n",programManager.running->priority, programManager.running->pid, programManager.running->name);
740 | if (!programManager.running->pid)
741 | {
742 | programManager.executeThread(second_thread, nullptr, "second thread", 2);
743 | programManager.executeThread(third_thread, nullptr, "third thread", 2);
744 | }
745 | if(interruptManager.getInterruptStatus() == true) printf("interrupt is open.\n");
746 | else printf("interrupt still close...");
747 | programManager.schedule_2();
748 | }
749 | ```
750 |
751 | 得到的结果是这样的。
752 |
753 | 
754 |
755 | 所有的中断都是打开的,这符合我们的期待。
756 |
757 | 说明演示代码里提供的时间片流转算法是真的有问题...
758 |
--------------------------------------------------------------------------------
/04_实验四实验报告.md:
--------------------------------------------------------------------------------
1 | > 声明:本实验报告中对复现任务的解释都是作者个人见解和对实验指导未解释部分的补充,同时也是作者的学习笔记,而非单纯对实验指导的复制粘贴。
2 |
3 | # Assignment 1
4 |
5 | ## 1.1 定义函数
6 |
7 | 本次实验要求使用三种语言来定义函数。最终调用的主程序是`cpp`文件。
8 |
9 | 整个函数的逻辑如下。
10 |
11 | - 函数`main`(CPP)
12 | - 调用函数`function_from_asm`(来自ASM)
13 | - 调用`function_from_C`(来自C)
14 | - 调用`function_from_CPP`(来自CPP)
15 |
16 | 我们从内到外定义函数。
17 |
18 | ### 1.1.1 c_func.c
19 |
20 | 定义`function_from_C`。
21 |
22 | ```c
23 | #include
24 |
25 | void function_from_C() {
26 | printf("This is a function from C.\n");
27 | }
28 | ```
29 |
30 | C语言编译时会保留其原来的函数名。因此不需要做过多操作。
31 |
32 | ### 1.1.2 cpp_func.cpp
33 |
34 | 定义`function_from_CPP`
35 |
36 | ```cpp
37 | #include
38 |
39 | extern "C" void function_from_CPP() {
40 | std::cout << "This is a function from C++." << std::endl;
41 | }
42 | ```
43 |
44 | **注:因为C++支持函数重载,因此正常情况下,编译后的函数名不再是原来的函数名。**
45 |
46 | 因此需要使用`extern "C"`指令,使其函数编译规则与C保持一致。
47 |
48 | ### 1.1.3 function_from_asm
49 |
50 | 定义`function_from_asm`
51 |
52 | ```asm
53 | [bits 32]
54 | global function_from_asm
55 | extern function_from_C
56 | extern function_from_CPP
57 |
58 | function_from_asm:
59 | call function_from_C
60 | call function_from_CPP
61 | ret
62 | ```
63 |
64 | 在这次的复现中,两个被调用的函数都没有返回值,所以暂且不用关心调用外部函数后返回值放在哪里。
65 |
66 | 同样,这个函数本身也没有返回值,因此这次不需要关注汇编函数返回值应该存放在哪里的问题。
67 |
68 | 如果这个函数本身有返回值,统一放在`eax`的位置。
69 |
70 | ### 1.1.4 main.cpp
71 |
72 | ```cpp
73 | #include
74 |
75 | extern "C" void function_from_asm();
76 |
77 | int main() {
78 | std::cout << "Call function from assembly." << std::endl;
79 | function_from_asm();
80 | std::cout << "Done." << std::endl;
81 | }
82 | ```
83 |
84 | 同样,需要使用`extern "C"`指令,使其函数编译规则与C保持一致。
85 |
86 | ## 1.2 Makefile
87 |
88 | makefile文件的本质是将所有指令打包,由编译器执行。
89 |
90 | 最后根据我们上述的文件,编写makefile文件。
91 |
92 | ```makefile
93 | main.out: main.o c_func.o cpp_func.o asm_func.o
94 | g++ -o main.out main.o c_func.o cpp_func.o asm_func.o -m32
95 |
96 | c_func.o: c_func.c
97 | gcc -o c_func.o -m32 -c c_func.c
98 |
99 | cpp_func.o: cpp_func.cpp
100 | g++ -o cpp_func.o -m32 -c cpp_func.cpp
101 |
102 | main.o: main.cpp
103 | g++ -o main.o -m32 -c main.cpp
104 |
105 | asm_func.o: asm_func.asm
106 | nasm -o asm_func.o -f elf32 asm_func.asm
107 |
108 | run:
109 | make ./main.out
110 | ./main.out
111 |
112 | clean:
113 | rm *.o
114 | ```
115 |
116 | 我们可以知道,最后合成的可执行文件后缀是`.o`。
117 |
118 | 上述的gcc指令在实验报告中已经有解释,我解释一下nasm中指令的含义。
119 |
120 | - **-o asm_func.o**:这部分指定了生成的目标文件的名称为 **asm_func.o**。
121 | - **-f elf32**:这部分指定了目标文件的格式。在这里,我们使用 **elf32** 格式,它是一种适用于 **Linux** 系统的目标文件格式。
122 | - **asm_func.asm**:汇编源代码文件的名称。
123 |
124 | 最后,如果我们没有定义`run`的话,程序若要在终端运行,需要两行指令。
125 |
126 | ```bash
127 | make ./main.out
128 | ./main.out
129 | ```
130 |
131 | 定义之后就只需要一条指令了。
132 |
133 | ```bash
134 | make run
135 | ```
136 |
137 | ## 1.3 执行结果
138 |
139 | 最后执行代码的截图如下。
140 |
141 | 
142 |
143 | 可以看到最后的代码结果和我们想象的一致。
144 |
145 | # Assignment 2
146 |
147 | 这次实验设计大项目。因此我们所有文件应该放在一个根目录下。
148 |
149 | 在进行复现前,实验指导中没有说明这个漂亮的文件目录是怎么生成的。
150 |
151 | ```shell
152 | ├── build
153 | │ └── makefile
154 | ├── include
155 | │ ├── asm_utils.h
156 | │ ├── boot.inc
157 | │ ├── os_type.h
158 | │ └── setup.h
159 | ├── run
160 | │ ├── gdbinit
161 | │ └── hd.img
162 | └── src
163 | ├── boot
164 | │ ├── bootloader.asm
165 | │ ├── entry.asm
166 | │ └── mbr.asm
167 | ├── kernel
168 | │ └── setup.cpp
169 | └── utils
170 | └── asm_utils.asm
171 | ```
172 |
173 | 这里给出方法。
174 |
175 | 首先安装`tree`库。
176 |
177 | ```bash
178 | sudo apt intstall tree
179 | ```
180 |
181 | 然后直接使用如下指令。
182 |
183 | ```bash
184 | tree
185 | ```
186 |
187 | 就能在终端中显示这个漂亮的文件目录。
188 |
189 | 
190 |
191 | 实验将各个文件分门别类。这在后期管理大型项目时非常有用。
192 |
193 | ## 2.1 实验思路
194 |
195 | 操作系统,亦或者说,内核本身只是一段可执行代码。内核的内容是什么,完全可以有我们自己定义。写完内核之后,代码仍然需要加载到内存中,才能运行。
196 |
197 | 因此为了加载内核,需要做以下几件事。
198 |
199 | - 编写好对应的`mbr`和`bootloader`。此时`bootloader`要做的事情只有把操作系统内核到内存中某一个位置并跳转。这里我们定义这个起始处为`0x20000`,定义内核在硬盘上的第六个扇区中,假设硬盘有200个扇区。
200 | - 操作系统内核编写对应程序。这里我们写一个`Hello World`程序。
201 |
202 | ## 2.2 bootloader程序
203 |
204 | `bootloader`的作用如下。
205 |
206 | - 先进入保护模式
207 | - 再加载内核
208 |
209 | 因此们只需要基于实验三的程序做修改即可。加载内核的过程还和`mbr`加载`bootloader`的过程几乎一模一样,只是起始地址,起始盘和加载次数发生了变化。
210 |
211 | `bootloader`的关键改动如下。其中的`asm_read_hard_disk`是直接复用`mbr`中的代码。
212 |
213 | ```asm
214 | mov eax, KERNEL_START_SECTOR
215 | mov ebx, KERNEL_START_ADDRESS
216 | mov ecx, KERNEL_SECTOR_COUNT
217 |
218 | load_kernel:
219 | push eax
220 | push ebx
221 | call asm_read_hard_disk ; 读取硬盘
222 | add esp, 8
223 | inc eax
224 | add ebx, 512
225 | loop load_kernel
226 |
227 | jmp dword CODE_SELECTOR:KERNEL_START_ADDRESS
228 | ```
229 |
230 | `boot.inc`新增以下内容。
231 |
232 | ```asm
233 | ; __________kernel_________
234 | KERNEL_START_SECTOR equ 6
235 | KERNEL_SECTOR_COUNT equ 200
236 | KERNEL_START_ADDRESS equ 0x20000
237 | ```
238 |
239 | ## 2.3 内核编写
240 |
241 | 内核是一组程序的集合。
242 |
243 | 因此我们可以用分体思想编写一个简单的内核。该内核的逻辑如下。
244 |
245 | - 内核接管。这里我们设置为跳转到`setup_kernel`程序。
246 | - 编写初始化内核的程序。这里使用汇编编写学号程序,练习外部链接。
247 |
248 | 内核接管程序如下。
249 |
250 | ```asm
251 | global enter_kernel
252 | extern setup_kernel
253 | enter_kernel:
254 | jmp setup_kernel
255 | ```
256 |
257 | 初始化内核程序如下。
258 |
259 | ```cpp
260 | #include "asm_utils.h"
261 |
262 | extern "C" void setup_kernel()
263 | {
264 | asm_hello_world();
265 | while(1) {
266 |
267 | }
268 | }
269 | ```
270 |
271 | ```cpp
272 | // "asm_utils.h"头文件定义
273 | #ifndef ASM_UTILS_H
274 | #define ASM_UTILS_H
275 |
276 | extern "C" void asm_hello_world();
277 |
278 | #endif
279 | ```
280 |
281 | 其中的学号程序编写如下。
282 |
283 | ```asm
284 | [bits 32]
285 |
286 | global asm_hello_world
287 |
288 | asm_hello_world:
289 | push eax
290 | xor eax, eax
291 |
292 | mov ah, 0x03 ;青色
293 | mov al, '2'
294 | mov [gs:2 * 0], ax
295 |
296 | mov al, '2'
297 | mov [gs:2 * 1], ax
298 |
299 | mov al, '3'
300 | mov [gs:2 * 2], ax
301 |
302 | mov al, '0'
303 | mov [gs:2 * 3], ax
304 |
305 | mov al, '5'
306 | mov [gs:2 * 4], ax
307 |
308 | mov al, '0'
309 | mov [gs:2 * 5], ax
310 |
311 | mov al, '5'
312 | mov [gs:2 * 6], ax
313 |
314 | mov al, '3'
315 | mov [gs:2 * 7], ax
316 |
317 | pop eax
318 | ret
319 | ```
320 |
321 | **这里不使用cpp程序直接编写的原因可能是因为虚拟机的屏幕映射问题。**
322 |
323 | ## 2.4 编译文件
324 |
325 | 首先我们理清楚我们要做什么。
326 |
327 | - 首先编译`mbr`和`bootloader`。
328 | - 然后编译内核代码。内核代码最后应该是**一个**可执行的二进制代码。
329 | - 先单独编译`entry.asm`,`asm_utils.asm`和`setup.cpp`。
330 | - 再链接三个文件。
331 | - 我们要求内核代码的起始位置必须是我们期望的地址。因此在重定位时,我们必须将`entry.obj`的位置放在指令首位。
332 | - 最后生成的程序是直接塞在内存中运行的,因此我们生成的文件应该是`bin`文件,而不是一个完整的`o`文件。但是完整的`o`可以用来调试使用。使用`.o`文件进行调试可比用`.bin`文件调试舒服太多了。因此我们可以同时生成两个文件。
333 | - 把三个代码塞进内存中。
334 | - 启动虚拟机。
335 |
336 | ### 2.4.1 编译 mbr 和 bootloader
337 |
338 | ```bash
339 | nasm -o mbr.bin -f bin -I../include/ ../src/boot/mbr.asm
340 | nasm -o bootloader.bin -f bin -I../include/ ../src/boot/bootloader.asm
341 | ```
342 |
343 | ### 2.4.2 编译内核代码
344 |
345 | 除了最终我们生成的内核代码应该是不夹杂任何多余项的`bin`文件,在进行链接时的文件必须是可重定位文件,在Linux中,这种文件格式是`ELF32`。
346 |
347 | 首先编译三个单独的文件。
348 |
349 | ```bash
350 | nasm -o entry.obj -f elf32 ../src/boot/entry.asm
351 | nasm -o asm_utils.o -f elf32 ../src/utils/asm_utils.asm
352 | g++ -g -Wall -march=i386 -m32 -nostdlib -fno-builtin -ffreestanding -fno-pic -I../include -c ../src/kernel/setup.cpp
353 | ```
354 |
355 | 各个参数在实验指导中讲解得很详细。
356 |
357 | 然后链接成用来写进内存的`bin`文件。
358 |
359 | ```bash
360 | ld -o kernel.bin -melf_i386 -N entry.obj setup.o asm_utils.o -e enter_kernel -Ttext 0x00020000 --oformat binary
361 | ```
362 |
363 | 当然,为了调试,我们可以生成`o`文件。
364 |
365 | ```bash
366 | ld -o kernel.o -melf_i386 -N entry.obj setup.o asm_utils.o -e enter_kernel -Ttext 0x00020000
367 | ```
368 |
369 | ### 2.4.3 把 bin 文件塞进内存中
370 |
371 | ```bash
372 | dd if=mbr.bin of=../run/hd.img bs=512 count=1 seek=0 conv=notrunc
373 | dd if=bootloader.bin of=../run/hd.img bs=512 count=5 seek=1 conv=notrunc
374 | dd if=kernel.bin of=../run/hd.img bs=512 count=200 seek=6 conv=notrunc
375 | ```
376 |
377 | ### 2.4.4 启动虚拟机
378 |
379 | ```bash
380 | qemu-system-i386 -hda ../run/hd.img -serial null -parallel stdio -no-reboot
381 | ```
382 |
383 | ## 2.5 Makefile
384 |
385 | 如果直接在终端中执行2.4中的所有指令,会有以下麻烦。
386 |
387 | - 操作过于繁琐。
388 | - 每一个指令需要在不同的文件夹下打开终端来执行。
389 |
390 | 因此我们需要编写Makefile简化这个过程。
391 |
392 | 我们需要Makefile包含如下内容。
393 |
394 | - 指明编译使用到的的工具包。
395 | - 指明文件所在的位置。
396 | - 指明用到的所有文件
397 | - 指明编译完的文件应该放在哪里。
398 | - 指明执行的指令
399 |
400 | 因此,在Makefile的开头,我们可以像定义常量一样,定义一些路径和工具。所有的定义本质上都是定义字符串。
401 |
402 | ```makefile
403 | C_COMPLIER = gcc
404 | CXX_COMPLIER = g++
405 | CXX_COMPLIER_FLAGS = -g -Wall -march=i386 -m32 -nostdlib -fno-builtin -ffreestanding -fno-pic
406 | LINKER = ld
407 |
408 | SRCDIR = ../src
409 | RUNDIR = ../run
410 | BUILDDIR = build
411 | INCLUDE_PATH = ../include
412 |
413 | CXX_SOURCE += $(wildcard $(SRCDIR)/kernel/*.cpp)
414 | CXX_OBJ += $(CXX_SOURCE:$(SRCDIR)/kernel/%.cpp=%.o)
415 |
416 | ASM_SOURCE += $(wildcard $(SRCDIR)/utils/*.asm)
417 | ASM_OBJ += $(ASM_SOURCE:$(SRCDIR)/utils/%.asm=%.o)
418 |
419 | OBJ += $(CXX_OBJ)
420 | OBJ += $(ASM_OBJ)
421 | ```
422 |
423 | 下面的六条指令需要逐条解释。
424 |
425 | - `CXX_SOURCE += $(wildcard $(SRCDIR)/kernel/*.cpp)`
426 | - 这一行代码使用`wildcard`函数查找指定目录下所有的`.cpp`文件,并将它们的路径保存在`CXX_SOURCE`变量中。通常`$(SRCDIR)/kernel/*.cpp`表示源代码目录下的`kernel`子目录中的所有`.cpp`文件。
427 | - `CXX_OBJ += $(CXX_SOURCE:$(SRCDIR)/kernel/%.cpp=%.o)`
428 | - 这一行代码将`CXX_SOURCE`中的每个`.cpp`文件路径替换为对应的`.o`文件路径,并将结果保存在`CXX_OBJ`变量中。例如,如果`CXX_SOURCE`包含了`kernel/foo.cpp`,那么对应的`.o`文件路径会是`kernel/foo.o`。
429 | - `ASM_SOURCE += $(wildcard $(SRCDIR)/utils/*.asm)`
430 | - 这一行代码和第一行类似,只不过它是用于汇编语言(Assembly)文件,查找指定目录下所有的`.asm`文件,并将它们的路径保存在`ASM_SOURCE`变量中。
431 | - `ASM_OBJ += $(ASM_SOURCE:$(SRCDIR)/utils/%.asm=%.o)`
432 | - 这一行代码和第二行类似,将`ASM_SOURCE`中的每个`.asm`文件路径替换为对应的`.o`文件路径,并将结果保存在`ASM_OBJ`变量中。
433 | - `OBJ += $(CXX_OBJ)`
434 | - 这一行代码将C++源文件编译生成的目标文件路径添加到总的目标文件列表`OBJ`中。
435 | - `OBJ += $(ASM_OBJ)`
436 | - 这一行代码将汇编语言源文件编译生成的目标文件路径也添加到总的目标文件列表`OBJ`中,以便最终链接。
437 |
438 | 接下来,我们就可以开始把所有终端指令转化为makefile指令。
439 |
440 | ```makefile
441 | build : mbr.bin bootloader.bin kernel.bin kernel.o
442 | dd if=mbr.bin of=$(RUNDIR)/hd.img bs=512 count=1 seek=0 conv=notrunc
443 | dd if=bootloader.bin of=$(RUNDIR)/hd.img bs=512 count=5 seek=1 conv=notrunc
444 | dd if=kernel.bin of=$(RUNDIR)/hd.img bs=512 count=145 seek=6 conv=notrunc
445 | # nasm的include path有一个尾随/
446 | ```
447 |
448 | ```makefile
449 | mbr.bin : $(SRCDIR)/boot/mbr.asm
450 | $(ASM_COMPILER) -o mbr.bin -f bin -I$(INCLUDE_PATH)/ $(SRCDIR)/boot/mbr.asm
451 | ```
452 |
453 | ```makefile
454 | bootloader.bin : $(SRCDIR)/boot/bootloader.asm
455 | $(ASM_COMPILER) -o bootloader.bin -f bin -I$(INCLUDE_PATH)/ $(SRCDIR)/boot/bootloader.asm
456 | ```
457 |
458 | ```makefile
459 | entry.obj : $(SRCDIR)/boot/entry.asm
460 | $(ASM_COMPILER) -o entry.obj -f elf32 $(SRCDIR)/boot/entry.asm
461 | ```
462 |
463 | ```makefile
464 | kernel.bin : entry.obj $(OBJ)
465 | $(LINKER) -o kernel.bin -melf_i386 -N entry.obj $(OBJ) -e enter_kernel -Ttext 0x00020000 --oformat binary
466 | ```
467 |
468 | ```makefile
469 | kernel.o : entry.obj $(OBJ)
470 | $(LINKER) -o kernel.o -melf_i386 -N entry.obj $(OBJ) -e enter_kernel -Ttext 0x00020000
471 | ```
472 |
473 | ```makefile
474 | asm_utils.o : $(SRCDIR)/utils/asm_utils.asm
475 | $(ASM_COMPILER) -o asm_utils.o -f elf32 $(SRCDIR)/utils/asm_utils.asm
476 | ```
477 |
478 | 上述对asm编译中都是逐条编译,逐条改写。
479 |
480 | 我们可以直接使用上述我们定义好的常量直接对多个文件批处理编译,更加快速。
481 |
482 | 如下指令,我对所有的C和Cpp文件进行批处理。
483 |
484 | ```makefile
485 | $(CXX_OBJ):
486 | $(CXX_COMPLIER) $(CXX_COMPLIER_FLAGS) -I$(INCLUDE_PATH) -c $(CXX_SOURCE)
487 | ```
488 |
489 | 最后我们写上启动程序和其他处理程序。
490 |
491 | ```makefile
492 | clean:
493 | rm -f *.o* *.bin
494 | ```
495 |
496 | ```makefile
497 | run:
498 | qemu-system-i386 -hda $(RUNDIR)/hd.img -serial null -parallel stdio -no-reboot
499 | ```
500 |
501 | ```makefile
502 | debug:
503 | qemu-system-i386 -S -s -parallel stdio -hda $(RUNDIR)/hd.img -serial null&
504 | @sleep 1
505 | gnome-terminal -e "gdb -q -tui -x $(RUNDIR)/gdbinit"
506 | ```
507 |
508 | 注:这里的debug不是必要的。其中有一些陌生的指令,我在此解释一下。
509 |
510 | - `qemu-system-i386 -S -s -parallel stdio -hda $(RUNDIR)/hd.img -serial null&`
511 |
512 | 这一行命令使用 QEMU 模拟器来运行编译后的程序。具体参数的含义如下:
513 |
514 | - `-S`:表示在启动时暂停 CPU,等待调试器连接。
515 | - `-s`:表示启动一个 GDB 服务器,监听本地端口 1234,以便 GDB 连接。
516 | - `-parallel stdio`:表示将 QEMU 的标准输入和标准输出重定向到当前终端。
517 | - `-hda $(RUNDIR)/hd.img`:指定硬盘镜像文件路径,`$(RUNDIR)/hd.img`是一个变量,可能是在Makefile中定义的。
518 | - `-serial null`:表示将串行端口重定向到`null`,即关闭串行输出。
519 | - `&`:表示在后台运行 QEMU。
520 |
521 | - `@sleep 1`
522 |
523 | 这一行命令暂停当前的 Makefile 执行,等待一秒钟,以确保 QEMU 已经完全启动。
524 |
525 | - `gnome-terminal -e "gdb -q -tui -x $(RUNDIR)/gdbinit"`
526 |
527 | 这一行命令打开一个新的 GNOME 终端,并在其中启动 GDB 调试器。具体参数的含义如下:
528 |
529 | - `gnome-terminal`:表示启动 GNOME 终端。
530 | - `-e "gdb -q -tui -x $(RUNDIR)/gdbinit"`:表示在终端中执行 GDB 调试器,并指定了一些参数:
531 | - `-q`:表示以“安静模式”启动 GDB,即不显示欢迎信息。
532 | - `-tui`:表示启动 GDB 的文本用户界面。
533 | - `-x $(RUNDIR)/gdbinit`:指定一个 GDB 初始化脚本文件。
534 |
535 | 这里出现了一个新的文件:`gdbinit`。
536 |
537 | 在此开一个小节介绍一下这个文件。
538 |
539 | ### 2.5.1 gdbinit
540 |
541 | `gdbinit` 是 GDB(GNU Debugger)的初始化文件。
542 |
543 | 它可以包含一系列 GDB 命令,用于配置 GDB 的行为、设置断点、显示变量等。
544 |
545 | 当你启动 GDB 调试器时,它会尝试加载名为 `.gdbinit` 的文件,如果不存在,它会尝试加载 `~/.gdbinit` 文件(用户主目录下的 `.gdbinit` 文件)。可以通过 `-x` 选项在启动 GDB 时指定特定的初始化文件。
546 |
547 | 以下是一些常见命令:
548 |
549 | - `break` 或 `b`:设置断点。
550 | - `run` 或 `r`:运行被调试的程序。
551 | - `print` 或 `p`:打印变量的值。
552 | - `step` 或 `s`:逐行执行代码,如果有函数调用则进入函数内部。
553 | - `next` 或 `n`:逐行执行代码,不进入函数内部。
554 | - `continue` 或 `c`:继续执行程序直到下一个断点。
555 | - `list` 或 `l`:显示源代码。
556 | - `info`:显示关于程序状态的信息,如断点、栈帧等。
557 | - `set`:设置 GDB 的参数。
558 |
559 | 如果需要启动图形界面化的调试界面,并且方便地对照源代码的话,`gdbinit` 文件是必须要编写的,而且需要自己手动编写。
560 |
561 | 在本次实验中,这个文件编写的内容如下。
562 |
563 | ```bash
564 | target remote:1234
565 | file ../build/kernel.o
566 | set disassembly-flavor intel
567 | set architecture i386
568 | ```
569 |
570 | ## 2.6 运行结果
571 |
572 | 最后我们在`makefile`文件的目录下运行。
573 |
574 | ```bash
575 | make
576 | make run
577 | ```
578 |
579 | 注:make指令是编译的意思,不可缺少。
580 |
581 | 最后执行的截图如下。
582 |
583 | 
584 |
585 | 也很好的运行了。
586 |
587 | # Assignment 3
588 |
589 | 复习一下内核的作用。
590 |
591 | - 内核接管。这里我们设置为跳转到`setup_kernel`程序。
592 | - 编写初始化内核的程序。
593 |
594 | 内核接管之后的模式是保护模式。保护模式下中断需要自己初始化。
595 |
596 | 因此本节我们来初始化中断描述符表IDT。
597 |
598 | 和段选择子的逻辑差不多,中断描述符表也是集中按顺序放置的,位置任意,起始地址有一个叫IDTR的寄存器存储。
599 |
600 | IDTR也是一个四十八位的寄存器。前一个字代表数目,后两个字代表地址。
601 |
602 | 
603 |
604 | 一个中断描述符有八个字节,也就是64位,四个字。
605 |
606 | 
607 |
608 | 因此我们可以算出,256个中断描述符,是2048个字节。
609 |
610 | 因此表界限是`2048-1=2047`。
611 |
612 | 我们这次设定IDT的32位基地址是`0x8880`。
613 |
614 | 整个中断的处理逻辑如下:
615 |
616 | + CPU 检查是否有中断信号。
617 | + CPU根据中断向量号到IDT中取得处理这个向量的中断描述符。
618 | + CPU根据中断描述符中的段选择符到 GDT 中找到相应的段描述符。
619 | + CPU 根据特权级的判断设定即将运行程序的栈地址。
620 | + CPU保护现场。
621 | + CPU跳转到中断服务程序的第一条指令开始处执行。
622 | + 中断服务程序运行。
623 | + 中断服务程序处理完成,使用iret返回。
624 |
625 | 开头带有CPU的流程都是CPU自动完成的。这意味着我们要实现的功能并不多。
626 |
627 | 我们已经学习过混合编程了。这次我们用c或者cpp实现大部分功能。也称之为软件中断。
628 |
629 | ## 3.1 实验思路
630 |
631 | 写代码前我们依然要理一理我们应该干什么。
632 |
633 | 为了方便整个模块的管理,我们可以使用cpp的语言特性定义一个“中断处理模块”,名字叫`InterruptManager`。
634 |
635 | ```cpp
636 | #ifndef INTERRUPT_H
637 | #define INTERRUPT_H
638 |
639 | #include "os_type.h"
640 |
641 | class InterruptManager
642 | {
643 | };
644 |
645 | #endif
646 | ```
647 |
648 | 这个模块要有以下的内容。
649 |
650 | - 变量:IDT的起始位置
651 | - 函数:构造函数(调用初始化IDT的函数)
652 | - 函数:初始化IDT
653 | - 给IDT初始化起始地址
654 | - 把IDT的起始地址送给IDTR
655 | - 把每一个中断描述符压进IDT中
656 | - 函数:中断描述符初始化
657 |
658 | ```cpp
659 | private:
660 | // IDT起始地址
661 | uint32 *IDT;
662 |
663 | public:
664 | InterruptManager();
665 | // 初始化
666 | void initialize();
667 | // 设置中断描述符
668 | // index 第index个描述符,index=0, 1, ..., 255
669 | // address 中断处理程序的起始地址
670 | // DPL 中断描述符的特权级
671 | void setInterruptDescriptor(uint32 index, uint32 address, byte DPL);
672 | ```
673 |
674 | 为了保证我们的c语言思路能够和汇编语言思路对的上,我们可以将变量重命名,将正确的变量格式对应到汇编风格中常用到的变量。
675 |
676 | ```cpp
677 | #ifndef OS_TYPE_H
678 | #define OS_TYPE_H
679 |
680 | // 类型定义
681 | typedef unsigned char byte;
682 | typedef unsigned char uint8;
683 |
684 | typedef unsigned short uint16;
685 | typedef unsigned short word;
686 |
687 | typedef unsigned int uint32;
688 | typedef unsigned int uint;
689 | typedef unsigned int dword;
690 |
691 | #endif
692 | ```
693 |
694 | 同样的,我们也可以定义好对应的常量。
695 |
696 | ```c
697 | #ifndef OS_CONSTANT_H
698 | #define OS_CONSTANT_H
699 |
700 | #define IDT_START_ADDRESS 0x8880
701 | #define CODE_SELECTOR 0x20
702 |
703 | #endif
704 | ```
705 |
706 | ## 3.2 初始化IDT函数
707 |
708 | 这个函数应该是`InterruptManager::initialize`。
709 |
710 | 根据实验思路设立的三步走
711 |
712 | - 给IDT初始化起始地址
713 | - 把IDT的起始地址送给IDTR
714 | - 把每一个中断描述符压进IDT中
715 |
716 | 可以写出如下代码。
717 |
718 | ```cpp
719 | void InterruptManager::initialize()
720 | {
721 | // 初始化IDT
722 | IDT = (uint32 *)IDT_START_ADDRESS;
723 | asm_lidt(IDT_START_ADDRESS, 256 * 8 - 1);
724 |
725 | for (uint i = 0; i < 256; ++i)
726 | {
727 | setInterruptDescriptor(i, (uint32)asm_unhandled_interrupt, 0);
728 | }
729 | }
730 | ```
731 |
732 | - 第一步很好理解。
733 |
734 | - 第二步,C语言不能实现我们的要求。我们需要用汇编实现这个函数
735 |
736 | 注:所有用到的汇编函数我们都可以直接写在同一个文件中。
737 |
738 | 这个函数若要实现应该如此。
739 |
740 | ```asm
741 | ASM_IDTR dw 0
742 | dd 0
743 | ;void asm_lidt(uint32 start, uint16 limit)
744 | asm_lidt:
745 | push ebp
746 | mov ebp, esp
747 | push eax
748 |
749 | mov eax, [ebp + 4 * 3]
750 | mov [ASM_IDTR], ax
751 | mov eax, [ebp + 4 * 2]
752 | mov [ASM_IDTR + 2], eax
753 | lidt [ASM_IDTR] ;把IDT的起始地址送给IDTR
754 |
755 | pop eax
756 | pop ebp
757 | ret
758 | ```
759 |
760 | 这里必须要说明,如果一个汇编函数要被C或者C++调用,且这个函数有输入值和返回值,应该按照如下方式定义函数。
761 |
762 | ```asm
763 | function_from_asm:
764 | ; 假设这个函数是int function_from_C(int arg1, int arg2);
765 | push ebp
766 | mov ebp, esp
767 |
768 | ; 下面通过ebp引用函数参数
769 | ; [ebp + 4 * 0]是之前压入的ebp值
770 | ; [ebp + 4 * 1]是返回地址
771 | ; [ebp + 4 * 2]是arg1
772 | ; [ebp + 4 * 3]是arg2
773 | ; 返回值需要放在eax中
774 | ; 所有在这个程序段内的压栈都一定要记得弹栈
775 | ; ...
776 |
777 | pop ebp
778 | ret
779 | ```
780 |
781 | - 第三步:`setInterruptDescriptor`这个函数是初始化中断描述符的函数。我们现在就来定义它。
782 |
783 | ## 3.3 中断描述符初始化
784 |
785 | 我们仔细看看中断描述符应该怎么写。
786 |
787 | 
788 |
789 | + 段选择子:中断程序所在段的选择子。
790 | + 偏移量:中断程序的代码在中断程序所在段的偏移位置。
791 | + P位:段存在位。 0表示不存在,1表示存在。
792 | + DPL:特权级描述。 0-3 共4级特权,特权级从0到3依次降低。
793 | + D位: D=1表示32位代码,D=0表示16位代码。
794 | + 保留位:保留不使用。
795 |
796 | 根据上述的代码,我们开始着手编写中断描述符。
797 |
798 | - **第一个32位**
799 |
800 | 高16位填写段选择子。因此,我们填写代码段选择子。`CODE_SELECTOR`在我们实验三中定义是`0x20`。
801 |
802 | 低16位填写偏移量的低16位。有一个巧妙的代码方法,就是把`address`和`0x0000ffff`做与运算,就能得到低16位了。
803 |
804 | - **第二个32位**
805 |
806 | 高十六位的写法也一样,不过改成`0xffff0000`。
807 |
808 | 低十六位我们根据上面的需求编码即可。
809 |
810 | 最后写成的代码如下。
811 |
812 | ```cpp
813 | // 设置中断描述符
814 | // index 第index个描述符,index=0, 1, ..., 255
815 | // address 中断处理程序的起始地址
816 | // DPL 中断描述符的特权级
817 | void InterruptManager::setInterruptDescriptor(uint32 index, uint32 address, byte DPL)
818 | {
819 | IDT[index * 2] = (CODE_SELECTOR << 16) | (address & 0xffff);
820 | IDT[index * 2 + 1] = (address & 0xffff0000) | (0x1 << 15) | (DPL << 13) | (0xe << 8);
821 | }
822 | ```
823 |
824 | ## 3.4 代码组合
825 |
826 | 最后我们可以把代码组合起来。
827 |
828 | ```cpp
829 | #include "interrupt.h"
830 | #include "os_type.h"
831 | #include "os_constant.h"
832 | #include "asm_utils.h"
833 |
834 | InterruptManager::InterruptManager()
835 | {
836 | initialize();
837 | }
838 |
839 | void InterruptManager::initialize()
840 | {
841 | // 初始化IDT
842 | IDT = (uint32 *)IDT_START_ADDRESS;
843 | asm_lidt(IDT_START_ADDRESS, 256 * 8 - 1);
844 |
845 | for (uint i = 0; i < 256; ++i)
846 | {
847 | setInterruptDescriptor(i, (uint32)asm_unhandled_interrupt, 0);
848 | }
849 |
850 | }
851 |
852 | void InterruptManager::setInterruptDescriptor(uint32 index, uint32 address, byte DPL)
853 | {
854 | // 中断描述符的低32位
855 | IDT[index * 2] = (CODE_SELECTOR << 16) | (address & 0xffff);
856 | // 中断描述符的高32位
857 | IDT[index * 2 + 1] = (address & 0xffff0000) | (0x1 << 15) | (DPL << 13) | (0xe << 8);
858 | }
859 | ```
860 |
861 | 仔细看,address对应的应该是每一个中断程序的入口才对。这里我们把所有中断程序设置为同一个是为了方便。
862 |
863 | 那这个中断程序我们总得定义。因此我们还需要在汇编函数库里加多一个函数。
864 |
865 | ```asm
866 | global asm_unhandled_interrupt
867 |
868 | ASM_UNHANDLED_INTERRUPT_INFO db 'Sorry!Halt!'
869 | db 0
870 |
871 | ; void asm_unhandled_interrupt()
872 | asm_unhandled_interrupt:
873 | cli
874 | mov esi, ASM_UNHANDLED_INTERRUPT_INFO
875 | xor ebx, ebx
876 | mov ah, 0x03
877 | .output_information:
878 | cmp byte[esi], 0
879 | je .end
880 | mov al, byte[esi]
881 | mov word[gs:bx], ax
882 | inc esi
883 | add ebx, 2
884 | add ah, 0x00010010
885 | jmp .output_information
886 | .end:
887 | jmp $
888 | ```
889 |
890 | 这个函数一目了然是答应一行字符串用的。注意,`cli`指令值得是关中断。
891 |
892 | 我在此改成了打印一行炫彩字符串(按实验报告的建议)
893 |
894 | 最后我们只要修改setup函数就可以了。
895 |
896 | ```cpp
897 | #include "asm_utils.h"
898 | #include "interrupt.h"
899 |
900 | // 中断管理器
901 | InterruptManager interruptManager;
902 |
903 | extern "C" void setup_kernel()
904 | {
905 | // 中断处理部件
906 | interruptManager.initialize();
907 |
908 | // 尝试触发除0错误
909 | int a = 1 / 0;
910 |
911 | // 死循环
912 | asm_halt();
913 | }
914 | ```
915 |
916 | 在上面我们已经知道,一旦触发一个错误,CPU会根据对应的索引号(CPU自带)自动执行中断程序。因此触发中断的方法非常简单:制造错误。这里我们触发除0错误。
917 |
918 | ## 3.5 运行
919 |
920 | 我们发现新增的文件只有C++文件,然后我们原来的Makefile文件就已经做了对C和C++文件的批处理了!因此我们完全不用修改我们的Makefile文件。
921 |
922 | 直接编译运行得到结果如下。
923 |
924 | 
925 |
926 | 可以看到,确实输出了炫彩字符串,说明中断被触发了。
927 |
928 | # Assignment 4
929 |
930 | 本次我们要实现硬件中断。
931 |
932 | 为了实现时钟中断,我们需要扩写中断模块。
933 |
934 | ```cpp
935 | class InterruptManager
936 | {
937 | private:
938 | uint32 *IDT; // IDT起始地址
939 |
940 | uint32 IRQ0_8259A_MASTER; // 主片中断起始向量号
941 | uint32 IRQ0_8259A_SLAVE; // 从片中断起始向量号
942 |
943 | public:
944 | InterruptManager();
945 | void initialize();
946 | // 设置中断描述符
947 | // index 第index个描述符,index=0, 1, ..., 255
948 | // address 中断处理程序的起始地址
949 | // DPL 中断描述符的特权级
950 | void setInterruptDescriptor(uint32 index, uint32 address, byte DPL);
951 |
952 | // 开启时钟中断
953 | void enableTimeInterrupt();
954 | // 禁止时钟中断
955 | void disableTimeInterrupt();
956 | // 设置时钟中断处理函数
957 | void setTimeInterrupt(void *handler);
958 |
959 | private:
960 | // 初始化8259A芯片
961 | void initialize8259A();
962 | };
963 | ```
964 |
965 | ## 4.1 初始化 8259A 芯片
966 |
967 | 8259A芯片有较为严格的初始化过程,而且使用方法比较特殊。下面的代码都是按照严格的声明顺序进行声明的,原理在实验指导中写得很清楚了。
968 |
969 | 我们要做的只是不断传参数。
970 |
971 | ```cpp
972 | void InterruptManager::initialize8259A()
973 | {
974 | // ICW 1
975 | asm_out_port(0x20, 0x11);
976 | asm_out_port(0xa0, 0x11);
977 | // ICW 2
978 | IRQ0_8259A_MASTER = 0x20;
979 | IRQ0_8259A_SLAVE = 0x28;
980 | asm_out_port(0x21, IRQ0_8259A_MASTER);
981 | asm_out_port(0xa1, IRQ0_8259A_SLAVE);
982 | // ICW 3
983 | asm_out_port(0x21, 4);
984 | asm_out_port(0xa1, 2);
985 | // ICW 4
986 | asm_out_port(0x21, 1);
987 | asm_out_port(0xa1, 1);
988 |
989 | // OCW 1 屏蔽主片所有中断,但主片的IRQ2需要开启
990 | asm_out_port(0x21, 0xfb);
991 | // OCW 1 屏蔽从片所有中断
992 | asm_out_port(0xa1, 0xff);
993 | }
994 | ```
995 |
996 | 传参数这个事情本身也是C语言没办法完成的,因此我们用汇编写这个功能,放进汇编代码库文件中。
997 |
998 | ```asm
999 | ; void asm_out_port(uint16 port, uint8 value)
1000 | asm_out_port:
1001 | push ebp
1002 | mov ebp, esp
1003 |
1004 | push edx
1005 | push eax
1006 |
1007 | mov edx, [ebp + 4 * 2] ; port
1008 | mov eax, [ebp + 4 * 3] ; value
1009 | out dx, al
1010 |
1011 | pop eax
1012 | pop edx
1013 | pop ebp
1014 | ret
1015 | ```
1016 |
1017 | 既然都写了传参数的函数了,写个读参数的函数以免万一吧。
1018 |
1019 | ```asm
1020 | ; void asm_in_port(uint16 port, uint8 *value)
1021 | asm_in_port:
1022 | push ebp
1023 | mov ebp, esp
1024 |
1025 | push edx
1026 | push eax
1027 | push ebx
1028 |
1029 | xor eax, eax
1030 | mov edx, [ebp + 4 * 2] ; port
1031 | mov ebx, [ebp + 4 * 3] ; *value
1032 |
1033 | in al, dx
1034 | mov [ebx], al
1035 |
1036 | pop ebx
1037 | pop eax
1038 | pop edx
1039 | pop ebp
1040 | ret
1041 | ```
1042 |
1043 | ## 4.2 时钟中断处理函数
1044 |
1045 | 计算机中有一个芯片`8253`会在从通电开始就源源不断自动产生时钟中断信号。
1046 |
1047 | 我们要做的只有根据这个信号写函数。
1048 |
1049 | 时钟中断是第0位。因此我们的开中断和关中断函数很直观。
1050 |
1051 | ```cpp
1052 | void InterruptManager::enableTimeInterrupt()
1053 | {
1054 | uint8 value;
1055 | // 读入主片OCW
1056 | asm_in_port(0x21, &value);
1057 | // 开启主片时钟中断,置0开启
1058 | value = value & 0xfe;
1059 | asm_out_port(0x21, value);
1060 | }
1061 |
1062 | void InterruptManager::disableTimeInterrupt()
1063 | {
1064 | uint8 value;
1065 | asm_in_port(0x21, &value);
1066 | // 关闭时钟中断,置1关闭
1067 | value = value | 0x01;
1068 | asm_out_port(0x21, value);
1069 | }
1070 | ```
1071 |
1072 | ## 4.3 通过时间中断实现某些功能的函数
1073 |
1074 | 我们这次希望时间中断开启之后,向屏幕输出字符告诉用户这个时间中断一共开启了多少次。
1075 |
1076 | 也是借这次机会,我们来规范化一下输出屏幕的程序。之前为了输出屏幕我们需要一次次输出,实在不够优雅。我们此次封装一个可用的print函数。
1077 |
1078 | ### 4.3.1 封装一个能用的屏幕输出模块
1079 |
1080 | 我们写下我们想要的功能。
1081 |
1082 | ```cpp
1083 | #ifndef STDIO_H
1084 | #define STDIO_H
1085 |
1086 | #include "os_type.h"
1087 |
1088 | class STDIO
1089 | {
1090 | private:
1091 | uint8 *screen;
1092 |
1093 | public:
1094 | STDIO();
1095 | // 初始化函数
1096 | void initialize();
1097 | // 打印字符c,颜色color到位置(x,y)
1098 | void print(uint x, uint y, uint8 c, uint8 color);
1099 | // 打印字符c,颜色color到光标位置
1100 | void print(uint8 c, uint8 color);
1101 | // 打印字符c,颜色默认到光标位置
1102 | void print(uint8 c);
1103 | // 移动光标到一维位置
1104 | void moveCursor(uint position);
1105 | // 移动光标到二维位置
1106 | void moveCursor(uint x, uint y);
1107 | // 获取光标位置
1108 | uint getCursor();
1109 |
1110 | public:
1111 | // 滚屏
1112 | void rollUp();
1113 | };
1114 |
1115 | #endif
1116 | ```
1117 |
1118 | 光标处理函数不能使用之前用过的中断,因为实模式的中断函数都需要自己定义。
1119 |
1120 | 因此对光标的操作,我们需要直接对端口操作。
1121 |
1122 | 与光标读写相关的端口为`0x3d4`和`0x3d5`。
1123 |
1124 | 具体的操作如下。
1125 |
1126 | ```cpp
1127 | void STDIO::moveCursor(uint position)
1128 | {
1129 | if (position >= 80 * 25)
1130 | {
1131 | return;
1132 | }
1133 |
1134 | uint8 temp;
1135 |
1136 | // 处理高8位
1137 | temp = (position >> 8) & 0xff;
1138 | asm_out_port(0x3d4, 0x0e);
1139 | asm_out_port(0x3d5, temp);
1140 |
1141 | // 处理低8位
1142 | temp = position & 0xff;
1143 | asm_out_port(0x3d4, 0x0f);
1144 | asm_out_port(0x3d5, temp);
1145 | }
1146 |
1147 | uint STDIO::getCursor()
1148 | {
1149 | uint pos;
1150 | uint8 temp;
1151 |
1152 | pos = 0;
1153 | temp = 0;
1154 | // 处理高8位
1155 | asm_out_port(0x3d4, 0x0e);
1156 | asm_in_port(0x3d5, &temp);
1157 | pos = ((uint)temp) << 8;
1158 |
1159 | // 处理低8位
1160 | asm_out_port(0x3d4, 0x0f);
1161 | asm_in_port(0x3d5, &temp);
1162 | pos = pos | ((uint)temp);
1163 |
1164 | return pos;
1165 | }
1166 | ```
1167 |
1168 | 其他函数的实现基本都是之前实验的内容了。
1169 |
1170 | 滚屏函数也只是把下一行的函数放在上一行而已。
1171 |
1172 | ### 4.3.2 中断功能函数
1173 |
1174 | 我们通过调用函数的方法嵌套调用中断功能函数。
1175 |
1176 | ```cpp
1177 | void InterruptManager::setTimeInterrupt(void *handler)
1178 | {
1179 | setInterruptDescriptor(IRQ0_8259A_MASTER, (uint32)handler, 0);
1180 | }
1181 | ```
1182 |
1183 | 然后就可以编写功能函数了。
1184 |
1185 | ```cpp
1186 | // 中断处理函数
1187 | extern "C" void c_time_interrupt_handler()
1188 | {
1189 | // 清空屏幕
1190 | for (int i = 0; i < 80; ++i)
1191 | {
1192 | stdio.print(0, i, ' ', 0x07);
1193 | }
1194 |
1195 | // 输出中断发生的次数
1196 | ++times;
1197 | char str[] = "interrupt happend: ";
1198 | char number[10];
1199 | int temp = times;
1200 |
1201 | // 将数字转换为字符串表示
1202 | for(int i = 0; i < 10; ++i ) {
1203 | if(temp) {
1204 | number[i] = temp % 10 + '0';
1205 | } else {
1206 | number[i] = '0';
1207 | }
1208 | temp /= 10;
1209 | }
1210 |
1211 | // 移动光标到(0,0)输出字符
1212 | stdio.moveCursor(0);
1213 | for(int i = 0; str[i]; ++i ) {
1214 | stdio.print(str[i]);
1215 | }
1216 |
1217 | // 输出中断发生的次数
1218 | for( int i = 9; i > 0; --i ) {
1219 | stdio.print(number[i]);
1220 | }
1221 | }
1222 | ```
1223 |
1224 | ## 4.4 中断封装
1225 |
1226 | **不要忘记,如果不返回EOI,芯片不会再接受中断。同时,C语言不会帮你恢复现场,也不会帮你开中断。**
1227 |
1228 | 因此在中断的最后,我们不能忘记处理好这三个问题。
1229 |
1230 | 这三个问题都需要靠汇编直接处理。
1231 |
1232 | ```asm
1233 | asm_time_interrupt_handler:
1234 | pushad
1235 |
1236 | nop ; 否则断点打不上去
1237 | ; 发送EOI消息,否则下一次中断不发生
1238 | mov al, 0x20
1239 | out 0x20, al
1240 | out 0xa0, al
1241 |
1242 | call c_time_interrupt_handler
1243 |
1244 | popad
1245 | iret
1246 | ```
1247 |
1248 | ```asm
1249 | ; void asm_enable_interrupt()
1250 | asm_enable_interrupt:
1251 | sti
1252 | ret
1253 | ```
1254 |
1255 | ## 4.5 运行代码
1256 |
1257 | 最后我们直接执行代码看看效果。
1258 |
1259 | 
1260 |
1261 | ## 4.6 编写自己的中断功能函数
1262 |
1263 | ```cpp
1264 | extern "C" void c_time_interrupt_handler()
1265 | {
1266 | // 清空屏幕
1267 | for (int i = 0; i < 80; ++i)
1268 | {
1269 | stdio.print(0, i, ' ', 0x07);
1270 | }
1271 |
1272 | ++times;
1273 | times = times%6;
1274 | char name[6] = {'S','o','l','i','T','a'};
1275 | int temp = times;
1276 |
1277 |
1278 | // 移动光标到(0,0)输出字符
1279 | stdio.moveCursor(0);
1280 |
1281 | for( int i = 0; i < 6; ++i ) {
1282 | if(i != times) stdio.print(' ');
1283 | else stdio.print(name[i]);
1284 | }
1285 | }
1286 | ```
1287 |
1288 | 这个函数按照实验报告的要求做了一个跑马灯。跑马灯的字符串是我的网络ID“SoliTa”。
1289 |
1290 | 修改之后运行如下。在此截取两个画面,证明跑马灯的结果是正确的。
1291 |
1292 | 
1293 |
1294 | 
1295 |
--------------------------------------------------------------------------------