1 PROJECT 2 GRADING ADVICE
2 ========================
4 This file contains advice for grading project 2. General advice for
5 Pintos grading is in README, which you should read first.
12 Most correct solutions don't introduce any new data structures for
15 If any global variables are introduced, be suspicious about the
16 possibility for race conditions.
20 The "Program Startup Details" section in the User Programs chapter
21 of the Pintos manual describes what needs to be done.
23 There are two main ways to put argv[] in the correct order:
25 - Many solutions use strtok_r() to divide the arguments into
26 space-separated strings, storing a pointer to each argument
27 in a temporary storage area, then push each pointer on the
28 stack in reverse order.
30 - Another approach is to push the arguments obtained by
31 strtok_r() on the user stack directly, then reverse their
32 order when finished. This is what the sample solution does.
34 Students must specify how they avoid overflow:
36 - One acceptable approach is to check for overflow just before
37 pushing each item on the stack.
39 - Another acceptable approach is to make two passes. In the
40 first pass, calculate how many bytes of stack space are
41 required. Abort at this point if the stack would overflow.
42 Then, in the second pass, actually set up the stack.
44 - A third acceptable approach is to allocate additional stack
45 pages as the currently allocate page or pages are exhausted.
46 If this is done, verify that the case where no more memory
47 is available is correctly handled (by returning an error and
48 ensuring that the pages already allocated are freed).
50 Few groups choose this approach.
52 - Some students assume that any command line under 4,096 bytes
53 long will fit. This is wrong: some space is required for
54 the pointers in argv[], as well as a little extra for other
55 data that goes on the stack and some space for the user
56 process's use as well.
58 - Generalizing the previous approach, some students set some
59 other fixed limit on command-line length in bytes. Usually
60 they don't properly consider the consequences. Every two
61 bytes in a command line can require four bytes for a pointer
62 in argv[], as in a command line that consists of alternating
63 space and non-space characters. Thus, N bytes of
64 command-line arguments can require N + 4(N / 2) = 3N bytes
65 of stack space. Setting 3N = 4,096 we obtain N = 1,365.
66 Thus, any solution that sets a fixed cap on command-line
67 length greater than 1,365 is incorrect. Preferably, any
68 fixed limit should be significantly less than that.
70 - Some students believe that there is a 128-byte limit on the
71 length of a command line. This is wrong: the 128-byte limit
72 only applies to the "pintos" utility's command line. It
73 does not apply to the command line passed to the "exec"
76 - Some groups limit the number of command line arguments to a
77 fixed value. There's nothing wrong with that as long as
78 it's sufficiently large, e.g. 512.
80 One common fallacy is an assumption that each string must be
81 word-aligned for performance. This is not true. It's okay if
82 they align strings, because it's harmless, but stating that it
83 improves performance should incur a small deduction.
87 strtok() uses a static object to hold the starting position for
88 the next token. Thus, it isn't safe for concurrent use in
89 different threads (the problem here) or even for nested use within
92 strtok_r() solves the problem by requiring the caller to provide
93 storage for the starting position for the next token.
97 - Some students believe that strtok_r() does not modify the
98 string it tokenizes whereas strtok() does. This is wrong:
99 both functions modify the string that they tokenize.
101 - Some students think strtok() uses a "static buffer" for
102 parsing. This is wrong: it modifies the string provided and
103 stores a pointer into it statically.
107 Some correct answers:
109 - Increased flexibility: a variety of shells can accept
110 varying syntaxes without any need to change the kernel.
112 - Simplifies the kernel and improves its security: a simpler
113 kernel is a kernel less likely to have important bugs or
114 security holes. This is especially true given the
115 complicated parsing rules of real-world shells.
119 - Saves time and disk space: doing parsing in user space or
120 kernel space doesn't have a strong time or space trade-off,
121 but some students think so.
123 - Relative paths: some students think that relative paths are
124 implemented by the shell, but this is wrong.
126 - Fewer context switches: where parsing is done has no effect
127 on the number of context switches required to start a
128 process. (It might be possible to justify this claim by
129 giving specific details.)
131 - Policy enforcement: some students think that the shell is in
132 charge of enforcing system security policies, but this is
135 Check the submitted code, by hand, for the following:
137 1. That the code used for argument passing is readable. This is
138 probably near the end of userprog/process.c, in or near
139 setup_stack(). In particular, many stack "push" operations are
140 required, so the code to do this should either be simple
141 (e.g. "*--p = something;") or, if it is more complicated,
142 abstracted into a function or set of functions.
149 This question has a pretty big answer, because system calls need
150 quite a few data structures. The guidelines for B1 are therefore
151 broken down into B1(i) through B1(iv), below, to make them easier
156 A global lock or semaphore for file system synchronization is
159 /* Serializes file system operations. */
160 static struct lock fs_lock;
164 Some mechanism for tracking file descriptors is needed. There are
165 three common variants (see B2 for more on this topic):
167 - Per-thread list: adds a list to struct thread, like so:
169 struct list fds; /* List of file descriptors. */
171 Each member of the list includes at least a file and a fd
174 /* A file descriptor, for binding a file handle to a file. */
175 struct file_descriptor
177 struct list_elem elem; /* List element. */
178 struct file *file; /* File. */
179 int handle; /* File handle. */
182 This case also requires a way to come up with unique file
183 descriptors. The simplest and best way is to add a counter
184 to struct thread, e.g.:
186 int next_handle; /* Next handle value. */
188 Another alternative is to use a global counter, but this
189 requires adding a lock as well. (It's possible to use the
190 global file system lock or some other lock for this, but I
191 haven't seen that done in practice.) Deduct points if
192 synchronization is omitted:
194 static int next_fd = 2; /* Next file descriptor. */
195 static struct lock fd_lock; /* Protects next_fd. */
197 - Per-thread array: adds an array of 128 elements to struct
198 thread (128 because the assignment states that as a minimum
201 struct file *fds[128];
203 Some solutions include some extra members to speed up
204 finding an empty slot.
206 - Global hash table: adds a global hash table that maps from a
207 descriptor number to a "struct file *". Check that there is
208 also a new hash_elem in struct thread in this case.
210 Some way to generate unique file descriptors is needed,
211 usually a global counter. A lock or other synchronization
214 The global data for this case looks something like this:
216 static struct hash fd_hash;
217 static int fd_next = 2;
218 static struct lock fd_lock;
220 The hash table elements look something like this:
222 struct file_descriptor
224 struct hash_elem hash_elem;
230 Notice the inclusion of the "owner" member, which ensures
231 that a fd can only be used by the process that opened it.
232 If there's no mechanism to ensure this, deduct points.
236 Data structures for implementing the "wait" system call are needed
237 (see B5 and B8 for more on this topic). Fundamentally, "wait"
238 requires communication between a process and its children, usually
239 implemented through shared data. The shared data might be added
240 to struct thread, but many solutions separate it into a separate
241 structure. At least the following must be shared between a parent
242 and each of its children:
244 - Child's exit status, so that "wait" can return it.
246 - Child's thread id, for "wait" to compare against its
249 - A way for the parent to block until the child dies (usually
252 - A way for the parent and child to tell whether the other is
253 already dead, in a race-free fashion (to ensure that their
254 shared data can be freed).
256 The sample solution uses a new "struct wait_status" for this
259 /* Tracks the completion of a process.
260 Reference held by both the parent, in its `children' list,
261 and by the child, in its `wait_status' pointer. */
264 struct list_elem elem; /* `children' list element. */
265 struct lock lock; /* Protects ref_cnt. */
266 int ref_cnt; /* 2=child and parent both alive,
267 1=either child or parent alive,
268 0=child and parent both dead. */
269 tid_t tid; /* Child thread id. */
270 int exit_code; /* Child exit code, if dead. */
271 struct semaphore dead; /* 1=child alive, 0=child dead. */
274 If the shared data is integrated into struct thread, then one of
275 the cleanest ways to do it is to use a pair of semaphores, both
276 initially set to 0, like so (see B5/B8 below for more information
277 on the usage of these semaphores):
279 int exit_code; /* Process's exit status. */
280 struct semaphore exiting; /* Up'd when process ready to exit. */
281 struct semaphore may_exit; /* Up'd after parent reads exit code. */
283 Of course, each thread needs a way to find the shared data for
284 each of its children and, if shared data is not stored in struct
285 thread, its own shared data as well. The sample solution adds a
286 pair of members to struct thread:
288 struct wait_status *wait_status; /* This process's completion state. */
289 struct list children; /* Completion status of children. */
291 If the shared data is integrated into struct thread, then a list
292 of children and a corresponding list element is needed:
294 struct list children; /* List of child processes. */
295 struct list_elem child_elem; /* Used for `children' list. */
297 Most solutions add an exit code member to struct thread, even if
298 there is an exit code member in a separate shared data structure.
303 Some data is needed to allow a process that calls "exec" to wait
304 until the new process completes loading. The sample solution, and
305 some student submissions, just add a new structure. A pointer to
306 the new structure is passed to the new thread as the "void *"
307 pointer argument to thread_create(). Here is the sample
308 solution's structure:
310 /* Data structure shared between process_execute() in the
311 invoking thread and execute_thread() in the newly invoked
315 const char *file_name; /* Program to load. */
316 struct semaphore load_done; /* "Up"ed when loading complete. */
317 struct wait_status *wait_status;/* Child process. */
318 bool success; /* Program successfully loaded? */
321 Many student submissions instead add members to struct thread to
322 do the same things, which is fine.
326 See B1(ii) above for information on the basic data structures used
327 for file descriptors. Part of the answer to this question might
330 Globally unique and per-process unique file descriptors are both
333 If a global counter is used to generate file descriptors, there
334 must be some mention of synchronization.
336 If an array totalling 512 bytes or more (e.g. 128 elements of
337 "int" or pointer type) is added to struct thread, then there must
338 be some mention that this is a large amount of data to add to
339 struct thread. This can be mentioned here, or in B1(ii), or in
340 B10 below. See the "struct thread" section in the Reference Guide
341 chapter of the Pintos manual for explanation.
343 If the fd counter skips the first 3 fd values, with mention that
344 this is for stdin, stdout, and stderr, then it must also mention
345 that Pintos does not have stderr.
347 There seems to be some confusion about locking here among
348 students. No locking is necessary for per-thread data structures,
349 such as a thread-specific list of file descriptors. Locking is
350 required for data that might be concurrently accessed by multiple
353 There is no need for submissions to handle overflow of the counter
354 used for generating fds.
358 This question was badly phrased in previous quarters, so there's
359 no pool of good student answers to compare against.
361 The answer should say which of the techniques described in
362 "Accessing User Memory" in the assignment is used. It should
363 describe a few of the implementation details, such as interfaces
364 of functions used to read or write user memory.
366 A few students believe that writing to the "eax" member of struct
367 intr_frame is a form of writing to user memory. This is wrong:
368 struct intr_frame is in the kernel stack. Modifying its "eax"
369 member changes the value of the EAX register seen by the process
370 when a system call returns, not any kind of user memory.
372 Occasionally, a group extends the page fault-based method
373 described in the Pintos manual. The method described there has a
374 little bit of overhead in that it requires the EAX register to be
375 set up with the address of a recovery handler before accessing
376 memory. This is necessary for the page fault handler to know what
377 to do. An alternative without this overhead is to maintain a
378 table that associates EIP values of potentially faulting
379 instructions with their recovery handlers. This technique is used
380 in the Linux kernel and elsewhere to good effect.
384 In the first part of this question, students should address their
385 implementation. In the second part, they should address an ideal
386 implementation. They need to answer both parts, although the
387 answers may be very brief as long as they are correct.
389 An ideal implementation would only inspect as many page table
390 entries as aligned virtual pages spanned by the buffer. Both
391 2-byte and 4,096 buffers can span either 1 or 2 pages, so they
392 would require 1 or 2 checks.
394 Student implementations often check page table entries more often
395 than this. That's acceptable, within reason. For example, one
396 common student implementation checks the first byte and last byte
397 of the user memory block to be accessed, plus one byte spaced
398 every 4,096 bytes within the block. That's fine, although it
399 often makes a few more checks than absolutely necessary.
401 Implementations that always inspect every byte read or written are
402 not acceptable. Take off points.
404 Bizarrely, some students believe that checking a page table entry
405 every 4 bytes within a block is correct. It's not: it will fail,
406 for example, in the case of a 2-byte block spanning a pair of
407 virtual pages. If they check each *aligned* 4-byte region, then
408 it's correct, but then they're still doing 1,024 checks per page,
409 which is still unacceptable.
411 An implementation that uses page faults to detect bad user memory
412 accesses requires 0 page table inspections to copy any number of
413 bytes. An answer from this point of view is acceptable too.
415 Some students suggest that further improvements could be made by
416 page-aligning the buffers passed to the kernel. That's a
417 reasonable suggestion.
419 Some very confused students think that each of the bytes in a
420 4,096 user memory region can be on a separate page. Penalize
425 These questions really have to be graded together, because they
426 are so closely related. See B1(iii) above for information on the
427 basic data structures used for "wait".
429 A call to "wait" should not require disabling interrupts or
430 calling thread_block() by hand. Take off points.
432 Global locks, semaphores, etc. should not generally be used for
433 implementing "wait". Their use forces serialization of process
434 exit, and they are unnecessary. They are also, often, an
435 indication of an incorrect implementation.
437 Some students indicate that they actually implement the wrong
438 semantics: If C exits before P, then "wait" by P on C should
439 return the process's exit code, not -1 as these students say.
440 Take off points for this. (But it's strange that they would think
441 so, seeing as the tests for "wait" are pretty thorough.)
443 The "exit" and "wait" system calls should not have to search a
444 global list of all processes, because it is inefficient and, more
445 importantly, unnecessary given that it is so easy to keep a
446 per-thread list of children. Take off points.
448 The procedures for "wait" and "exit" vary broadly based on the
449 data structures used. If shared data separate from struct thread
452 The procedure for "wait" is roughly this:
454 - Find the child in the list of shared data structures.
455 (If none is found, return -1.)
457 - Wait for the child to die, by downing a semaphore in the
460 - Obtain the child's exit code from the shared data.
462 - Destroy the shared data structure and remove it from the
465 - Return the exit code.
467 The procedure for "exit" is then:
469 - Save the exit code in the shared data.
471 - Up the semaphore in the data shared with our parent
472 process (if any). In some kind of race-free way (such
473 as using a lock and a reference count or pair of boolean
474 variables in the shared data area), mark the shared data
475 as unused by us and free it if the parent is also dead.
477 - Iterate the list of children and, as in the previous
478 step, mark them as no longer used by us and free them if
479 the child is also dead.
481 - Terminate the thread.
483 A common variation on the above "wait" procedure is to mark
484 the shared data structure as "used" instead of removing it
485 from the list and destroying it, then on subsequent "wait"s
486 noticing that it is already used, skipping the down of the
487 semaphore, and returning -1.
489 If the shared data is integrated into struct thread:
491 The procedure for "wait" is roughly this:
493 - Find the child in the list of child processes. (If none
494 is found, return -1.)
496 - Wait for the child to die, by downing a semaphore in the
497 child thread struct (this is the "exiting" semaphore in
498 the example in B1(iii)).
500 - Obtain the child's exit code from its thread struct.
502 - Remove the child from the list of children.
504 - Give the child permission to finish exiting, by "up"ing
505 its may_exit semaphore. (After this action, the child
506 thread struct must no longer be accessed, because the
507 child could wake up at any time and exit. In particular
508 this means that the previous two steps *must* come
511 - Return the exit code.
513 The procedure for "exit" is then:
515 - Save the exit code in the exiting process's thread
518 - Up the "exiting" semaphore, to allow a waiting parent to
519 retrieve the exit code.
521 - Iterate the list of children and up each child's
522 may_exit semaphore, to allow them to exit when they're
525 - Down the "may_exit" semaphore, to wait for our parent to
526 retrieve our exit code.
528 - Terminate the thread.
530 A common variation on the above "wait" procedure is to mark a
531 child as "used" instead of allowing it to terminate then on
532 subsequent "wait"s noticing that it is already used, skipping
533 the down of the semaphore, and returning -1. Then the "exit"
534 procedure actually gives each child permission to terminate.
536 The main "trick" in this kind of implementation is that an
537 exiting process must not terminate until its parent process
538 has terminated or retrieved its exit status. (If the exiting
539 process were allowed to terminate immediately, then its thread
540 struct would be freed and the exit status could not be
541 retrieved at all.) If the students do not explain how they do
544 (Students are not required to explain the "exit" procedure as part
545 of B5, but they usually end up doing so.)
547 Some groups come up with extraordinarily complicated, confusing
548 schemes for implementing wait/exit. If you can't understand their
549 description, take off points.
551 You should expect to see an answer to each of the 4 parts in B8.
552 Here's an example answer that corresponds to the sample solution
553 (which uses shared data separate from the thread struct):
555 - If P calls wait(C) before C exits, P will wait for C to die
556 by "down"ing the semaphore in C's wait_status struct, which
557 C will "up" when it dies.
559 - If P calls wait(C) after C exits, P will "down" the
560 semaphore similarly but no waiting will be required.
562 - If P terminates without waiting, before C exits, then
563 C will "up" the semaphore when it terminates, but P will
564 never "down" it, which is fine.
566 - If P terminates without waiting, after C exits, then the
567 same thing happens, except that C "up"s the semaphore before
568 P dies instead of after.
570 Students should also say something about freeing resources and
573 - When either C or P terminates, it decrements their shared
574 wait_status's reference count (under the wait_status lock).
575 When the reference count reaches 0, the wait_status is no
576 longer referenced so it is freed.
578 - There are no special cases.
582 There are three common types of answers:
584 - Pre-checking: System call handlers verify that all the data
585 they need to access can be accessed before allocating any
586 resources. Thus, there's never anything to free on error.
588 (Once project 3 rolls around, this will no longer be
589 possible, so these students haven't done a good job of
592 - Manual freeing: If an error occurs, the system call handler
593 manually frees everything allocated up to that point. Often
594 these answers focus on "abstraction", making the point that
595 it's easier to free resources if the resources are correctly
598 - Resource pools: All the resources allocated by a thread
599 (memory, locks, etc.) is put on a list. If an error occurs,
600 a handler iterates through the list and releases all of the
603 (This is a clever solution, but rare.)
605 Any of these are fine.
609 This depends on the data structures defined in B1(iv) above.
611 The usual solution is to use a semaphore initialized to 0. The
612 process calling "exec" downs it. The new process "up"s it once
613 the load success or failure is known. A shared Boolean variable
614 is used to indicate success or failure.
616 Some students use thread_block() instead of a semaphore. Take off
619 Some students store the success indicator variable in the child
620 process. This requires additional synchronization so that the
621 child process can't terminate before the parent reads the status.
622 Typically, an additional semaphore is used to give the process
623 calling "exec" a chance to read the shared Boolean variable
624 without a race on the new process's termination. This is not a
625 good approach, because it is so easy to do without the additional
626 synchronization by storing the success indicator in the parent's
627 thread struct or in a local variable (e.g. as in the sample
628 solution). Take off points.
632 See "B5 and B8", above.
636 Just make sure of a few things here:
638 - They actually answered "why", not "how" or "what".
640 - Their answers do not contain any factual inaccuracies
641 (e.g. do not claim that Pintos processes inherit file
642 descriptors on exec, which contradicts what the assignment
645 - Their answers seem "reasonable".
649 A typical answer is just "we didn't change anything". Otherwise,
650 see "B9 and B10" above.
652 Take off points if they come up with some bizarre or stupid scheme
653 and don't properly justify it, e.g. one group decided that pid_t
654 for a process should be the sum of its tid_t and its parent's
657 Take off points if an answer assumes that there can be more than
658 one thread per process and doesn't mention that this would be an
659 extension to the Pintos design.
661 Check the submitted code, by hand, for the following:
663 1. That the system call handler, which is probably in
664 userprog/syscall.c in syscall_handler(), is reasonably
665 abstracted. It should not all be in one function as one huge
666 switch statement, for example. The code that accesses user
667 memory should be separated into specialized functions. If either
668 of these is not the case, or if the code is otherwise difficult
669 to read, take off points.
671 2. The "Source Code" section in the Introduction to the Pintos
672 manual says, "Update existing comments as you modify code." Most
673 students ignore this. Check whether the comment on
674 process_wait() in process.c has been updated. At a minimum, the
675 two final sentences ("This function will be implemented in
676 problem 2-2. For now, it does nothing.") should have been
679 3. Find the implementation of the "open" system call. Verify that
680 all resources are released in error cases and that errors are
681 properly handled. One particular problem can be malloc() failure
682 after a file is successfully opened--is the return value checked?
683 If so, is the file closed before returning an error?
685 (Failure to check malloc() is in the OVERALL section of the grade
686 report, under DESIGN.)