Stanford Pintos Project2源代码分析&实现思路

0. 引言

本文是对以下仓库中Pintos Project2源代码的思路分析,截取了重要的核心函数,以方便读者理解整个的实现思路。仅供参考,完成作业记得自己深入理解噢~

https://github.com/NicoleMayer/pintos_project2

1. 打印进程终结信息

1.1 process_exit


void
process_exit (void)
{
    
    
  struct thread *cur = thread_current ();
  uint32_t *pd;
  int exit_status = current_thread->exit_status;//exit_status变量为退出状态
  if (exit_status == INIT_EXIT_STAT)
    exit_process(-1);

  printf("%s: exit(%d)\n",current_thread->name,exit_status);//打印当前线程名和退出状态

  /* 销毁当前线程的页目录,切换回内核目录 */
  pd = cur->pagedir;
  if (pd != NULL)
  {
    
    
    cur->pagedir = NULL;//当前线程页面设置为空,保证timer的中断就不会切换回进程页面目录
    pagedir_activate (NULL);//激活线程页表
    pagedir_destroy (pd);//销毁线程页面之前的目录
  }
}

每当一个用户进程因为该进程调用exit或其它原因而结束时,需要打印该进程的进程名和退出码(exit code)。

同时,在结束一个进程时,我们要释放该进程占用的所有资源。


2. 参数传递

2.1 process_execute()


tid_t
process_execute (const char *file_name)
{
    
    
  tid_t tid;
  char *fn_copy = malloc(strlen(file_name)+1);
  char *fn_copy2 = malloc(strlen(file_name)+1);
  strlcpy (fn_copy, file_name, strlen(file_name)+1);
  strlcpy (fn_copy2, file_name, strlen(file_name)+1);//file_name的两份拷贝,避免caller和load的冲突
  
  char *save_ptr;
  fn_copy2 = strtok_r (fn_copy2, " ", &save_ptr);//用strtok_r函数分离字符串,获得thread_name(存放在fn_copy2),为实现参数传递做准备
    
  /* 创建以file_name为名字的新线程,新的子进程执行start_process函数. */
  tid = thread_create (fn_copy2, PRI_DEFAULT, start_process, fn_copy);
  free(fn_copy2);   //手动释放fn_copy2
    
  if (tid == TID_ERROR){
    
    
    free (fn_copy);
    return tid;
  }
  sema_down(&thread_current()->sema);//降低父进程的信号量,等待子进程结束
  if (!thread_current()->success) return TID_ERROR;//子进程加载可执行文件失败报错  
  return tid;
}

把传入的参数file_namestrtok_r函数分隔开,获得线程名,为接下来的参数传递(之后会传递给start_process,load , setup_stack)做准备。并以此为线程名创建一个新线程,然后新线程转去执行start_process函数。若子进程加载可执行文件的过程没有问题,则返回新建线程的tid.在这之前,父进程无法返回。这样的同步操作是依靠struct thread里的success变量以及信号量的增减实现的。success记录了线程是否成功执行,而通过创建子进程时父进程信号量减少、子进程结束时父进程信号量增加来实现父进程等待子进程的效果,并保证子进程结束唤醒父进程。


2.2 push_argument()


void
push_argument (void **esp, int argc, int argv[]){
    
    
  *esp = (int)*esp & 0xfffffffc;
  *esp -= 4;
  *(int *) *esp = 0;
  /*下面这个for循环的意义是:按照argc的大小,循环压入argv数组,这也符合argc和argv之间的关系*/
  for (int i = argc - 1; i >= 0; i--)
  {
    
    
    *esp -= 4;//每次入栈后栈指针减4
    *(int *) *esp = argv[i];
  }
  *esp -= 4;
  *(int *) *esp = (int) *esp + 4;
  *esp -= 4;
  *(int *) *esp = argc;
  *esp -= 4;
  *(int *) *esp = 0;
}

简单来说,这个函数完成了根据argc的大小将argv数组压入栈的操作。在2.3start_process函数中被调用。压入栈顶的过程中,依次存放入参数argv数组(靠参数分离得到)、argv的地址和argc的地址。


2.3 start_process()


static void
start_process (void *file_name_)
{
    
    
  char *file_name = file_name_;
  struct intr_frame if_;
  bool success;
    
  char *fn_copy=malloc(strlen(file_name)+1);
  strlcpy(fn_copy,file_name,strlen(file_name)+1);//file_name的一份拷贝

  
  memset (&if_, 0, sizeof if_);
  if_.gs = if_.fs = if_.es = if_.ds = if_.ss = SEL_UDSEG;
  if_.cs = SEL_UCSEG;
  if_.eflags = FLAG_IF | FLAG_MBS;
  
  char *token, *save_ptr;
  file_name = strtok_r (file_name, " ", &save_ptr);//字符串分离,得到线程名,为了传入接下来load函数的参数
  success = load (file_name, &if_.eip, &if_.esp);
    //调用load函数,判断其是否成功load

  if (success){
    
    
    int argc = 0;
    //限制命令行长度不得超过50
    int argv[50];
    
    //token也就是命令行输入的参数分离后得到的数组,包含了argv
    for (token = strtok_r (fn_copy, " ", &save_ptr); token != NULL; token = strtok_r (NULL, " ", &save_ptr)){
    
    
      if_.esp -= (strlen(token)+1);
      memcpy (if_.esp, token, strlen(token)+1);//栈指针退后token的长度,空出token长度的空间用来存放token
      argv[argc++] = (int) if_.esp;//argv数组的末尾存放栈顶地址,也就是argv的地址
    }
    push_argument (&if_.esp, argc, argv);//将argv参数数组按argc的大小推入栈

    thread_current ()->parent->success = true;//保存父进程的执行状态为成功执行
    sema_up (&thread_current ()->parent->sema);//提升父进程的信号量
  }

  //如果调用load失败,则:
  else{
    
    
    thread_current ()->parent->success = false;//保存父进程的执行状态为执行失败
    sema_up (&thread_current ()->parent->sema);//提升父进程的信号量
    thread_exit ();//退出
  }
  
  asm volatile ("movl %0, %%esp; jmp intr_exit" : : "g" (&if_) : "memory");
  NOT_REACHED ();
}

对于刚刚新创建的线程,先初始化中断帧,再调用load函数(load还会调用setup_stack,为程序分配内存并创建用户栈)。如果调用成功,分配好了地址空间并创建完成用户栈,则调用push_argument把argv数组压入栈顶,否则就退出当前的线程。


2.4 file_deny_write()


void
file_deny_write (struct file *file) 
{
    
    
  ASSERT (file != NULL);
  if (!file->deny_write) 
    {
    
    
      file->deny_write = true;
      inode_deny_write (file->inode);
    }//如果file的deny_write属性为false,则置为true
}

将文件设置为拒绝写入的状态,目的是为了防止对一个文件进行写入操作,直到file_allow_write被调用或该文件被关闭。在2.5load函数中被调用。


2.5 load()


bool
load (const char *file_name, void (**eip) (void), void **esp)
{
    
    
  struct thread *t = thread_current();
  struct Elf32_Ehdr ehdr;
  struct file *file = NULL;
  off_t file_ofs;
  bool success = false;
  int i;
  
  /*分配页目录 */
  t->pagedir = pagedir_create();
  if (t->pagedir == NULL)
    goto done;
  process_activate();
    
  acquire_lock_f ();
  file = filesys_open (file_name);//根据传入load函数的参数file_name打开指定文件
  if (file == NULL)
  {
    
    
    printf ("load: %s: open failed\n", file_name);
    goto done;
  }//打开失败
  
  /* 通过调用file_deny_write函数,拒绝写入文件 */
  file_deny_write(file);
  t->file_owned = file;

  
  if (file_read(file, &ehdr, sizeof ehdr) != sizeof ehdr || memcmp(ehdr.e_ident, "\177ELF\1\1\1", 7) || ehdr.e_type != 2 || ehdr.e_machine != 3 || ehdr.e_version != 1 || ehdr.e_phentsize != sizeof(struct Elf32_Phdr) || ehdr.e_phnum > 1024)
  {
    
    
    printf("load: %s: error loading executable\n", file_name);
    goto done;
  }


  file_ofs = ehdr.e_phoff;
  for (i = 0; i < ehdr.e_phnum; i++)
  {
    
    
    struct Elf32_Phdr phdr;
    if (file_ofs < 0 || file_ofs > file_length(file))
      goto done;
    file_seek(file, file_ofs);
    if (file_read(file, &phdr, sizeof phdr) != sizeof phdr)
      goto done;
    file_ofs += sizeof phdr;
    switch (phdr.p_type)
    {
    
    
    case PT_NULL:
    case PT_NOTE:
    case PT_PHDR:
    case PT_STACK:
    default:
      
      break;
    case PT_DYNAMIC:
    case PT_INTERP:
    case PT_SHLIB:
      goto done;
    case PT_LOAD:
      if (validate_segment(&phdr, file))
      {
    
    
        bool writable = (phdr.p_flags & PF_W) != 0;
        uint32_t file_page = phdr.p_offset & ~PGMASK;
        uint32_t mem_page = phdr.p_vaddr & ~PGMASK;
        uint32_t page_offset = phdr.p_vaddr & PGMASK;
        uint32_t read_bytes, zero_bytes;

        if (phdr.p_filesz > 0)
        {
    
    
          read_bytes = page_offset + phdr.p_filesz;
          zero_bytes = (ROUND_UP(page_offset + phdr.p_memsz, PGSIZE) - read_bytes);
        }
        else
        {
    
    
          read_bytes = 0;
          zero_bytes = ROUND_UP(page_offset + phdr.p_memsz, PGSIZE);
        }
        if (!load_segment(file, file_page, (void *)mem_page,read_bytes, zero_bytes, writable))
          goto done;
      }
      else
        goto done;
      break;
    }
  }
  
  if (!setup_stack(esp, file_name))//调用setup_stack创建用户栈时,不仅要传入esp,还要传入file_name,因为file_name里包含了参数,传入file_name才能实现参数传递
    goto done;//如果创建栈失败,则跳过下面的设置起始地址
  /* 起始地址 */
  *eip = (void (*)(void))ehdr.e_entry;
  success = true;//分配地址、创建用户栈成功
  
done:
  release_lock_f();
  return success;
}

为当前用户程序分配内存,初始化页目录。之后调用setup_stack创建用户栈,并把参数传入setup_stack.

2.6 setup_stack()


static bool
setup_stack (void **esp, char * file_name)
{
    
    
  uint8_t *kpage;
  bool success = false;
    
  kpage = palloc_get_page (PAL_USER | PAL_ZERO);
  if (kpage != NULL)
    {
    
    
      success = install_page (((uint8_t *) PHYS_BASE) - PGSIZE, kpage, true);
      if (success)
        *esp = PHYS_BASE;
      else
        palloc_free_page (kpage);
    }
  return success;
}

创建用户栈,并返回创建是否成功的状态。

总结来说,参数传递部分的主要流程为:

process_execute创建线程,并分离参数,并把参数(含文件名)传递给start_process函数,让新线程执行start_process函数。start_process将参数继续传递给load函数,load函数为用户程序分配了地址空间,并继续将参数传递给setup_stack函数,setup_stack创建了用户栈并返回到loadload返回到start_process。接下来,在start_process中调用push_argument将用户程序所需的参数argc,argv及他们的地址入栈。这样就利用参数传递,完成了用户程序执行的准备过程。

参数传递部分函数调用关系流程图:

在这里插入图片描述

3. 系统调用

3.1 check_ptr2()


void * 
check_ptr2(const void *vaddr)
{
    
     
  /* 检查指针是否无效 */
  if (!is_user_vaddr(vaddr))
  {
    
    
    exit_special ();//无效就退出
  }
  /* 检查页面是否无效 */
  void *ptr = pagedir_get_page (thread_current()->pagedir, vaddr);
  if (!ptr)
  {
    
    
    exit_special ();//无效就退出
  }
  /* 检查页面的内容是否无效 */
  uint8_t *check_byteptr = (uint8_t *) vaddr;
  for (uint8_t i = 0; i < 4; i++) 
  {
    
    
    if (get_user(check_byteptr + i) == -1)
    {
    
    
      exit_special ();//无效就退出
    }
  }
  return ptr;
}

这个函数主要是为了检查地址和页面的有效性,来确保系统调用时各种操作的合法性。

3.2 syscall_init()


void
syscall_init (void)
{
    
    
  intr_register_int (0x30, 3, INTR_ON, syscall_handler, "syscall");
  /*通过syscall数组来存储13个系统调用,在syscall_handler中决定调用哪一个*/
  syscalls[SYS_HALT] = &sys_halt;
  syscalls[SYS_EXIT] = &sys_exit;
  syscalls[SYS_EXEC] = &sys_exec;
  syscalls[SYS_WAIT] = &sys_wait;
  syscalls[SYS_CREATE] = &sys_create;
  syscalls[SYS_REMOVE] = &sys_remove;
  syscalls[SYS_OPEN] = &sys_open;
  syscalls[SYS_WRITE] = &sys_write;
  syscalls[SYS_SEEK] = &sys_seek;
  syscalls[SYS_TELL] = &sys_tell;
  syscalls[SYS_CLOSE] =&sys_close;
  syscalls[SYS_READ] = &sys_read;
  syscalls[SYS_FILESIZE] = &sys_filesize;

}

初始化系统调用,通过syscall数组来存储13个系统调用,在syscall_handler里通过识别数组的序号决定调用哪一个系统调用。

3.3 syscall_handler()


static void
syscall_handler (struct intr_frame *f UNUSED)
{
    
    
  int * p = f->esp;
  check_ptr2 (p + 1);//检查有效性
  int type = * (int *)f->esp;//记录在栈顶的系统调用类型type
  if(type <= 0 || type >= max_syscall){
    
    
    exit_special ();//类型错误,退出
  }
  syscalls[type](f);//类型正确,查找数组调用对应系统调用并调用执行
}

用户的命令(如创建文件)会被中断识别,并把命令的参数压入栈。所以系统调用的类型是存放在栈顶的。在syscall_handler中,我们弹出用户栈参数,将这一类型取出,再按照这个类型去查找在syscall_init中定义的syscalls数组,找到对应的系统调用并执行它。

3.4 sys_halt()


void 
sys_halt (struct intr_frame* f)
{
    
    
  shutdown_power_off();
}

调用shutdown_power_off让pintos关机。

3.5 sys_exit()


void 
sys_exit (struct intr_frame* f)
{
    
    
  uint32_t *user_ptr = f->esp;
  check_ptr2 (user_ptr + 1);
  *user_ptr++;
  thread_current()->st_exit = *user_ptr;
  thread_exit ();
}

结束当前的用户程序,并返回状态给内核kernel. 这里先检查栈指针的合法性,然后将当前线程的退出状态st_exit(在thread.c中新增定义,代表线程退出状态)设为user_ptr的值。

3.6 sys_exec()


void 
sys_exec (struct intr_frame* f)
{
    
    
  uint32_t *user_ptr = f->esp;
  check_ptr2 (user_ptr + 1);
  check_ptr2 (*(user_ptr + 1));
  *user_ptr++;
  f->eax = process_execute((char*)* user_ptr);
}

执行文件。首先,检查由file_name指向的文件是否有效(调用check_ptr2)。若有效,则调用process_execute来去执行它。

3.7 sys_wait()


void 
sys_wait (struct intr_frame* f)
{
    
    
  uint32_t *user_ptr = f->esp;
  check_ptr2 (user_ptr + 1);
  *user_ptr++;
  f->eax = process_wait(*user_ptr);
}

首先,还是检查传入参数f是否有效。若有效则调用process_wait来完成系统调用,等待一个子进程的结束。


3.8 process_wait()


int
process_wait (tid_t child_tid UNUSED)
{
    
    
  struct list *l = &thread_current()->childs;//当前进程的子进程们
  struct list_elem *temp;
  temp = list_begin (l);
  struct child *temp2 = NULL;
  /*这个循环一直执行直到找到子进程中父进程正在等待的那个(child_tid)*/
  while (temp != list_end (l))
  {
    
    
    temp2 = list_entry (temp, struct child, child_elem);
    if (temp2->tid == child_tid)
    {
    
    //找到了正在等待的子进程
      if (!temp2->isrun)//isrun是在结构体child里定义的成员变量,代表子进程是否成功运行
      {
    
    
        temp2->isrun = true;
        sema_down (&temp2->sema);
        break;//如果正在等待的那个子进程没有在运行了,则减少它的信号量(为了唤醒父进程)
      } 
      else 
      {
    
    
        return -1;//子进程还在运行,没有退出,则返回-1
      }
    }
    temp = list_next (temp);
  }
  if (temp == list_end (l)) {
    
    
    return -1;//没有找到对应子进程,返回-1
  }
  //执行到这里说明子进程正常退出
  list_remove (temp);//从子进程列表中删除该子进程,因为它已经没有在运行了,也就是说父进程重新抢占回了资源
  return temp2->store_exit;//store_exit是结构体child里定义的子进程的返回状态
}

整个函数的功能是等待tid为child_tid的子进程,记录它的退出状态。

其中,首先循环当前进程的子进程列表,找到要wait的那个子进程。然后判断它是否已经运行结束(可以唤醒父进程了):如果已经结束则减少子进程信号量以唤醒父进程,再从子进程列表中删除该子进程并返回它的退出状态;如果还在运行则返回-1(没有找到child_tid也返回-1)。

3.9 sys_create()


void 
sys_create(struct intr_frame* f)
{
    
    
  uint32_t *user_ptr = f->esp;
  check_ptr2 (user_ptr + 5);
  check_ptr2 (*(user_ptr + 4));
  *user_ptr++;
  acquire_lock_f ();//创建文件之前先获得文件的锁
  f->eax = filesys_create ((const char *)*user_ptr, *(user_ptr+1));//调用filesys_create创建文件,参数名存放在栈顶
  release_lock_f ();//创建完成释放锁
}

这个函数创建了一个文件。首先还是检查地址和页面的有效性。然后先获得文件的锁,这是为了保证用户程序在运行时,可执行文件不可被修改(文件系统的系统调用的同步性)。然后调用filesys_create来创建文件,参数(文件名等)存放在栈顶,用*user_ptr可以找到。系统调用完成后释放文件锁。

像3.9中的获得锁+释放锁的组合在接下来的几个函数都有用到。因为这几个函数都涉及到了文件系统的系统调用。

3.10 sys_remove()


void 
sys_remove(struct intr_frame* f)
{
    
    
  uint32_t *user_ptr = f->esp;
  check_ptr2 (user_ptr + 1);
  check_ptr2 (*(user_ptr + 1));
  *user_ptr++;
  acquire_lock_f ();//删除文件之前先获得文件的锁
  f->eax = filesys_remove ((const char *)*user_ptr);//调用filesys_remove删除文件,参数名存放在栈顶
  release_lock_f ();//释放锁
}

这个函数删除了一个文件。首先还是检查地址和页面的有效性。之后先获得文件的锁,然后调用filesys_remove来删除文件,参数(文件名等)存放在栈顶,用*user_ptr可以找到。系统调用完成后释放文件锁。

3.11 sys_open()


void 
sys_open (struct intr_frame* f)
{
    
    
  uint32_t *user_ptr = f->esp;
  check_ptr2 (user_ptr + 1);
  check_ptr2 (*(user_ptr + 1));
  *user_ptr++;
  acquire_lock_f ();
  struct file * file_opened = filesys_open((const char *)*user_ptr);//打开文件
  release_lock_f ();
  struct thread * t = thread_current();
  if (file_opened)
  {
    
    
    struct thread_file *thread_file_temp = malloc(sizeof(struct thread_file));
    thread_file_temp->fd = t->file_fd++;//为文件按序添加一个属于当前线程的文件标识符
    thread_file_temp->file = file_opened;
    list_push_back (&t->files, &thread_file_temp->file_elem);//放入线程的打开文件列表
    f->eax = thread_file_temp->fd;//打开成功,返回文件标识符(非负)
  } 
  else
  {
    
    
    f->eax = -1;//打开失败返回-1
  }
}

打开栈顶指向的文件。调用filesys_open打开文件。之后,为该文件按序添加一个属于当前线程(打开这个文件的线程)的文件标识符fd. (同一个文件的不同文件标识符是独立的,就是说当一个文件被重复打开了多次,不管它是被同一个或者不同的进程打开的,每一个open都要返回一个新的文件标识符)再将这个文件添加入当前线程打开文件的列表t->files中。将文件标识符fd存放于eax中。

3.12 sys_filesize()


void 
sys_filesize (struct intr_frame* f){
    
    
  uint32_t *user_ptr = f->esp;
  check_ptr2 (user_ptr + 1);
  *user_ptr++;
  struct thread_file * thread_file_temp = find_file_id (*user_ptr);//先通过文件标识符找到指定的文件
  if (thread_file_temp)
  {
    
    
    acquire_lock_f ();
    f->eax = file_length (thread_file_temp->file);//file_length获取文件长度
    release_lock_f ();
  } 
  else
  {
    
    
    f->eax = -1;
  }
}

通过find_file_id获取文件标识符。之后调用file_length返回以文件标识符fd指代的文件的大小。

3.13 sys_write()


void 
sys_write (struct intr_frame* f)
{
    
    
  uint32_t *user_ptr = f->esp;
  check_ptr2 (user_ptr + 7);
  check_ptr2 (*(user_ptr + 6));
  *user_ptr++;
  int temp2 = *user_ptr;//判断写入类型
  const char * buffer = (const char *)*(user_ptr+1);
  off_t size = *(user_ptr+2);
  if (temp2 == 1) {
    
    
    /* 写入到缓冲区 */
    putbuf(buffer,size);
    f->eax = size;
  }
  else
  {
    
    
    /* 写入到文件 */
    struct thread_file * thread_file_temp = find_file_id (*user_ptr);//找到文件
    if (thread_file_temp)
    {
    
    
      acquire_lock_f ();
      f->eax = file_write (thread_file_temp->file, buffer, size);//写入
      release_lock_f ();
    } 
    else
    {
    
    
      f->eax = 0;
    }
  }
}

write的情况有两种:往缓冲区写入数据或往文件中写入数据。取栈顶元素记为temp2,temp2这个参数标记了我们是要往哪里写入数据。

如果是往缓冲区写入,则调用putbuf函数完成写入;如果是往文件中写入,则先调用find_file_id获取要写入的文件的文件标识符,然后调用file_write往对应文件中写入数据。

3.14 sys_seek()


void 
sys_seek(struct intr_frame* f)
{
    
    
  uint32_t *user_ptr = f->esp;
  check_ptr2 (user_ptr + 5);
  *user_ptr++;
  struct thread_file *file_temp = find_file_id (*user_ptr);
  if (file_temp)
  {
    
    
    acquire_lock_f ();
    file_seek (file_temp->file, *(user_ptr+1));//把下一个要读入或写入的字节跳转到指定位置(第二个参数代表位置)
    release_lock_f ();
  }
}

seek函数先通过find_file_id找到要进行seek操作的文件,然后根据传入的参数(文件标识符、位置),调用file_seek函数把下一个要读入或写入的字节跳转到指定文件的指定位置。

3.15 sys_tell()


void 
sys_tell (struct intr_frame* f)
{
    
    
  uint32_t *user_ptr = f->esp;
  check_ptr2 (user_ptr + 1);
  *user_ptr++;
  struct thread_file *thread_file_temp = find_file_id (*user_ptr);
  if (thread_file_temp)
  {
    
    
    acquire_lock_f ();
    f->eax = file_tell (thread_file_temp->file);
    release_lock_f ();
  }else{
    
    
    f->eax = -1;
  }
}

通过find_file_id获取文件标识符。然后调用file_tell返回下一个在已打开文件fd中即将被读入或写入的字节的位置。

3.16 sys_close()


void 
sys_close (struct intr_frame* f)
{
    
    
  uint32_t *user_ptr = f->esp;
  check_ptr2 (user_ptr + 1);
  *user_ptr++;
  struct thread_file * opened_file = find_file_id (*user_ptr);
  if (opened_file)
  {
    
    
    acquire_lock_f ();
    file_close (opened_file->file);//关闭文件
    release_lock_f ();
    list_remove (&opened_file->file_elem);//从线程文件列表里删除该文件
    free (opened_file);//释放资源
  }
}

通过find_file_id获取文件标识符。判断,如果该文件处于打开状态,则调用file_close关闭它。关闭后,把这个文件从线程的文件list中移除并释放资源。

总的来说,系统调用部分的主要流程为:

syscall_init存储了系统调用的类型。当中断发生,参数(包含了系统调用的类型)入栈,这时,syscall_handler弹出栈顶元素,也就是系统调用的类型,并去syscall_init里寻找有无定义该系统调用,找到了的话就转而执行该系统调用。

要注意的是,在执行有关文件系统的系统调用时,要注意文件系统系统调用的线程安全,以便任意数量的用户进程可以同步进行调用。具体的操作是先获得文件的锁,这是为了保证用户程序在运行时,可执行文件不可被修改。然后执行完系统调用后再释放锁。这样就实现了同步操作。

系统调用部分函数调用关系流程图:
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_44765402/article/details/111089137