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:
- It maintains a fixed-size history list, preventing unbounded memory growth
- It ensures that only the most recent commands are kept in the history
- It properly manages memory by freeing entries that are no longer needed
- 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:
- 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
- Add a new entry in
- Add the system call to the syscall table:
- Add an entry in the
syscalls
array inkernel/syscall.c
[SYS_kmem] sys_kmem,
- Add an entry in the
- Declare the system call handler:
- Add a function prototype in
kernel/defs.h
// proc.c or kalloc.c int kmem(void);
- Add a function prototype in
- Implement the system call handler:
- Implement the system call handler function in the appropriate kernel file (likely
kernel/kalloc.c
forkmem()
)uint64 sys_kmem(void) { return kmem(); }
- Implement the system call handler function in the appropriate kernel file (likely
- 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; }
- Implement the core functionality in the appropriate kernel file
- Add user-space declaration:
- Add the system call declaration to
user/user.h
so user programs can use itint kmem(void);
- Add the system call declaration to
- Add system call stub:
- Add an entry in
user/usys.pl
to generate the system call stubentry("kmem");
- Add an entry in
The files that need to be modified to implement the kmem()
system call are:
kernel/syscall.h
- Define the system call numberkernel/syscall.c
- Add the system call to the dispatch tablekernel/defs.h
- Declare the system call handler functionkernel/kalloc.c
orkernel/proc.c
- Implement the system call functionalityuser/user.h
- Add the user-space declarationuser/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:
- 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
- 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
- When memory is allocated using
- 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:
- Add the system call number:
// kernel/syscall.h #define SYS_kmem_total 23
- Add to syscall table:
// kernel/syscall.c [SYS_kmem_total] sys_kmem_total,
- Declare the handler:
// kernel/defs.h int kmem_total(void);
- 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);
- 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:
- 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 };
- Key Components:
lock
: A spinlock to protect the pipe data structure from concurrent accessdata
: A fixed-size circular buffer (typically 512 bytes) that holds the datanread
: The index where the next read will occurnwrite
: The index where the next write will occurreadopen
: Flag indicating if the read end of the pipe is openwriteopen
: Flag indicating if the write end of the pipe is open
- 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 reachednread
- The pipe is empty when
nread
equalsnwrite
- Data is written to the pipe at the position indicated by
- 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
- 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:
- 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;
- Check if
- 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
- Check the file type:
- Access the pipe structure:
- Get the pipe structure:
p = (struct pipe*)f->pipe;
- Get the pipe structure:
- 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
, returnp->nwrite - p->nread
- If
p->nwrite < p->nread
(wrapped around), returnPIPESIZE - (p->nread - p->nwrite)
- If the pipe is empty (
- Release the lock:
release(&p->lock);
- Acquire the pipe 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:
- PID - Process ID: A unique identifier for each process
- STATE - Process state: Indicates whether the process is running, sleeping, etc.
- SIZE - Memory size: The amount of memory (in bytes) used by the process
- 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:
Data Encapsulation: It encapsulates all the relevant process information in a single structure, making it easier to pass between kernel and user space.
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.
Information Hiding: It exposes only the necessary process information to user programs, hiding internal kernel details and maintaining proper abstraction.
Standardization: It provides a standardized format for process information, ensuring that all programs using the
getprocs()
system call receive consistent data.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:
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.
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.
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:
- System Call Interface:
int getprocs(struct procinfo *procs, int max_procs)
procs
: A pointer to an array ofprocinfo
structures in user spacemax_procs
: The maximum number of processes to return information for- Returns: The number of processes for which information was collected
- 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
- The kernel maintains a global array of process control blocks (PCBs) called
- 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
- For each active process, it extracts:
- 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
- The system call uses
- 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:
- Extend
struct procinfo
ingetproc.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 };
- 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;
}
- Update the user-space declaration in
user/user.h
:int getprocs(struct procinfo*, int, int);
- 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:
- Accept a
-p <pid>
option to display detailed information about a specific process - Show additional information (parent PID and open file descriptors) for the specified process
- 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.