Skip to main content Link Menu Expand (external link) Document Search Copy Copied

Project 02 Practice Problems with Solutions

Shell History

Question 1 (Easy)

What data structure is used to store command history in the xv6 shell implementation? How many commands are stored in the history?

Solution

The shell history is stored using a linked list data structure, specifically using the list.c implementation provided in xv6. The history stores a maximum of 10 commands, as defined by the constant MAX_HISTORY_LEN in the shell implementation.

#define MAX_HISTORY_LEN 10
struct list history;

Question 2 (Easy)

Explain the purpose of the !<num> and !<text> commands in the shell history implementation. Provide an example of how each would be used.

Solution

  • !<num>: This command retrieves and executes the command at the specified position in the history list. For example, !3 would execute the 3rd command in the history.
  • !<text>: This command searches the history for the most recent command that starts with the specified text and executes it. For example, !echo would find and execute the most recent command that starts with “echo”.

Examples:

$ echo hello
hello
$ ls
README
$ !2
echo hello
hello
$ !e
echo hello
hello

Question 3 (Moderate)

Consider the following code snippet from the shell history implementation:

void
history_add(char *buf)
{
  struct history_line *hlp;
  struct list_elem *e;
  int off = 1;

  hlp = (struct history_line *)malloc(sizeof(struct history_line));
  if(hlp == 0)
    panic("history_add(): malloc");
  hlp->line_str[0] = '\0';

  if(buf[strlen(buf)-1] == '\n')
    off += 1;

  hlp->line_num = history_index;
  strncpy(hlp->line_str, buf, strlen(buf) - off);

  list_push_back(&history, &hlp->elem);
  if(history_index > MAX_HISTORY_LEN){
    e = list_pop_front(&history);
    hlp = list_entry(e, struct history_line, elem);
    free(hlp);
  }

  history_index += 1;
}

Explain what happens when the history list exceeds the maximum number of entries. Why is this approach necessary?

Solution

When the history list exceeds the maximum number of entries (defined by MAX_HISTORY_LEN), the oldest entry is removed from the front of the list using list_pop_front(), and the memory allocated for that entry is freed using free(). This implements a “sliding window” or FIFO (First-In-First-Out) approach to history management.

This approach is necessary because:

  1. It maintains a fixed-size history list, preventing unbounded memory growth
  2. It ensures that only the most recent commands are kept in the history
  3. It properly manages memory by freeing entries that are no longer needed
  4. It maintains the correct numbering of history entries by incrementing the history index

Without this approach, the history list would grow indefinitely, consuming more and more memory as the shell is used.

Question 4 (Moderate)

The history_run function is responsible for executing commands from the history:

void
history_run(char *buf)
{
  struct list_elem *e;
  struct history_line *hlp;
  int ascii = 1;
  int index = 0;
  int found = 0;
  int len = 0;

  if(buf[1] >= '0' && buf[1] <= '9'){
    ascii = 0;
    index = atoi(&buf[1]);
  }

  for(e = list_end(&history); e != list_head(&history); e = list_prev(e)){
    hlp = list_entry(e, struct history_line, elem);

    if(ascii){
      len = strlen(buf) - 2;
      if(strncmp(hlp->line_str, &buf[1], len) == 0){
        found = 1;
        break;
      }
    }else if(hlp->line_num == index){
      found = 1;
      break;
    }
  }

  if(found){
    printf("%s\n", hlp->line_str);
    runcmd_builtin(hlp->line_str);
  }else{
    buf[strlen(buf) - 1] = '\0';
    printf("-sh: %s: event not found\n", buf);
  }
}

Explain how it determines which command to run when a user enters !<text> versus !<num>. What happens if the specified command is not found in the history?

Solution

The history_run function determines which type of history command is being used by checking the character after the ! symbol:

if(buf[1] >= '0' && buf[1] <= '9'){
  ascii = 0;
  index = atoi(&buf[1]);
}

If the character is a digit (0-9), it treats the command as !<num> and converts the number string to an integer using atoi(). Otherwise, it treats the command as !<text> and will search for commands that start with the specified text.

For !<num>, it searches for a history entry with the matching line number:

if(hlp->line_num == index){
  found = 1;
  break;
}

For !<text>, it compares the beginning of each history command with the specified text:

len = strlen(buf) - 2;
if(strncmp(hlp->line_str, &buf[1], len) == 0){
  found = 1;
  break;
}

If the specified command is not found in the history, the function prints an error message:

if(found){
  printf("%s\n", hlp->line_str);
  runcmd_builtin(hlp->line_str);
}else{
  buf[strlen(buf) - 1] = '\0';
  printf("-sh: %s: event not found\n", buf);
}

The error message would look like: -sh: !4: event not found or -sh: !xyz: event not found.

Question 5 (Difficult)

Implement a function called history_clear() that would clear all entries from the command history. Your implementation should properly free all allocated memory and reset the history index. Then, modify the shell to add a new built-in command history -c that calls this function.

Solution

Here’s an implementation of the history_clear() function:

void
history_clear(void)
{
  struct list_elem *e;
  struct history_line *hlp;
  
  // Free all entries in the history list
  while (!list_empty(&history)) {
    e = list_pop_front(&history);
    hlp = list_entry(e, struct history_line, elem);
    free(hlp);
  }
  
  // Reset the history index
  history_index = 1;
}

Now, we need to modify the shell to add the history -c command. We would update the runcmd_builtin function:

void
runcmd_builtin(char *buf)
{
  if(buf[0] == 'c' && buf[1] == 'd' && buf[2] == ' '){
    // Chdir must be called by the parent, not the child.
    buf[strlen(buf)-1] = 0;  // chop \n
    if(chdir(buf+3) < 0)
      fprintf(2, "cannot cd %s\n", buf+3);
    history_add(buf);
  }else if(strncmp("history", buf, 7) == 0){
    // Check if it's "history -c"
    if(strlen(buf) >= 10 && buf[8] == '-' && buf[9] == 'c'){
      history_clear();
      history_add(buf);
    }else{
      history_add(buf);
      history_print();
    }
  }else if(buf[0] == '!')
    history_run(buf);
  else{
    if(buf[0] != '\n' && buf[0] != '\0')
      history_add(buf);

    if(fork1() == 0)
      runcmd(parsecmd(buf));

    wait(0);
  }
}

This modification checks if the command is “history -c” by checking if the string is at least 10 characters long and if the 9th and 10th characters are ‘-‘ and ‘c’, respectively. If it is, it calls the history_clear() function to clear the history. Note that we still add the “history -c” command itself to the history after clearing it.

Free Kernel Memory

Question 1 (Easy)

What is the purpose of the kmem() system call in xv6? What information does it return to the user?

Solution

The purpose of the kmem() system call in xv6 is to provide information about the amount of free kernel memory available in the system. It returns the total number of free kernel memory pages to the user. This allows user programs to monitor the kernel’s memory usage and potentially make decisions based on available resources.

Question 2 (Easy)

In the kmem user program, what is the significance of the constant 4096? Why is this value used in the calculation?

Solution

The constant 4096 represents the page size in bytes for the xv6 operating system. In xv6, memory is allocated in units of pages, and each page is 4096 bytes (4 KB).

This value is used in the calculation to convert the number of free pages (returned by the kmem() system call) into the total number of free bytes:

int free_pages = kmem();
int page_size = 4096; // PGSIZE in xv6 is 4096 bytes
printf("Free kernel memory: %d pages (%d bytes)\n", free_pages, free_pages * page_size);

By multiplying the number of free pages by the page size, the program can display the total amount of free kernel memory in bytes, which is more meaningful to users than just the number of pages.

Question 3 (Moderate)

Describe the steps required to add a new system call to xv6. What files need to be modified to implement the kmem() system call?

Solution

To add a new system call to xv6, the following steps are required:

  1. Define the system call number:
    • Add a new entry in kernel/syscall.h to define the system call number
      #define SYS_kmem    22  // Use the next available number
      
  2. Add the system call to the syscall table:
    • Add an entry in the syscalls array in kernel/syscall.c
      [SYS_kmem]    sys_kmem,
      
  3. Declare the system call handler:
    • Add a function prototype in kernel/defs.h
      // proc.c or kalloc.c
      int             kmem(void);
      
  4. Implement the system call handler:
    • Implement the system call handler function in the appropriate kernel file (likely kernel/kalloc.c for kmem())
      uint64
      sys_kmem(void)
      {
      return kmem();
      }
      
  5. Implement the actual functionality:
    • Implement the core functionality in the appropriate kernel file
      int
      kmem(void)
      {
      // Code to count free memory pages
      return free_page_count;
      }
      
  6. Add user-space declaration:
    • Add the system call declaration to user/user.h so user programs can use it
      int kmem(void);
      
  7. Add system call stub:
    • Add an entry in user/usys.pl to generate the system call stub
      entry("kmem");
      

The files that need to be modified to implement the kmem() system call are:

  • kernel/syscall.h - Define the system call number
  • kernel/syscall.c - Add the system call to the dispatch table
  • kernel/defs.h - Declare the system call handler function
  • kernel/kalloc.c or kernel/proc.c - Implement the system call functionality
  • user/user.h - Add the user-space declaration
  • user/usys.pl - Add the system call stub

Question 4 (Moderate)

The kmem() system call returns the number of free kernel pages. Explain how the kernel tracks free memory pages and how the system call would access this information.

Solution

In xv6, free memory pages are tracked using a linked list called the “free list”. Here’s how it works:

  1. Free List Structure:
    • The kernel maintains a linked list of free memory pages
    • Each free page contains a pointer to the next free page at its beginning
    • The head of this list is stored in a global variable called kmem.freelist
    • The list is protected by a spinlock (kmem.lock) to ensure thread safety
  2. Memory Allocation:
    • When memory is allocated using kalloc(), a page is removed from the free list
    • When memory is freed using kfree(), the page is added back to the free list
  3. Implementing kmem():
    • To count free pages, the system call would traverse the free list
    • It would acquire the lock, count the number of pages in the list, and release the lock

Here’s a simplified implementation of how the kmem() system call might access this information:

int
kmem(void)
{
  struct run *r;
  int count = 0;
  
  acquire(&kmem.lock);
  r = kmem.freelist;
  while(r){
    count++;
    r = r->next;
  }
  release(&kmem.lock);
  
  return count;
}

This function acquires the lock protecting the free list, traverses the list counting each page, releases the lock, and returns the count.

Question 5 (Difficult)

Extend the kmem program to also display the total amount of kernel memory (both free and used) and calculate the percentage of memory that is currently in use. What additional system call(s) would you need to implement to support this functionality?

Solution

To extend the kmem program to display total memory and usage percentage, we would need an additional system call to get the total amount of kernel memory. Let’s call it kmem_total().

First, let’s implement the new system call:

  1. Add the system call number:
    // kernel/syscall.h
    #define SYS_kmem_total  23
    
  2. Add to syscall table:
    // kernel/syscall.c
    [SYS_kmem_total]  sys_kmem_total,
    
  3. Declare the handler:
    // kernel/defs.h
    int             kmem_total(void);
    
  4. Implement the handler: ```c // kernel/kalloc.c uint64 sys_kmem_total(void) { return kmem_total(); }

int kmem_total(void) { // In xv6, we can calculate this based on the memory layout // The kernel memory starts at KERNBASE and ends at PHYSTOP // These are defined in kernel/memlayout.h return (PHYSTOP - KERNBASE) / PGSIZE; }


5. **Add user-space declaration**:
```c
// user/user.h
int kmem_total(void);
  1. Add system call stub:
    // user/usys.pl
    entry("kmem_total");
    

Now, let’s modify the kmem program to use this new system call:

#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

int
main(int argc, char *argv[])
{
  int free_pages = kmem();
  int total_pages = kmem_total();
  int used_pages = total_pages - free_pages;
  int page_size = 4096; // PGSIZE in xv6 is 4096 bytes
  
  // Calculate percentage used (avoid division by zero)
  int percent_used = 0;
  if(total_pages > 0)
    percent_used = (used_pages * 100) / total_pages;
  
  printf("Kernel memory:\n");
  printf("  Total: %d pages (%d bytes)\n", 
         total_pages, total_pages * page_size);
  printf("  Free:  %d pages (%d bytes)\n", 
         free_pages, free_pages * page_size);
  printf("  Used:  %d pages (%d bytes, %d%%)\n", 
         used_pages, used_pages * page_size, percent_used);
  
  exit(0);
}

This extended program now displays:

  • Total kernel memory (in pages and bytes)
  • Free kernel memory (in pages and bytes)
  • Used kernel memory (in pages, bytes, and as a percentage)

The additional system call kmem_total() provides the total number of kernel memory pages, which allows the program to calculate the used memory and usage percentage.

Pipe Count

Question 1 (Easy)

What is the purpose of the pipect(int fd) system call? What does it return when the file descriptor is not a pipe?

Solution

The purpose of the pipect(int fd) system call is to return the number of bytes available to be read from a pipe. It allows a program to check how much data is waiting in a pipe without actually reading the data.

If the file descriptor is not a pipe or is not a valid file descriptor, the system call returns 0. This is a simple way to handle error cases without requiring additional error codes.

Question 2 (Easy)

When running the command echo foo | pipect, the output is 4. Explain why the count is 4 and not 3, given that “foo” is only 3 characters.

Solution

The count is 4 because the echo command automatically adds a newline character (\n) at the end of its output unless specifically told not to (using the -n option). So, when echo foo is executed, it actually outputs foo\n, which is 4 bytes:

  • ‘f’ (1 byte)
  • ‘o’ (1 byte)
  • ‘o’ (1 byte)
  • ‘\n’ (1 byte)

The pipect command then counts these 4 bytes in the pipe. If you wanted to see just 3 bytes, you could use echo -n foo | pipect which would output 3.

Question 3 (Moderate)

Describe how pipes are implemented in xv6. What data structures are used to represent pipes, and how is data stored in them?

Solution

In xv6, pipes are implemented as a circular buffer with separate read and write pointers. Here’s how they are structured:

  1. Pipe Data Structure:
    struct pipe {
      struct spinlock lock;
      char data[PIPESIZE];  // Circular buffer
      uint nread;           // Number of bytes read
      uint nwrite;          // Number of bytes written
      int readopen;         // Read fd is still open
      int writeopen;        // Write fd is still open
    };
    
  2. Key Components:
    • lock: A spinlock to protect the pipe data structure from concurrent access
    • data: A fixed-size circular buffer (typically 512 bytes) that holds the data
    • nread: The index where the next read will occur
    • nwrite: The index where the next write will occur
    • readopen: Flag indicating if the read end of the pipe is open
    • writeopen: Flag indicating if the write end of the pipe is open
  3. Data Flow:
    • Data is written to the pipe at the position indicated by nwrite
    • Data is read from the pipe at the position indicated by nread
    • When nwrite reaches the end of the buffer, it wraps around to the beginning
    • When nread reaches the end of the buffer, it also wraps around
    • The pipe is full when nwrite has wrapped around and reached nread
    • The pipe is empty when nread equals nwrite
  4. File Descriptors:
    • A pipe is represented by two file descriptors: one for reading and one for writing
    • These file descriptors point to the same pipe structure
    • The read file descriptor has writable set to 0
    • The write file descriptor has readable set to 0
  5. Blocking Behavior:
    • Reading from an empty pipe blocks until data is available
    • Writing to a full pipe blocks until space is available
    • If all read ends are closed, writing to a pipe returns an error
    • If all write ends are closed, reading from a pipe returns 0 (EOF) after all data is read

This implementation allows for efficient inter-process communication with proper synchronization.

Question 4 (Moderate)

The pipect(int fd) system call needs to determine if a file descriptor refers to a pipe. Explain how the system call would check this and access the pipe’s data structure to determine the number of bytes available.

Solution

The pipect(int fd) system call would follow these steps to check if a file descriptor refers to a pipe and determine the number of bytes available:

  1. Validate the file descriptor:
    • Check if fd is within the valid range (0 to NOFILE-1)
    • Get the file structure from the process’s file table: f = myproc()->ofile[fd]
    • Check if the file is open: if(f == 0) return 0;
  2. Check if it’s a pipe:
    • Check the file type: if(f->type != FD_PIPE) return 0;
    • If it’s not a pipe, return 0 as specified
  3. Access the pipe structure:
    • Get the pipe structure: p = (struct pipe*)f->pipe;
  4. Calculate bytes available:
    • Acquire the pipe lock: acquire(&p->lock);
    • Calculate bytes available:
      • If the pipe is empty (p->nread == p->nwrite), return 0
      • If p->nwrite > p->nread, return p->nwrite - p->nread
      • If p->nwrite < p->nread (wrapped around), return PIPESIZE - (p->nread - p->nwrite)
    • Release the lock: release(&p->lock);

Here’s a simplified implementation:

int
pipect(int fd)
{
  struct file *f;
  struct pipe *p;
  int count;
  
  // Validate file descriptor
  if(fd < 0 || fd >= NOFILE || (f = myproc()->ofile[fd]) == 0)
    return 0;
  
  // Check if it's a pipe
  if(f->type != FD_PIPE)
    return 0;
  
  // Access pipe structure
  p = (struct pipe*)f->pipe;
  
  // Calculate bytes available
  acquire(&p->lock);
  if(p->nread == p->nwrite)
    count = 0;  // Empty pipe
  else if(p->nwrite > p->nread)
    count = p->nwrite - p->nread;  // Simple case
  else
    count = PIPESIZE - (p->nread - p->nwrite);  // Wrapped around
  release(&p->lock);
  
  return count;
}

This implementation properly handles all cases, including empty pipes and the case where the write pointer has wrapped around in the circular buffer.

Process Status

Question 1 (Easy)

What information does the ps command display about each process in xv6? List the fields shown in the output.

Solution

The ps command in xv6 displays the following information about each process:

  1. PID - Process ID: A unique identifier for each process
  2. STATE - Process state: Indicates whether the process is running, sleeping, etc.
  3. SIZE - Memory size: The amount of memory (in bytes) used by the process
  4. NAME - Process name: The name of the program being executed

Example output:

PID     STATE   SIZE    NAME
1       sleep   20480   init
2       sleep   24576   sh
4       run     24576   ps

These fields provide a basic overview of the processes running in the system, allowing users to monitor system activity and resource usage.

Question 2 (Easy)

Explain the purpose of the struct procinfo defined in getproc.h. Why is this structure needed for the getprocs() system call?

Solution

The struct procinfo defined in getproc.h serves as a data structure for transferring process information from the kernel to user space. Here’s its definition:

struct procinfo {
  int pid;                // Process ID
  enum procstate state;   // Process state
  uint64 sz;              // Size of process memory (bytes)
  char name[16];          // Process name
};

This structure is needed for the getprocs() system call for several reasons:

  1. Data Encapsulation: It encapsulates all the relevant process information in a single structure, making it easier to pass between kernel and user space.

  2. Memory Safety: It defines a fixed-size structure that can be safely copied between kernel and user space without risking buffer overflows or memory corruption.

  3. Information Hiding: It exposes only the necessary process information to user programs, hiding internal kernel details and maintaining proper abstraction.

  4. Standardization: It provides a standardized format for process information, ensuring that all programs using the getprocs() system call receive consistent data.

  5. Shared Definition: By placing this structure in a header file that can be included by both kernel and user code, it ensures that both sides have the same understanding of the data format.

Without this structure, the getprocs() system call would need to use multiple parameters or a more complex mechanism to transfer process information, making it more difficult to use and more prone to errors.

Question 3 (Moderate)

The ps program defines an array of strings called states to map process state values to human-readable strings. Explain why this mapping is necessary and how it’s used in the program.

Solution

The states array in the ps program maps numeric process state values to human-readable string representations:

char *states[] = {
  [UNUSED]    "unused",
  [USED]      "used",
  [SLEEPING]  "sleep ",
  [RUNNABLE]  "runble",
  [RUNNING]   "run   ",
  [ZOMBIE]    "zombie"
};

This mapping is necessary for several reasons:

  1. Human Readability: The internal representation of process states in the kernel is numeric (an enum), which is efficient for computation but not user-friendly. The string representations make the output more understandable to humans.

  2. Consistent Formatting: The strings are padded to a consistent length (6 characters), which helps maintain aligned columns in the output, making it easier to read.

  3. Abstraction: It abstracts the internal representation from the display format, allowing changes to either without affecting the other.

The mapping is used in the program when printing process information:

printf("%d\t%s\t%ld\t%s\n",
  procs[i].pid,
  states[procs[i].state],  // Convert state enum to string
  procs[i].sz,
  procs[i].name);

When displaying each process, the program uses the process’s state value as an index into the states array to get the corresponding string representation. This is then included in the formatted output.

For example, if a process has a state value of RUNNING (which is 4 in the enum), the program will use states[4], which is "run ", in the output. This makes the output much more meaningful to users than if it just displayed the number 4.

Question 4 (Moderate)

Describe how the getprocs() system call works. How does it collect information about all processes in the system and make it available to user programs?

Solution

The getprocs() system call works by iterating through the kernel’s process table, collecting information about each active process, and copying that information to a user-provided buffer.

int
getprocs(uint64 addr, int max)
{
  struct proc *p;
  struct procinfo pi;
  int count = 0;

  // Iterate through the process table
  for(p = proc; p < &proc[NPROC] && count < max; p++){
    acquire(&p->lock);

    if(p->state != UNUSED){
      // Fill in the procinfo structure
      pi.pid = p->pid;
      pi.state = p->state;
      pi.sz = p->sz;
      safestrcpy(pi.name, p->name, sizeof(pi.name));

      // Copy to user space
      if(copyout(myproc()->pagetable, addr + count * sizeof(pi),
        (char *)&pi, sizeof(pi)) < 0){
        release(&p->lock);
        return -1;
      }

      count++;
    }

    release(&p->lock);
  }

  return count;
}

Here’s a detailed explanation of how it works:

  1. System Call Interface:
    int getprocs(struct procinfo *procs, int max_procs)
    
    • procs: A pointer to an array of procinfo structures in user space
    • max_procs: The maximum number of processes to return information for
    • Returns: The number of processes for which information was collected
  2. Process Table Iteration:
    • The kernel maintains a global array of process control blocks (PCBs) called proc
    • The system call iterates through this array, looking for active processes
    • For each active process (state != UNUSED), it collects the relevant information
  3. Information Collection:
    • For each active process, it extracts:
      • Process ID (pid)
      • Process state (state)
      • Process memory size (sz)
      • Process name (name)
    • This information is stored in a temporary procinfo structure
  4. User Space Transfer:
    • The system call uses copyout() to safely copy the collected information from kernel space to the user-provided buffer
    • It ensures that it doesn’t exceed the buffer size specified by max_procs
    • It keeps track of how many processes it has copied information for
  5. Return Value:
    • The system call returns the number of processes for which information was collected
    • This allows the user program to know how many entries in the array are valid

This implementation safely transfers process information from the kernel to user space, allowing user programs like ps to display information about all processes in the system.

Question 5 (Difficult)

Extend the ps command to add a new option -p <pid> that displays detailed information about a specific process identified by its PID. The detailed information should include all the standard fields plus the parent PID and the number of open file descriptors. What modifications would you need to make to the struct procinfo, the getprocs() system call, and the ps program to implement this feature?

Solution

To implement the -p <pid> option for the ps command, we need to make several modifications:

  1. Extend struct procinfo in getproc.h:
    struct procinfo {
      int pid;                // Process ID
      int ppid;               // Parent Process ID (new field)
      enum procstate state;   // Process state
      uint64 sz;              // Size of process memory (bytes)
      int open_files;         // Number of open file descriptors (new field)
      char name[16];          // Process name
    };
    
  2. Modify the getprocs() system call: We need to modify the system call to collect the additional information and to support filtering by PID:
int
getprocs(struct procinfo *procs, int max_procs, int pid_filter)
{
  struct proc *p;
  struct procinfo pi;
  int count = 0;
  int open_count;
  
  // Iterate through the process table
  for(p = proc; p < &proc[NPROC]; p++){
    // Skip unused process slots
    if(p->state == UNUSED)
      continue;
    
    // If pid_filter is non-zero, only include the specified process
    if(pid_filter != 0 && p->pid != pid_filter)
      continue;
    
    // Collect process information
    pi.pid = p->pid;
    pi.ppid = p->parent ? p->parent->pid : 0;  // Get parent PID
    pi.state = p->state;
    pi.sz = p->sz;
    
    // Count open file descriptors
    open_count = 0;
    for(int fd = 0; fd < NOFILE; fd++){
      if(p->ofile[fd])
        open_count++;
    }
    pi.open_files = open_count;
    
    strncpy(pi.name, p->name, sizeof(pi.name));
    
    // Copy to user space
    if(copyout(myproc()->pagetable, (uint64)&procs[count], 
               (char*)&pi, sizeof(pi)) < 0)
      return -1;
    
    // Increment count and check if we've reached the limit
    count++;
    if(count >= max_procs)
      break;
    
    // If we found the specific PID and pid_filter is non-zero, we're done
    if(pid_filter != 0 && p->pid == pid_filter)
      break;
  }
  
  return count;
}
  1. Update the user-space declaration in user/user.h:
    int getprocs(struct procinfo*, int, int);
    
  2. Modify the ps program: ```c #include “kernel/types.h” #include “kernel/getproc.h” #include “user/user.h”

char *states[] = { [UNUSED] “unused”, [USED] “used”, [SLEEPING] “sleep “, [RUNNABLE] “runble”, [RUNNING] “run “, [ZOMBIE] “zombie” };

void print_usage(void) { fprintf(2, “Usage: ps [-p pid]\n”); exit(1); }

int main(int argc, char *argv[]) { struct procinfo procs[64]; // Assume max 64 processes int nprocs, i; int pid_filter = 0; // Default: show all processes int detailed = 0; // Default: standard output

// Parse command line arguments if(argc > 1){ if(argc != 3 || strcmp(argv[1], “-p”) != 0){ print_usage(); } pid_filter = atoi(argv[2]); detailed = 1; }

// Get process information nprocs = getprocs(procs, 64, pid_filter); if(nprocs < 0){ printf(“getprocs failed\n”); exit(1); }

if(detailed){ // Detailed output for a specific process if(nprocs == 0){ printf(“No process with PID %d found\n”, pid_filter); exit(1); }

printf("DETAILED PROCESS INFORMATION:\n");
printf("PID:       %d\n", procs[0].pid);
printf("PPID:      %d\n", procs[0].ppid);
printf("STATE:     %s\n", states[procs[0].state]);
printf("SIZE:      %d bytes\n", procs[0].sz);
printf("OPEN FDs:  %d\n", procs[0].open_files);
printf("NAME:      %s\n", procs[0].name);   }else{
// Standard output for all processes
printf("PID\tSTATE\tSIZE\tNAME\n");

for(i = 0; i < nprocs; i++)
  if(procs[i].state != UNUSED)
    printf("%d\t%s\t%d\t%s\n",
      procs[i].pid,
      states[procs[i].state],
      procs[i].sz,
      procs[i].name);   }

exit(0); } ```

These modifications allow the ps command to:

  1. Accept a -p <pid> option to display detailed information about a specific process
  2. Show additional information (parent PID and open file descriptors) for the specified process
  3. Continue to support the standard behavior of listing all processes when no options are provided

The changes to the getprocs() system call make it more flexible by adding a pid_filter parameter. When this parameter is non-zero, the system call only collects information for the process with the matching PID. This makes the implementation more efficient when only information about a specific process is needed.