Experiment 33 of operating system truth restoration experiment record: realizing system call wait and exit

Posted by barbgd on Wed, 09 Feb 2022 14:56:03 +0100

Experiment 33 of operating system truth restoration experiment record: realizing system call wait and exit

1.wait, exit, orphan process, zombie process

exit is called by the subprocess. On the surface, its function is to make the subprocess finish running and pass the return value to the kernel. In essence, the kernel recovers all resources of the subprocess except the pcb page behind the scenes.

wait is called by the parent process. On the surface, its function is to make the parent process block itself until the child process calls exit to get the return value of the child process. In essence, the kernel will wake up the parent process and recycle the pcb of the child process after passing the return value of the child process to the parent process behind the scenes.

An orphan process is a child process whose life cycle has not ended and exit has not been called, but the parent process ends ahead of time, so all resources of the child process cannot be recycled, so it is called an orphan process. In Linux, orphan processes are adopted by init process, which is the parent process of all orphan processes and manages the resource recovery of all orphan processes.

A zombie process is a child process that calls exit. All other resources except pcb are recycled. At the same time, the return value is also passed to the kernel, but the parent process does not call wait. In this way, the pcb of the zombie process will remain in the kernel and occupy the pid. When the number of zombie processes is very large, the operating system will have no pid to allocate, resulting in the failure of loading the process.
The solution is ps -ef to find the ppid of all processes in Z status and kill -9 parent processes.

2. Basic code

thread.h supplementary process exit status value member variable

struct task_struct{

	int8_t exit_status;
	uint32_t stack_magic;

memory. Free of C_ a_ phy_ page

/* According to the physical page box address pg_phy_addr clears 0 in the bitmap of the corresponding memory pool without changing the page table*/
void free_a_phy_page(uint32_t pg_phy_addr) {
   struct pool* mem_pool;
   uint32_t bit_idx = 0;
   if (pg_phy_addr >= user_pool.phy_addr_start) {
      mem_pool = &user_pool;
      bit_idx = (pg_phy_addr - user_pool.phy_addr_start) / PG_SIZE;
   } else {
      mem_pool = &kernel_pool;
      bit_idx = (pg_phy_addr - kernel_pool.phy_addr_start) / PG_SIZE;
   bitmap_set(&mem_pool->pool_bitmap, bit_idx, 0);

Accept the physical page frame address, clear 0 in the bitmap of the corresponding memory pool, and do not change the page table.

thread.c increase

Previously, only the pid function was allocated, and the pid function was not released

The process is create d and pid can be allocated during initialization
The fork or pid process is also allocated

But the pid function is not released.

The following is pid pool and pid bitmap to manage pid

/* pid Bitmap, supporting 1024 PIDs at most */
uint8_t pid_bitmap_bits[128] = {0};

/* pid pool */
struct pid_pool {
   struct bitmap pid_bitmap;  // pid bitmap
   uint32_t pid_start;	      // Initial pid
   struct lock pid_lock;      // Assign pid lock

/* Initialize pid pool */
static void pid_pool_init(void) { 
   pid_pool.pid_start = 1;
   pid_pool.pid_bitmap.bits = pid_bitmap_bits;
   pid_pool.pid_bitmap.btmp_bytes_len = 128;

/* Assign pid */
static pid_t allocate_pid(void) {
   int32_t bit_idx = bitmap_scan(&pid_pool.pid_bitmap, 1);
   bitmap_set(&pid_pool.pid_bitmap, bit_idx, 1);
   return (bit_idx + pid_pool.pid_start);

/* Release pid */
void release_pid(pid_t pid) {
   int32_t bit_idx = pid - pid_pool.pid_start;
   bitmap_set(&pid_pool.pid_bitmap, bit_idx, 0);

thread_init Called in pid_pool_init();

The following is to release the pcb and page table of the process

thread. Thread of C_ exit,pid_check,pid2thread

/* Recycle thread_over and remove them from the scheduling queue */
void thread_exit(struct task_struct* thread_over, bool need_schedule) {
   /* Ensure that the schedule is called during shutdown */
   thread_over->status = TASK_DIED;

   /* If thread_ If the over is not the current thread, it may still be in the ready queue and deleted from it */
   if (elem_find(&thread_ready_list, &thread_over->general_tag)) {
   if (thread_over->pgdir) {     // If it is a process, recycle the page table of the process
      mfree_page(PF_KERNEL, thread_over->pgdir, 1);

   /* From all_ thread_ Remove this task from the list */
   /* Recycle the page where the pcb is located. The pcb of the main thread is not in the heap and crosses the */
   if (thread_over != main_thread) {
      mfree_page(PF_KERNEL, thread_over, 1);

   /* Return pid */

   /* If the next round of scheduling is required, schedule will be called actively */
   if (need_schedule) {
      PANIC("thread_exit: should not be here\n");

/* pid of comparison task */
static bool pid_check(struct list_elem* pelem, int32_t pid) {
   struct task_struct* pthread = elem2entry(struct task_struct, all_list_tag, pelem);
   if (pthread->pid == pid) {
      return true;
   return false;

/* Find the pcb according to the pid. If found, return the pcb; otherwise, return NULL */
struct task_struct* pid2thread(int32_t pid) {
   struct list_elem* pelem = list_traversal(&thread_all_list, pid_check, pid);
   if (pelem == NULL) {
      return NULL;
   struct task_struct* thread = elem2entry(struct task_struct, all_list_tag, pelem);
   return thread;

3. Implement wait and exit system calls

wait_ exit. Release of C_ prog_ resource

/* Free user process resources: 
 * 1 Corresponding physical page in page table
 * 2 Virtual memory pool in physical page box
 * 3 Close open files */
static void release_prog_resource(struct task_struct* release_thread) {
   uint32_t* pgdir_vaddr = release_thread->pgdir;
   uint16_t user_pde_nr = 768, pde_idx = 0;
   uint32_t pde = 0;
   uint32_t* v_pde_ptr = NULL;	    // v stands for var, and the function pde_ptr distinction

   uint16_t user_pte_nr = 1024, pte_idx = 0;
   uint32_t pte = 0;
   uint32_t* v_pte_ptr = NULL;	    // Add v to represent var and function pte_ptr distinction

   uint32_t* first_pte_vaddr_in_pde = NULL;	// Used to record the address of the 0th pte in pde
   uint32_t pg_phy_addr = 0;

   /* Reclaim the page box of user space in the page table */
   while (pde_idx < user_pde_nr) {
      v_pde_ptr = pgdir_vaddr + pde_idx;
      pde = *v_pde_ptr;
      if (pde & 0x00000001) {   // If the p-bit of the page directory entry is 1, it indicates that there may be page table entries under the page directory entry
	 first_pte_vaddr_in_pde = pte_ptr(pde_idx * 0x400000);	  // The memory capacity represented by a page table is 4M, i.e. 0x400000
	 pte_idx = 0;
	 while (pte_idx < user_pte_nr) {
	    v_pte_ptr = first_pte_vaddr_in_pde + pte_idx;
	    pte = *v_pte_ptr;
	    if (pte & 0x00000001) {
	       /* Clear the physical page frame recorded in pte directly in the bitmap of the corresponding memory pool */
	       pg_phy_addr = pte & 0xfffff000;
	 /* Clear the physical page frame recorded in pde directly in the bitmap of the corresponding memory pool */
	 pg_phy_addr = pde & 0xfffff000;

   /* Reclaim physical memory occupied by user virtual address pool*/
   uint32_t bitmap_pg_cnt = (release_thread->userprog_vaddr.vaddr_bitmap.btmp_bytes_len) / PG_SIZE;
   uint8_t* user_vaddr_pool_bitmap = release_thread->userprog_vaddr.vaddr_bitmap.bits;
   mfree_page(PF_KERNEL, user_vaddr_pool_bitmap, bitmap_pg_cnt);

   /* Close the file opened by the process */
   uint8_t local_fd = 3;
   while(local_fd < MAX_FILES_OPEN_PER_PROC) {
      if (release_thread->fd_table[local_fd] != -1) {

Recycle the physical pages in the page table, the physical pages occupied by the virtual memory pool, and close the open files.
Free is used_ a_ phy_ Page to release. The page table is not modified, that is, the effective bit P of the page table is still 1.
The physical pages in the page table can also be released by traversing the virtual memory pool.


/* list_traversal Callback function,
 * Find parent of pelem_ Whether the PID is ppid, return true if successful, and false if failed */
static bool find_child(struct list_elem* pelem, int32_t ppid) {
   struct task_struct* pthread = elem2entry(struct task_struct, all_list_tag, pelem);
   if (pthread->parent_pid == ppid) {     // If the parent of the task_ PID is ppid, return
      return true;   // list_traversal will stop traversal only when the callback function returns true, so it returns true here
   return false;     // Let list_traversal continues to pass the next element

/* list_traversal Callback function,
 * The search status is task_ The task of changing */
static bool find_hanging_child(struct list_elem* pelem, int32_t ppid) {
   struct task_struct* pthread = elem2entry(struct task_struct, all_list_tag, pelem);
   if (pthread->parent_pid == ppid && pthread->status == TASK_HANGING) {
      return true;
   return false; 

/* list_traversal Callback function,
 * Inherit a child process to init */
static bool init_adopt_a_child(struct list_elem* pelem, int32_t pid) {
   struct task_struct* pthread = elem2entry(struct task_struct, all_list_tag, pelem);
   if (pthread->parent_pid == pid) {     // If the parent of the process_ pid is pid, return
      pthread->parent_pid = 1;
   return false;		// Let list_traversal continues to pass the next element

It's all lists_ Callback function of traversal
find_child: find the child process whose parent process pid is ppid
find_hanging_child: specially find the task status_ Child process with changing and parent process ppid
init_adopt_a_child: find the parent of all processes_ The process with pid equal to pid is handed over to the Init process.


/* Wait for the subprocess to call exit and save the exit state of the subprocess to the variable pointed to by status
 * If successful, return the pid of the child process; if failed, return - 1 */
pid_t sys_wait(int32_t* status) {
   struct task_struct* parent_thread = running_thread();

   while(1) {
      /* Prioritize tasks that are already pending */
      struct list_elem* child_elem = list_traversal(&thread_all_list, find_hanging_child, parent_thread->pid);
      /* If there are pending child processes */
      if (child_elem != NULL) {
	 struct task_struct* child_thread = elem2entry(struct task_struct, all_list_tag, child_elem);
	 *status = child_thread->exit_status; 

	 /* thread_exit After that, the pcb will be recycled, so the pid will be obtained in advance */
	 uint16_t child_pid = child_thread->pid;

	 /* 2 Delete process table entries from the ready queue and all queues*/
	 thread_exit(child_thread, false); // Pass in false to make thread_ Return here after exit call
	 /* The process table entry is the last reserved resource of a process or thread, so far the process has completely disappeared */

	 return child_pid;

      /* Determine whether there are child processes */
      child_elem = list_traversal(&thread_all_list, find_child, parent_thread->pid);
      if (child_elem == NULL) {	 // If there is no child process, an error is returned
	 return -1;
      } else {
      /* If the subprocess has not finished running, that is, it has not called exit, it will suspend itself until the subprocess wakes itself up when executing exit */

sys_wait: accept an address status that stores the return value of the child process. Its function is to wait for the child process to call sys_exit. Save the exit state of the child process to the variable pointed to by status.
The process is as follows:
Using find_hanging_child filters out that the parent process is the current process and the status is task in all process queues_ The process of changing,
Exit child process_ Stauts and child_pid is saved, used for return, and then called thread_. Exit deletes the child process from the queue, reclaims the page table and pcb of the child process, releases the pid, and passes false into the thread here_ Exit means thread_ The new process will not be scheduled in exit, but will be returned to sys_wait, then sys_wait returns the return value of the child process that just ended,
Call sys_ The parent process of wait will judge whether the task of the child process has been completed according to this value.
If not, call find_child goes through all the processes again and finds out that sys has not been run yet_ The child process of exit, if any, blocks the parent process. When its child process calls sys_ After exit, the parent process will wake up.


/* Called when a subprocess ends itself */
void sys_exit(int32_t status) {
   struct task_struct* child_thread = running_thread();
   child_thread->exit_status = status; 
   if (child_thread->parent_pid == -1) {
      PANIC("sys_exit: child_thread->parent_pid is -1\n");

   /* Set the process child_ All child processes of thread are inherited to init */
   list_traversal(&thread_all_list, init_adopt_a_child, child_thread->pid);

   /* Recycle process child_thread resources */

   /* If the parent process is waiting for the child process to exit, wake up the parent process */
   struct task_struct* parent_thread = pid2thread(child_thread->parent_pid);
   if (parent_thread->status == TASK_WAITING) {

   /* Suspend yourself, wait for the parent process to get its status and recycle its pcb */

sys_exit: accept a parameter status, which is called when the child process ends itself.
The process is as follows:
First get your own pcb and save the status in your own pcb exit_status,
Then call init_. adopt_ a_ Child entrusts all its child processes to the init process before it ends, and the init process will call sys in an endless loop_ Wait to end these orphan processes.
Then call release_prog_resource releases its resources other than pcb
Then use pid2thread to obtain its own parent process pcb. If the parent process has called sys_wait for yourself, wake it up.
Finally, block yourself and the status is task_ Changing, when the parent process calls sys_wait see task_ Changing will recycle the pcb and pid of this sub process.

Finally, add wait and exit system calls


4. Implement cat command

Used to view file contents


#include "syscall.h"
#include "stdio.h"
#include "string.h"
int main(int argc, char** argv) {
   if (argc > 2) {
      printf("cat: argument error\n");

   if (argc == 1) {
      char buf[512] = {0};
      read(0, buf, 512);

   int buf_size = 1024;
   char abs_path[512] = {0};
   void* buf = malloc(buf_size);
   if (buf == NULL) { 
      printf("cat: malloc memory failed\n");
      return -1;
   if (argv[1][0] != '/') {
      getcwd(abs_path, 512);
      strcat(abs_path, "/");
      strcat(abs_path, argv[1]);
   } else {
      strcpy(abs_path, argv[1]);
   int fd = open(abs_path, O_RDONLY);
   if (fd == -1) { 
      printf("cat: open: open %s failed\n", argv[1]);
      return -1;
   int read_bytes= 0;
   while (1) {
      read_bytes = read(fd, buf, buf_size);
      if (read_bytes == -1) {
      write(1, buf, read_bytes);
   return 66;

cat only accepts one parameter, that is, the file name or absolute path to be viewed.
Process to an absolute path and open the file with the open system call
Read and write system calls read the file sector by sector to the memory and output it to the display screen. Read until the end of the file, close the file and return to 66.


####  This script should be executed in the command directory

if [[ ! -d "../lib" || ! -d "../build" ]];then
   echo "dependent dir don\`t exist!"
   if [[ $cwd != "command" ]];then
      echo -e "you\`d better in command dir\n"

CFLAGS="-Wall -c -fno-builtin -W -Wstrict-prototypes \
      -Wmissing-prototypes -Wsystem-headers"
#LIBS= "-I ../lib -I ../lib/user -I ../fs"
OBJS="../build/string.o ../build/syscall.o \
      ../build/stdio.o ../build/assert.o start.o"

nasm -f elf ./start.S -o ./start.o
ar rcs simple_crt.a $OBJS start.o

gcc -m32 $CFLAGS -I "../lib/" -I "../lib/kernel/" -I "../lib/user/" -I "../kernel/" -I "../device/"  -I "../thread/" -I "../userprog/" -I "../fs/" -I "../shell/"  -o  $BIN".o" $BIN".c"
ld -m elf_i386  $BIN".o" simple_crt.a -o $BIN
SEC_CNT=$(ls -l $BIN|awk '{printf("%d", ($5+511)/512)}')

if [[ -f $BIN ]];then
   dd if=./$DD_IN of=$DD_OUT bs=512 \
   count=$SEC_CNT seek=300 conv=notrunc

The execution process of cat should be like this. Cat belongs to external commands, cat O to write to the file system in advance, call through the shell.
After entering the cat file name in the shell, the shell C detects external commands, so execute execv system call, execute cat process, and cooperate with start S allows the file name parameter to be correctly passed to cat C's main function, start After s completes the parameter transfer, call main to print this file on the display screen.
compile3.sh is used to convert cat O write to the root directory of the file system.
And compile2 SH changing the process name to cat is enough.
Write in is 5553 bytes


int main(void) {
   put_str("I am kernel\n");

/*************    Write application*************/
   uint32_t file_size = 5553; 
   uint32_t sec_cnt = DIV_ROUND_UP(file_size, 512);
   struct disk* sda = &channels[0].devices[0];
   void* prog_buf = sys_malloc(file_size);
   ide_read(sda, 300, prog_buf, sec_cnt);
   int32_t fd = sys_open("/cat", O_CREAT|O_RDWR);
   if (fd != -1) {
      if(sys_write(fd, prog_buf, file_size) == -1) {
         printk("file write error!\n");
/*************    End of writing application*************/
   console_put_str("[rabbit@localhost /]$ ");
   thread_exit(running_thread(), true);
   return 0;

/* init process */
void init(void) {
   uint32_t ret_pid = fork();
   if(ret_pid) {  // Parent process
	int status;
	int child_pid;
		child_pid = wait(&status);
		printf("I'm init, My pid is 1, I recieve a child, It's pid is %d, status is %d\n", child_pid, status);		
   } else {	  // Subprocess
   panic("init: should not be here");

Write cat user program to seven80 IMG file system, use thread before the end of main_ Exit recycles the pid of the main thread pcb. Note that the page table of the main thread cannot be recycled. Because it is true, the new process is scheduled.
Note: exit reclaims memory, wait reclaims pid and page table, and wait calls thread_exit, only the pid of the main thread is recycled. Therefore, the kernel memory of the main thread cannot be recycled.
The child process calls exit and the parent process calls wait.

5. Experimental results and process sorting

exit, wait and cat are added to the core of this experiment.

exit and wait are for the execution order of parent-child processes, that is, fork.
When the parent process needs to implement a function, it will call fork to open a child process,
According to the return value pid of fork, the parent process goes if and the child process goes else.
Then, after the child process realizes the function, it always uses the while(1) loop, and the resources cannot be released. At the same time, the code logic of the parent process is if. The parent process needs the child process to tell whether the function is completed. If not, it needs to wait for the child process, so exit and wait are added.

Calling wait will return two values, one is the child of the finished child process_ PID, one is the status of the child process. Status is provided by the subprocess calling exit. Generally speaking, if the function execution fails, the subprocess will call exit(-1). Depending on these two values, you can know which child process performs the function successfully, so as to write the parent process code.

Now let's simulate the execution sequence of all code involving fork:

First, the init of the main function_ All, creating init thread, main thread and idle thread.
The main thread executes the main function, and the init thread executes the init function.
After calling the fork in the init function, the parent process calls the wait from the dead loop, so that the pid and page tables of all orphan processes can be recovered.
When the orphan process executes exit, it returns its status and pid, and the init process written by our operating system will only print them.
Let's look at the subprocess from the fork of the init process, which calls my_shell()´╝îmy_ The shell is an endless loop that continuously accepts and parses the commands entered by the user and will never end, so exit is not called.

my_shell() keeps printing the command line and readline accepts the commands entered by the user, and then cmd_parse parses this command with if else logic to decide which system call to execute.
We take this experiment as an example/ cat file1 example analysis:
Because/ cat is an external command. cat is a user program stored in the file system in advance. Therefore, my_shell() calls fork to open a child process to execute this external command, and the corresponding parent process calls wait to receive the child returned by the child process_ PID and status, even if the sub process function fails, it still just prints them, but if child_ If the PID is - 1, it means that there is no child process, and panic will report an error.
Next, look at the subprocess dedicated to executing external commands,
make_ clear_ abs_ Wash in path_ Path can put/ Cat such absolute path with points is washed into absolute path without points, argv[0]=/cat, argv[1]=file1. Check that cat is not in the file system, then call execv(argv[0], argv).
execv calls load(argv[0]),
Load parses the elf header of cat. The elf header contains the memory address that each loadable segment should be loaded. Using this information, load each loadable segment of cat into the corresponding memory address and return
The entry address of cat is_ start.
Then execv forces the current process to be modified to cat and causes the cpu to execute cat from start In S_ Execute at start, so that argv can be passed to main of cat, and then call main to execute cat C function.
cat.c's main function naturally knows that argv[1] is file1, which is a relative path. Therefore, use getcwd to obtain the current working directory and spell it into an absolute path. Finally, open file1 and return fd. read reads file1 into memory for many times in 1024 bytes; write prints in video memory. This completes viewing file1.

Topics: Linux Operating System