COMP 3000 2021F Assignment 4 Solutions 1. [2] System call code in the Linux kernel run in the context of a task. What is the variable name representing that task? What type is that variable? A: current, struct task_struct 2. [2] When making a read call on /dev/ones, the ones_read() function is called. The read system call takes three arguments, but ones_read() takes four. Explain at a high level how each argument for ones_read() comes from the arguments passed to a read system call. A: Read has this declaration: ssize_t read(int fd, void *buf, size_t count); While ones_read is as follows: static ssize_t ones_read(struct file *f, char *buf, size_t len, loff_t *offset) The buf -> buf and count -> len directly. (buf has to be treated as a userspace pointer in ones_read, but its value is exactly the same.) fd is used to look up the appropriate file struct in the set of open files associated with the current process/task. offset also comes from this file struct, it is the process's current position in the file. It is passed in separately so it can be modified without messing with f. 3. [2] The ones_fops struct is declared in the ones module on lines 51-55 and referenced on line 69. What does ones_fops specify? Does it have any fields that aren't initialized? Explain briefly. A: It specifies the functions that should be called to handle files-related system calls on the /dev/ones character device. Thus, it defines what should happen on a open, read, and release (close). It has many fields that are not initialized, e.g., for writing, mmap. We can seem them all if we look up struct file_operations. 4. [2] If we replace line 38 of the ones module with buf[i] = '1';, how does the behavior of the ones module change? Why? A: The module will generate a kernel oops for each write attempt because we're accessing a userspace buffer as if it was in kernel space, and that isn't allowed - you have to go through put_user/get_user or something equivalent to make the access work. 5. [4] How can we make the ones module act as if there are exactly 10,000 1's? Specifically, if we read /dev/ones from the beginning we should get 10,000 1's at which point the file ends. Note that this should work no matter the size of the buffer used for a read system call. (You may want to test using dd.) A: We just need to replace ones_read with the following modified version. Note that we just have to do the right bookkeeping to make sure we return the right number of ones based on the file offset and size of the buffer (buflen here rather than len in the original). static ssize_t ones_read(struct file *f, char *buf, size_t buflen, loff_t *offset) { const size_t MAXONES = 10000; size_t i; int len; /* If we're bigger than MAXONES, then don't return anything */ if (*offset > MAXONES) { return 0; } /* if they aren't at the start of the file, proportionately return fewer ones */ len = MAXONES - *offset; /* never return more ones than were requested */ if (len > buflen) { len = buflen; } for (i = 0; i < len; i++) { put_user('1', buf++); } /* update the offset to reflect how many bytes have been read */ *offset += i; return i; } 6. [3] Change 3000pc-fifo.c as follows: * Delete lines 192-196 (the lines that call pipe()) * Replace lines 200-209 with the following: if (pid == 0) { /* Producer */ pipefd[1] = open("the_pipe", O_WRONLY); producer(count, pipefd[1], prod_interval); } else { /* Consumer */ pipefd[0] = open("the_pipe", O_RDONLY); consumer(count, pipefd[0], con_interval); } * Add the following include lines to the top (for the call to open): #include #include #include Note that this code assumes that the_pipe exists and is writable. 6a. [1] How does this modified version behave if the_pipe is an empty regular file? A: If it is an empty regular file, it will mostly work, except that an indeterminate number of entries will be invalid if both the producer and consumer are running at full speed. Sometimes I see 1, sometimes I see 50. It all depends. Running the following multiple times shows the variation: cat /dev/null > the_pipe; ./3000pc-fifo 100 0 0 This happens because there is no mutual exclusion on access to the file. 6b. [1] Does its behavior change if we run it multiple times (without emptying the_pipe)? How? A: It seems to mostly work; however, it leaves behind a file which is the size of the maximum number of items produced. 6c. [1] Can we create a file that will make the modified 3000pc-fifo behave like the original? Explain briefly. A: Yes, we can just make a named pipe: mkfifo the_pipe With the named pipe it will function just like the it did before. 7. [2] If we replace the call to mmap() in 3000pc-rendezvous-timeout.c (lines 399-401) with a call to malloc(), how will the behavior of the program change? Why? A: If we replace s = (shared *) mmap(NULL, sizeof(shared), PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0); with s = malloc(sizeof(shared)); then 3000pc-rendezvous-timeout will lock up and not make any progress except for error messages such as: ERROR: Nothing for consumer after waiting 2000 times! ERROR: No room for producer after waiting 2000 times! This happens because the memory is no longer shared between the producer and consumer, and thus the consumer has no way of seeing what the producer has produced. 8. [3] What is the relationship between nonempty_mutex and queue_nonfull in 3000pc-rendezvous-timeout.c? Specifically, what is each for, and why are both necessary? A: nonempty_mutex ensures mutual exclusion when accessing queue_nonempty, a condition variable. The queue_nonempty condition variable is used by the producer to wait for the queue to become non-empty (i.e., for there to be room in the queue in which to produce). The consumer signals the producer to wake up via a call to pthread_cond_signal on queue_nonempty. Some condition variables are inherently atomic; however, it appears this one isn't and thus needs to be protected by a mutex.