【UCB操作系统CS162项目】Pintos Lab2:用户程序 User Programs(上)

本节 Lab 2 复杂度有所上升,考虑到文章太长影响观感且有些劝退,博主决定拆成上下两部分来写╰( ̄▽ ̄)╭
以下内容大部分都是博主自己的理解,因为本节开始网上能参考的代码明显变少了(很多文章都在解释同一个作者的代码),所以博主也不能保证文章完全正确,如有问题欢迎交流!

我的实现(更新至Lab 2):Altair-Alpha/pintos

先放一个本节中你会用到的命令行:

# 编译并运行单个测试(注意本节因为使用了文件系统所以命令和Lab 1不同)
# 在没有实现参数传递和系统调用时,运行以下命令看不到程序内的输出(运行测试也会提示Run didn't produce any output),请先去除-q参数
# 去除后,在实现Argument Passing前,运行会显示Page Fault,这是正常的
make && pintos --filesys-size=2 -p tests/userprog/args-multiple -a args-multiple -- -q -f extract run 'args-multiple some arguments for you!'

对 Lab 1 的一些修补

(如果你的实现没有参考博主上篇文章可以跳过该部分)

博主发现完成 Lab 1 后在 userprog 文件夹编译后无论如何运行 pintos 都会立刻发生以下 KERNEL PANIC:

在这里插入图片描述
观察调用栈发现是系统启动的 pintos_init() 函数先调用了 thread_init(),内部会调用 thread_schedule_tail(),而本节实验有 USERPROG 的宏定义,于是 thread_schedule_tail() 又会调用 process_activate(),后者最终会调用 pagedir_activate()
在这里插入图片描述
传入的 pd 参数是线程的 pagedir 成员,此时为 NULL,于是 pd 被赋为 init_page_dir。然而后者在 paging_init 中才会被初始化,该函数在 thread_init 之后被调用,所以此时 init_page_dir 也为 0x0,不属于有效内核地址,于是虚拟地址向物理地址转换的 vtop 函数断言失败。

查找发现 这个这个 链接中也提到了相似的问题,原因在于我在 sema_up 中无条件调用了 thread_yield(),导致调度过早发生。我也和他们使用了类似的解决方案:在 thread.c 中添加一个标记 schedule_started,该标记在 thread_start() 中才会被设为 true,thread_yield() 看到该标记为 true 才会执行调度。

然后再次运行,发现又 PANIC 了:

在这里插入图片描述
看调用栈发现是 thread_yield() 被在关中断时调用了…好吧,上一个 Lab 忘了考虑这种情况,不过当时也说了此时换用 intr_yield_on_return() 即可:

void
sema_up (struct semaphore *sema) 
{
    
    
  ...
  // 原来此处为直接调用thread_yield(),现改为如下判断调用
  if (intr_context()) {
    
    
    intr_yield_on_return();
  } else {
    
    
    thread_yield();
  }
}

现在运行系统就能正常启动了,不过我们还需要按文档说明把文件系统初始化一下:

pintos-mkdisk filesys.dsk --filesys-size=2
pintos -- -f -q

也可以尝试把文件复制到文件系统,尝试运行:

pintos -p ../../examples/echo -a echo -- -q
pintos -- -q run 'echo PKUOS'

当然因为还没有实现参数传递,所以是没有程序输出的。我们还可以用 ls 命令:

pintos -- -q ls

得到:

在这里插入图片描述
可以看到刚才拷贝到文件系统中的 echo。
OK,现在我们可以出发开始本节的实验了~

代码理解

在上手实现之前,首先来阅读一些本节相关的文档和代码,理解用户进程运行的基本流程。

  • We allow more than one process to run at a time. Each process has one thread (multithreaded processes are not supported in Pintos).

Background 部分提示我们,Pintos 中不支持多线程的进程,所以后文中提到的用户进程和系统的线程可以视为一一对应的关系。具体实现上,让我们从 init.c 开始,观察 Pintos 是如何运行某个任务的(命令行形如 pintos -- run alarm-multiple):

/** Pintos main entry point. */
int
pintos_init (void)
{
    
    
  // 读取+解析命令行argv
  // ...(各种init)
  
  if (*argv != NULL) {
    
    
    /* Run actions specified on kernel command line. */
    run_actions (argv);
  } else {
    
    
    // ...(Lab 1实现的当无命令行参数时的交互式Shell)
  }

  // ...(关机)
}

Lab 1 中已经讲过,pintos_init() 就是 Pintos 系统的主函数。带参数的运行,会进入 run_actions() 函数中:

/** Executes all of the actions specified in ARGV[]
   up to the null pointer sentinel. */
static void
run_actions (char **argv) 
{
    
    
  /* An action. */
  struct action 
  {
    
    
    char *name;                       /**< Action name. */
    int argc;                         /**< # of args, including action name. */
    void (*function) (char **argv);   /**< Function to execute action. */
  };

  /* Table of supported actions. */
  static const struct action actions[] = 
  {
    
    
    {
    
    "run", 2, run_task},
#ifdef FILESYS
    {
    
    "ls", 1, fsutil_ls},
    {
    
    "cat", 2, fsutil_cat},
    {
    
    "rm", 2, fsutil_rm},
    {
    
    "extract", 1, fsutil_extract},
    {
    
    "append", 2, fsutil_append},
#endif
    {
    
    NULL, 0, NULL},
  };

  while (*argv != NULL)
  {
    
    
    const struct action *a;
    int i;

    /* Find action name. */
    for (a = actions; ; a++)
      if (a->name == NULL)
        PANIC ("unknown action `%s' (use -h for help)", *argv);
      else if (!strcmp (*argv, a->name))
        break;

    /* Check for required arguments. */
    for (i = 1; i < a->argc; i++)
      if (argv[i] == NULL)
        PANIC ("action `%s' requires %d argument(s)", *argv, a->argc - 1);

    /* Invoke action and advance. */
    a->function (argv);
    argv += a->argc;
  }
  
}

一个 action 由名称(name),参数个数(argc)和执行函数(function)构成,这里固定写好了几种 action,如 lsrm 以及我们最关注的 run 等。解析整个入参 argv 时,外层套 while 循环,使得存在多个 action 时顺序执行。循环中先用每种写好的 action 的 name 做比对,找到对应的任务,再确认参数的个数正确,最后调用 action 的函数。

run 这个 action 对应的执行函数是 run_task()

/** Runs the task specified in ARGV[1]. */
static void
run_task (char **argv)
{
    
    
  const char *task = argv[1];
  
  printf ("Executing '%s':\n", task);
#ifdef USERPROG
  process_wait (process_execute (task));
#else
  run_test (task);
#endif
  printf ("Execution of '%s' complete.\n", task);
}

显然对于本节,会进入 process_wait(process_execute (task)) 中。process_wait() 是一个我们后面要实现的函数,暂时无视。process_execute()

/** Starts a new thread running a user program loaded from
   FILENAME.  The new thread may be scheduled (and may even exit)
   before process_execute() returns.  Returns the new process's
   thread id, or TID_ERROR if the thread cannot be created. */
tid_t
process_execute (const char *file_name) 
{
    
    
  char *fn_copy;
  tid_t tid;

  /* Make a copy of FILE_NAME.
     Otherwise there's a race between the caller and load(). */
  fn_copy = palloc_get_page (0);
  if (fn_copy == NULL)
    return TID_ERROR;
  strlcpy (fn_copy, file_name, PGSIZE);

  /* Create a new thread to execute FILE_NAME. */
  tid = thread_create (file_name, PRI_DEFAULT, start_process, fn_copy);
  if (tid == TID_ERROR)
    palloc_free_page (fn_copy); 
  return tid;
}

接受一个运行的文件名参数,调用 thread_create() 函数创建了一个线程,返回线程 ID。而它是如何让用户程序能够在线程上运行呢?答案在其给线程的函数 start_process() 中。这部分的原理请先阅读 Background 中 80x86 Calling Convention 部分,在下面实现时我也会结合代码具体解释。

同时也请阅读 process.c 中其它的代码,先大致理解其功能即可。做了这些准备,实现起来就不会感到无从下手了。

Task 1: Process Termination Messages

  • Whenever a user process terminates, because it called exit or for any other reason, print the process’s name and exit code, formatted as if printed by printf (“%s: exit(%d)\n”, …);.

在用户进程终止时打印一条信息,这个操作显然应该放在 process_exit() 中进行。进程名称在线程创建时有被记录到 name 成员中,但是退出状态目前是没有记录的。于是添加一个 exit_code 成员,并初始化为 0。但是该成员的更新与系统调用(exit 等)有关,这里我们还未实现,所以暂时先往后看。

Task 2: Argument Passing

  • Add argument passing support for process_execute()

根据代码理解部分,对于 pintos -- run xxx 这样的启动命令,xxx 会以一个整体成为 process_execute()file_name 参数,而实际上 xxx 应该是程序名+程序参数的格式。以 args-single 样例做一个测试:
在这里插入图片描述
可以看出,通过 GDB 打印 file_name 的值是 “args-single onearg”(右下角,命令是 p file_name),是程序名 “args-single” 和参数 “onearg” 的合体。所以在 process_execute() 中,我们首先应该进行参数的解析,把正确的纯程序名传递给 thread_create() 作为线程的名称。严格来说 file_name 这个参数名称不太合适,我的修改如下:

tid_t
process_execute (const char *proc_cmd) 
{
    
    
  char *proc_cmd_copy1, *proc_cmd_copy2;
  tid_t tid;

  /* Make two copies of PROC_CMD (one for proc_name and one for start_process).
     Otherwise there's a race between the caller and load(). */
  proc_cmd_copy1 = palloc_get_page(0);
  proc_cmd_copy2 = palloc_get_page(0);
  if (proc_cmd_copy1 == NULL || proc_cmd_copy2 == NULL)
    return TID_ERROR; 
  strlcpy (proc_cmd_copy1, proc_cmd, PGSIZE);
  strlcpy (proc_cmd_copy2, proc_cmd, PGSIZE);

  char *save_ptr;
  char *proc_name = strtok_r(proc_cmd_copy1, " ", &save_ptr);
  
  /* Create a new thread to execute PROC_CMD. */
  tid = thread_create (proc_name, PRI_DEFAULT, start_process, proc_cmd_copy2);

  if (tid == TID_ERROR) {
    
    
    palloc_free_page(proc_cmd_copy1);
    palloc_free_page(proc_cmd_copy2);
  }
  return tid;
}

接下来是 start_process() 函数,为了不改变函数接口,使入参仍然为 proc_cmd 整体。

第一个重点部分来了:start_process() 的任务是以文件名加载一个 ELF 格式的可执行文件,并将控制权移交给该程序,同时传递正确的参数

加载部分的函数 Pintos 已经为我们写好了,我们只需负责从 proc_cmd 中分离出文件名:

/** A thread function that loads a user process and starts it
   running. */
static void
start_process (void *proc_cmd_)
{
    
    
  char *proc_cmd = proc_cmd_;
  // we can modify proc_cmd since we've passed a copy from process_execute
  // but we need another copy: one for file name, one for arguments (including file name)
  char *proc_cmd_copy = palloc_get_page(0);
  strlcpy(proc_cmd_copy, proc_cmd, PGSIZE);

  struct intr_frame if_;
  bool success;

  char *token, *save_ptr;
  
  char *proc_name = strtok_r(proc_cmd, " ", &save_ptr);

  /* Initialize interrupt frame and load executable. */
  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;
  success = load (proc_name, &if_.eip, &if_.esp);

  /* If load failed, quit. */
  if (!success) 
    thread_exit ();
  ...
}

跳转的指令 Pintos 也为我们写好了:

static void
start_process (void *proc_cmd_)
{
    
    
  ...
  asm volatile ("movl %0, %%esp; jmp intr_exit" : : "g" (&if_) : "memory");
  NOT_REACHED ();
}

我们要负责的就是在跳转之前将参数正确的压入 interrupt frame 的 stack pointer(即代码中的 if_.esp,一个 void * 指针),使得跳转后可执行文件能够读取到这些参数。规则如下:

在这里插入图片描述
在这里插入图片描述

注意要运行的程序名也是一个参数!

中文版总结:

  1. ESP 的初始位置在 PHYS_BASE,压入时向下递减。
  2. 将分割的参数(n个字符串)压入,顺序无所谓,因为后面会通过指针引用。
  3. ESP 对齐到整除 4 的位置(因为前面压入的字符串总长度任意),这样能提高性能。
  4. 先压入一个空指针(哨兵),然后以从右到左的顺序将每个参数字符串的地址压入,称为 argv 数组。
  5. 压入 argv[0] 的地址, 压入参数个数 argc。argv[0] 的地址,实际上就是第 4 步完成时 ESP 的值。
  6. 压入一个伪返回地址(0)。

这里所谓的压入,就是将 ESP 下移一定量,然后将东西拷贝过来。除了第二步压入字符串时下移量是字符串长度+1(加一位 \0 结尾),其它都是一个指针的大小,即 sizeof(void *)。拷贝的方法对于字符串可以用 memcpy(),对于整型也可以先将 ESP 转为一个 int * 指针,然后赋值。第 3 步的对齐需要让指针和整型进行运算,应先将指针转为整型,计算后再转回 void *

我的实现如下:

static void
start_process (void *proc_cmd_)
{
    
    
  ...
  /* If load failed, quit. */
  if (!success) 
    thread_exit ();
  
  int argc = 0;
  void* argv[128]; // assume we have at most 128 command line arguments

  // See Background - Argument Passing.

  // STEP 1. Set the stack pointer at the beginning of user virtual address space, 
  // which is 0xc0000000 (3 GB) in Pintos (See Appendix - Virtual Addresses)
  if_.esp = PHYS_BASE;
  
  // STEP 2. Parse the arguments, place them at the top of the stack and record their address
  for (token = strtok_r (proc_cmd_copy, " ", &save_ptr); token != NULL; token = strtok_r (NULL, " ", &save_ptr)) {
    
    
    // printf("TOKEN %s LEN %d", token, strlen(token));
    size_t arg_len = strlen(token) + 1; // '\0' ending is needed
    if_.esp -= arg_len;
    memcpy(if_.esp, token, arg_len);
    argv[argc++] = if_.esp;
  }

  // STEP 3&4. Round the pointer down to a multiple of 4 first, then push the address of each string
  // plus a null pointer sentinel on the stack, in right-to-left order.
  
  // printf("BEFORE ROUND %p\n", if_.esp);
  uintptr_t tmp = (uintptr_t)if_.esp; // pointer can't do modulo directly
  if (tmp % 4 != 0)
    tmp -= tmp % 4;
  if_.esp = (void *)tmp;
  // printf("AFTER ROUND %p\n", if_.esp);
  
  size_t ptr_size = sizeof(void *); // size of a pointer
  if_.esp -= ptr_size;
  memset(if_.esp, 0, ptr_size); // null pointer sentinel
  for (int i = argc-1; i >= 0; i--)
  {
    
    
    if_.esp -= ptr_size;
    memcpy(if_.esp, &argv[i], ptr_size);
  }

  // STEP 5. Push argv (the address of argv[0]) and argc, in that order.
  if_.esp -= ptr_size;
  *(uintptr_t *)if_.esp = ((uintptr_t)if_.esp + ptr_size); // argv[0] is at where we just were
  if_.esp -= ptr_size;
  *(int *)if_.esp = argc;

  // STEP 6. Push a fake "return address"(0).
  if_.esp -= ptr_size;
  memset(if_.esp, 0, ptr_size);
  ...
}

文档中提示我们可以用 hex_dump() 函数打印内存状况以检验实现的正确性。在上面代码的后面添加:

  printf("STACK SET. ESP: %p\n", if_.esp);
  hex_dump((uintptr_t)if_.esp, if_.esp, 100, true); // 打印的byte数不用特别准确,随便填大一些

运行 args-multiple 测试:

# 注意先不要带-q参数
make && pintos --filesys-size=2 -p tests/userprog/args-multiple -a args-multiple -- -f extract run 'args-multiple some arguments for you!'

可以看到:

在这里插入图片描述
(相同颜色的上下两个框,上面的内容就是下面的地址,即参数字符串的地址和本体)
(↑补注:这里图有些问题,我一开始以为程序名不算参数里,正确图如下,不想重做彩色版了qwq)
在这里插入图片描述

除了打印出的内存信息,还可以看到后面从 Page Fault 变为了打印 “system call!”,这说明用户程序已经正常开始执行,因为调用了系统调用而被结束( 见 userprog/syscall.c,目前 syscall 到来时只是打印并调用 thread_exit()) ,证明我们的 Argument Passing 已经正确实现了!

后面的部分,我的实现顺序没有完全按照文档,所以就以我的思路来讲了。

Syscall:理解 + halt + exit + write(部分)

细心的小伙伴可能已经发现了:除了我们要实现的 userprog/syscall.c,在 lib/user 文件夹下,还有一对 syscall.hsyscall.c 文件,其函数形如:

void
halt (void) 
{
    
    
  syscall0 (SYS_HALT);
  NOT_REACHED ();
}

void
exit (int status)
{
    
    
  syscall1 (SYS_EXIT, status);
  NOT_REACHED ();
}

syscall0syscall1 一直到 3 是一组宏:

/** Invokes syscall NUMBER, passing no arguments, and returns the
   return value as an `int'. */
#define syscall0(NUMBER)                                        \
        ({
      
                                                            \
          int retval;                                           \
          asm volatile                                          \
            ("pushl %[number]; int $0x30; addl $4, %%esp"       \
               : "=a" (retval)                                  \
               : [number] "i" (NUMBER)                          \
               : "memory");                                     \
          retval;                                               \
        })

/** Invokes syscall NUMBER, passing argument ARG0, and returns the
   return value as an `int'. */
#define syscall1(NUMBER, ARG0)                                           \
        ({
      
                                                                     \
          int retval;                                                    \
          asm volatile                                                   \
            ("pushl %[arg0]; pushl %[number]; int $0x30; addl $8, %%esp" \
               : "=a" (retval)                                           \
               : [number] "i" (NUMBER),                                  \
                 [arg0] "g" (ARG0)                                       \
               : "memory");                                              \
          retval;                                                        \
        })

这组文件,是 提供给用户的系统调用接口,其通过 int $0x30 中断指令以唤起一个系统调用(请阅读文档 System Call Details 部分)。该调用最终会落入 userprog/syscall.csyscall_handler() 函数中。系统调用的参数有系统调用编号 NUMBER 加 0-3 个参数 ARG[0-3],其中前者的定义在 lib/syscall-nr.h 中:

#ifndef __LIB_SYSCALL_NR_H
#define __LIB_SYSCALL_NR_H

/** System call numbers. */
enum 
  {
    
    
    /* Projects 2 and later. */
    SYS_HALT,                   /**< Halt the operating system. */
    SYS_EXIT,                   /**< Terminate this process. */
    SYS_EXEC,                   /**< Start another process. */
    SYS_WAIT,                   /**< Wait for a child process to die. */
    SYS_CREATE,                 /**< Create a file. */
    SYS_REMOVE,                 /**< Delete a file. */
    SYS_OPEN,                   /**< Open a file. */
    SYS_FILESIZE,               /**< Obtain a file's size. */
    SYS_READ,                   /**< Read from a file. */
    SYS_WRITE,                  /**< Write to a file. */
    SYS_SEEK,                   /**< Change position in a file. */
    SYS_TELL,                   /**< Report current position in a file. */
    SYS_CLOSE,                  /**< Close a file. */

    /* Project 3 and optionally project 4. */
    SYS_MMAP,                   /**< Map a file into memory. */
    SYS_MUNMAP,                 /**< Remove a memory mapping. */

    /* Project 4 only. */
    SYS_CHDIR,                  /**< Change the current directory. */
    SYS_MKDIR,                  /**< Create a directory. */
    SYS_READDIR,                /**< Reads a directory entry. */
    SYS_ISDIR,                  /**< Tests if a fd represents a directory. */
    SYS_INUMBER                 /**< Returns the inode number for a fd. */
  };

#endif /**< lib/syscall-nr.h */

本节中共会用到 13 种,对应编号 0-12。

这些参数由调用者通过 pushl 指令直接压入栈,syscall_handler() 中,要从参数 interrupt frame 的 ESP 中提取这些参数

于是系统调用的实现我们就有总体的思路了:先在 syscall_handler() 中解析系统调用的编号,根据类型分发给各个具体处理函数,逐个实现。注意返回值要放在 EAX 中传回调用者

syscall_handler() 中分发:

static void
syscall_handler (struct intr_frame *f) 
{
    
    
  int syscall_type = *(int *)f->esp;
  switch (syscall_type)
  {
    
    
  case SYS_HALT:
    syscall_halt(f);
    break;
  case SYS_EXIT:
    syscall_exit(f);
  ...
  }
}

先实现一个最简单的调用:强制退出 halt

  • System Call: void halt (void)
    Terminates Pintos by calling shutdown_power_off() (declared in devices/shutdown.h).
static void 
syscall_halt(struct intr_frame *f UNUSED)
{
    
    
  shutdown_power_off();
}

再实现一个大部分程序会用到的:正常退出 exit

  • System Call: void exit (int status)
    Terminates the current user program, returning status to the kernel.
    If the process’s parent waits for it (see below), this is the status that will be returned. Conventionally, a status of 0 indicates success and nonzero values indicate errors.

先不管父子问题,简单实现:

static void 
syscall_exit(struct intr_frame *f)
{
    
    
  // exit_code is passed as ARG0, after syscall number
  int exit_code = *(int *)(f->esp + sizeof(void *));
  // printf("EXIT CODE: %d\n", exit_code);
  thread_current()->exit_code = exit_code;
  thread_exit();
}

此时运行

make && pintos --filesys-size=2 -p tests/userprog/args-multiple -a args-multiple -- -f extract run 'args-multiple some arguments for you!'

博主发现仍然没有参数的输出:

在这里插入图片描述

于是在 handler 函数中添加一句打印系统调用编号,发现前面几个调用是 9 号,最后一个才是 1 号(SYS_EXIT)。9 号是什么呢?是 SYS_WRITE,突然想起来:用户程序的打印(输出到 stdout)也是通过写的系统调用实现的。查阅文档:

  • System Call: int write (int fd, const void *buffer, unsigned size)
    • Writes size bytes from buffer to the open file fd. Returns the number of bytes actually written, which may be less than size if some bytes could not be written.
    • Fd 1 writes to the console. Your code to write to the console should write all of buffer in one call to putbuf(), at least as long as size is not bigger than a few hundred bytes. (It is reasonable to break up larger buffers.) Otherwise, lines of text output by different processes may end up interleaved on the console, confusing both human readers and our grading scripts.

提供给用户的接口:

int
write (int fd, const void *buffer, unsigned size)
{
    
    
  return syscall3 (SYS_WRITE, fd, buffer, size);
}

为了能看到至少一个样例测试通过(泪),我决定先把输出到 stdoutfd=1)的写调用实现,写文件的后面再考虑。实现如下:

static void 
syscall_write(struct intr_frame *f)
{
    
    
  int fd = *(int *)(f->esp + ptr_size);
  char *buf = *(char **)(f->esp + 2*ptr_size);
  int size = *(int *)(f->esp + 3*ptr_size);

  if (fd == 1) {
    
    
    putbuf(buf, size);
    f->eax = size;
  }
}

再次运行:
在这里插入图片描述

看起来到这里我们的程序应该就能正常运行了,但 make check 会发现测试仍然全部失败,错误信息为:“Run didn’t produce any output”。加上一开始去掉的 -q 参数,也会发现这些输出又消失了。最终博主在文档中找到了这样一段描述:

  • For now, change process_wait() to an infinite loop (one that waits forever). The provided implementation returns immediately, so Pintos will power off before any processes actually get to run. You will eventually need to provide a correct implementation.

好吧,原来是一开始忽略掉的 process_wait() 的问题,用户程序还没执行整个系统就关机了。那就在 return -1; 之前加一个死循环 while(1);,再次运行可以发现加上 -q 参数也能看到和上图一样的输出了,不过测试肯定还是通过不了的(泪)。

那么这篇文章就在此收住,下篇我们继续来完成剩余部分的实现。


彩蛋

如果你做到这里一定想看到一些测试能够通过(例如博主),那可以用以下这个略奇葩的方法:
thread.c 中添加以下函数(记得在头文件声明):

int thread_dead(tid_t tid)
{
    
    
  struct list_elem *e;
  for (e = list_begin (&all_list); e != list_end (&all_list);
       e = list_next (e))
  {
    
    
    struct thread *t = list_entry (e, struct thread, allelem);
    if (t->tid == tid) return 0;
  }
  return 1;
}

process_wait() 函数改成这样:

int
process_wait (tid_t child_tid) 
{
    
    
  while (1)
  {
    
    
    thread_yield();
    if (thread_dead(child_tid)) break;
  }
  return -1;
}

(博主最开始是写了一次 thread_yield(),发现 main 线程还是会先于运行的任务结束掉,优先级设为 PRI_MIN 也没用,debug 了好久发现是 start_process()load() 函数中加载用户程序时会主动阻塞…所以改成以上这样,判断用户线程寄了之后(从 all_list 中消失)再让主线程退出,就能看到用户线程的输出了。修改后运行 make check 的结果:
在这里插入图片描述
args-xxx,halt 和 exit 的测试都成功通过了!

猜你喜欢

转载自blog.csdn.net/Altair_alpha/article/details/126819252
今日推荐