 All righty, welcome back to Off Rating System. So, midterm review today. Got midterm after reading week. Why is reading week so late? I don't know. So, we're at like two thirds of the way through the course. So, good time to do midterm review, get you started before reading week because everyone's gonna spend all of reading week studying for this course, right? Yeah, thought so. All right, so, rewinding all the way the very beginning of the course, the main theme, there's three major concepts in this course and we haven't even got to the last one yet. So, there's virtualization. We saw lots of cases of virtualization. We essentially virtualized the CPU for implementing threads. We virtualized memory. We thought we virtualized a lot of things. Then the second thing we got into was concurrency which as far as the midterm was concerned, we only got up to threads and creating it. You don't have to know about solving data races or identifying data races or anything like that. Just that you can create threads and threads will essentially share memory within your process, they live within a process. So, first thing we got into was how to actually communicate with the kernel to find what an offering system actually is. The definition of an offering system is a part of what you're actually executing on. Oh, it kind of depends on what your application is as to what is part of the offering. Part of the offering system is the kernel. The kernel running privilege execution. They communicate through the kernel using the system call interface and that system call interface is how you transition between user mode and kernel mode. So, every program must use this interface except for some dodgy conditions where you're playing with virtual memory but we'll assume that everything has to pass through that system call interface. Other things we saw in the beginning lecture is like the file format for Hello World and everything like that. Don't need to know any of those details. What we do need to know is the difference between API, ABI. So, that was to have to do with libraries, API. More high level just describes what something should do, what arguments it takes and what returns. ABI is like the actual low level details where exactly everything is in memory and you can break the ABI without changing the API. And we also saw how to explore system calls, all that fun stuff. And then there's also more of a thing which there's different kernel architectures that shift how much code mode. So, if you have like a monolith kernel, pretty much anything that has anything to do with hardware is running in kernel mode and you don't try and separate it out. Mostly you do this for performance so you're not doing system calls all the time because the system call interface is a lot slower than just calling a function. If you take this to the extreme, there is micro kernels which just have the bare minimum running in kernel mode so they only deal with scheduling. They only deal with virtual memory management and then they only have basic APC or near process communication. So, then we saw libraries. We saw dynamic libraries, how they related to static libraries that was mostly for your own good and just your own general knowledge saw how to manipulate the dynamic loader. Again, mostly for your general knowledge, not something I'm going to be testing you on. The only thing is just showing you examples of breaking the ABI without changing the API and bad things that could happen with that. So, high level, maybe you should know the pros and cons to dynamic and static libraries but you don't need to know their actual details. So, then we got into process creation and that's where your world turned upside down. So, in Unix, you are only allowed to create a new process through cloning it and then all the processes maintain a parent-child relationship. So, the only way to create new processes is through a system called fork. After the fork, both processes are exactly the same. They look exactly the same. The only difference is the return value of the fork. Remember that the return value is zero for the child and in the parent, it's the process ID of the child. Other than that, you don't know. They look exactly the same and then which one of these two will execute next? Also, you don't know because the kernel scheduler will run and pick one of them. You do not have any determinism. It's just gonna pick whatever it wants to pick. So, after we were creating processes, well then we got into the fun terminology. So, your kernel will maintain that strict parent-child relationship between all the processes and we saw that wait system call, everyone's favorite, where you have to acknowledge your child and order for you to clean up all the resources involved with it, including its process ID, which you may have also picked up in Lab 4. So, with this, you should be able to identify and prevent zombie processes. Remember, a zombie process is when your child terminates and you do not acknowledge it. So, it's wasting some resources until you call wait on it and the offering system can't do anything about it. It's just a zombie process until that process that has that zombie child terminates, in which case the process may become a zombie orphan and then they get reparented likely to a knit or if that parent process just dies with children while they're just orphan processes, they get reparented to a knit or a sub-reaper like you did in Lab 2, possibly. Then we saw some basic IPC, some read and write system calls through file descriptors. They could represent a regular file. They could represent the terminal. We also know now they could represent a pipe. They could represent a socket. Doesn't, as far as what you program, you don't need to care. You just read and write bytes from a file descriptor. We also saw signals as another type of IPC, so those are like interrupts, but for your processes. So, the kernel internally has three different types of interrupts depending on the hardware. If your process is generating interrupts, if it does anything like divide by zero or seg fault or anything like that, those are things that your CPU is going to have to handle and then the kernel can go ahead and send, translate that into a signal to send to your process if it so chooses. All right, then we saw a scheduling. We had a bit of a break. We looked at a few different algorithms. So, there's first come first serve, shortest job first, shortest remaining time first, and shortest remaining time first, which was the same thing with preemptions. It optimized lowest waiting time or turnaround time, and then we saw round robin, which optimized fairness and response time. They were fairly boring. There were examples in the lecture and no one seemed to have any questions about that. Expect one of these questions on the midterm for, I guess, easy marks. So, that should be fairly straightforward and we'll go over examples at the end. Then we didn't have examples. We talked about more complex forms of scheduling. So, you could introduce priority, but then we have the problem priority inversion where a low dependency process depends on high dependency process. So, a high priority process, so that low priority process should technically, temporarily be a high priority process to remove that bottleneck. Some processes too in scheduling, while there's different types of processes, they are not all treated the same and you might have different trade offs or desires when you are scheduling one process versus another. So, if you're interacting with it, like it's part of a user interface, you might want it to be really responsive. If it's just some computation like a machine learning test, you might not really care how responsive it is. You just want to complete as quick as possible. We saw what might happen if you add multiple, cores on your CPU and that and how does that complicate scheduling? Well, I could just throw processes on all the CPUs, but then I might not be using my caches effectively, then I can do pure CPU queues and maybe swap processes between the queues if there is some type of work mismatch. We also saw real time scheduling, so that requires guarantees on completion time, requires predictability, requires very simple things. There's hard real time, soft real time, Linux is soft real time because you can't actually make the guarantees because the kernel is just so complex. Then finally, we saw the completely fair scheduler, which is what Linux uses today and that tries to model that ideal fairness where if we have three processes, each of them gets a third of the CPU time. So then we saw page tables, everyone's favorite topic. So that was all page tables did is help you translate a virtual address to a physical address. MMU is the hardware that actually does that translation and we saw different ways to implement this. The simplest was to just have a gigantic page table that just had page table entries and then you just translate the virtual page number to the physical page number in just one step and that's all you do. So that was easy, but our problem with that is it wasted a lot of memory even for 32-bit machines and it got real out of control real quick once we moved to 64-bit machines, even only using 39 bits for a virtual address while that page table was still like a gigabyte. So in order to solve that issue, we went to multi-level page tables to save space and that was the only reason we have multi-level page tables, otherwise they are slower, they are a lot slower. So idea behind multi-level page tables is well, we take advantage that we only deal with pages and we make each level of page table fit exactly on a page. We got some practice with that in lab three and you should hopefully be somewhat familiar with it now. Then finally, while we realize that if we have multi-level page tables, translating addresses is really, really, really, really, really slow. So we use something like the TLB or the translation look-aside buffer to speed up memory access and we saw that example where if we were accessing memory that was all on the same page, it was nice and fast. If we tried to access memory every page, then it was really, really, really slow. So finally, we got into threads. They enabled concurrency. We relayed them to something we already know, processes. They pretty much had a one-to-one relation except that each thread lives within a process and each thread only has its own independent registers and stack, otherwise it shares memory within the process. So each process can have multiple threads, but now we know that when we start a process, it has one thread initially and we usually call it the main thread because that's the thread that starts executing name. So both processes and kernel threads enable some parallelization. Each process, again, can have multiple kernel threads. There's different implementations. So one-to-one means one user thread maps to one kernel thread, which is what pthreads use and that lets you have parallelism. If you have pure user threads, like what you are almost done in Lab4, that's many to one. So you have many user threads that get mapped to one kernel thread and in that case, it's just the main thread. So in that case, the operating system has no idea that your threads exist, so if one user's thread blocks the whole process blocks and no other threads can make progress. So we saw also what happens if we have multiple threads and we also fork, while the operating system has to manage that and what it does is it creates a new process with only a single thread. Even if you have eight active threads, creates a new process with only a single thread, that's a copy of whoever called fork. And we also saw, oh, if you have multiple threads in your process and you get a signal, the kernel goes ahead and just picks a random thread to handle the signal. And then we had synchronization issues. Life got complicated, but thankfully, to the midterm we stopped here. So here's a forking question. So before that, also note, there's example midterms on the website. So EC353, that midterm, pretty much the exact same as this course. Lincoln's on my website, has been there the whole time, so it hasn't really been hiding. So that's the exam I wrote. And then at UCLA, CS111 was also essentially the same course. It was not a first year course. Their course numbering is just atrocious. Don't ask me why they label courses like this. I have no idea. So here's a forking question. So this is everyone's favorite. So the question is, here you have a main function that just forks three times in a row. And then it asks, what's a reasonable set of inputs, assuming that the initial process has a pit of two, are the outputs in a specific order? What do the relationships between the processes look like? So who is the parent of whom? So do we wanna do this question or do we wanna look at midterms or what we wanna do? It's two zero and adventure time. Viewer? All right, let's do this question then. So we have a main variable called first equal to fork. I'll skip some steps for important things. Second equal to fork. Third equal to fork. And then we had a print F, first equal, I'll just skip some steps. Second equal, third equal. So it asks, what's a reasonable set of inputs assuming the initial process has a pit of two? So I will write process two would start executing main. It would only have one thread, so I can just write P two. It would execute fork and then create a new child. So I will write the family tree up here. So I start with process two. If it reaches fork, it creates a new process. What likely will the process ID of the newly created process be three? All right, we got lots of threes everywhere. So likely it create a process called three. So at the time of the fork, in process two, process two would get what return from fork? The parent process. Three, right? So in process two, well that fork returns three because that's the process ID of my child. So I would create the variable first in process two and then assign it equal to three if it executed next. Let's say we, do we have any idea if process two will even finish fork first? No idea. So it could happen that process three returns from fork first. Let's switch over to process three. What would it get as a return value from fork? Zero, so it would get zero from fork and then assign that to its own variable called first because it's created after we return from fork. So at this point, we have two processes that have completed the first line. Process two is here and process three is here. Then which one goes next? No idea. But let's, for argument's sake, say that process two goes first. So process two goes first, it calls this fork. It will create a new process. What ID will it get? Probably four. And then if we created process four, would it have a variable called first already in it? Hopefully. What value would it have? Three. So process four has a variable called first and its value is three because it's a copy of process two that created it. So in process four, so in this one, what would fork return? Zero? Fork would return zero. So it would create a variable called second and then assign that equal to zero. If instead, if we switch back now to process two, what would it see as a return from fork? Four. So it would create a variable called second and it would be equal to four. And then at this point, process two and process four, have both finished the second line. So let's say process three execute. So process three will execute. It reaches that same fork. What's the process ID it's going to make? Probably five. So we can draw that. That will be a parent or a child of process three. So in process three, what will fork return? Fork will return five because it just created process five. So it would create a variable called second and it would assign it equal to five. And then in process five, well, it's a copy of process three, sorry, whenever it called fork. So whenever it called fork, it had the variable first defined and it was equal to zero. So in process five, what would it get returned from fork? Well, zero. So second would be equal to zero. And now all of my processes are on this line. This is fun, right? So do you want me to do the next step or is everyone good with the next step? So each of these processes will create another process. So if we want, we can do it. Do we want to do it? Okay. So they're all about to call the second fork line. So let's say process two goes first. So process two goes first, it would probably create a new process called six. And in process six, well, it's a copy of process two. So it should have the exact same variables defined with the same values. So first will be equal to three. Second will be equal to four. And then the return value of fork will be zero and then it will create a variable called third and assign zero to that. So process six will have third equal to zero. In process two, third would be equal to what? Six. And then both of these processes could make it to the print line and would print well, exactly what variables we have there. So at this point, process two is done. So then let's say process three goes next. So process three goes next, creates process seven. And then in process seven, well, it's going to be a copy of process three. So first is equal to zero. Second is equal to five. And then the only thing that's gonna be different is the return value of fork. So in process seven, it will get a return value of zero and then assign it to third. In process three, it would get returned process ID seven because that's what it just created. And that would be its value of third. So now process three is essentially done. Now let's say process four goes. So process four goes, it would create a new process, likely process eight. And it's a copy, oops, process eight. So first equals three. Second equals zero because it's a copy. And again, remember, we're returning from this fork. So in process four, what's it going to see for the return value of that third fork, eight. So it would get eight as a return and then assign it to third. And then in process eight, it should see zero, right? So third is equal to zero. So now process four is done. Now we just have process five. It would create a new process, likely called process nine. And in process nine, it would be a copy of process five. So first equals zero. Second equals zero. And then again, it's just whatever gets returned from fork. In process five, third will be equal to nine. And then in process nine, third is going to equal zero. And mercifully we are done. So we started with this process and then we created one, two, three, four, five, six, seven new processes. And our process tree looks like that. So process two was the first process. You can see it directly. Process two directly created three children. And then process three created two children. And then process five created one child and process four created one child. So it's kind of a lopsided tree, but it's still a tree. So any questions about that one? So in this case, the number, if it's just like this, it's to the power of number of fork calls minus one. So in this case, I created seven processes. If I just don't constrain it and just every new process forks again and forks again and forks again and forks again. Yeah. So I could say like I just could put while true fork or have a bounded number or something like that. But this is about as far as it would go on an exam because I mean, after a while it just got kind of boring. All right. So we can look at some other exams. Here, let's skip forward to, so this was the winter exam. Last one that is going to be closest to yours because well, its duration is a lot smaller. So same thing as yours. In fact, aside from the course code, your title page will probably look a lot like this. So there was some short answers like what prevents a process from modifying its own page tables. Well, the kernel manages that, that's what prevents it. Otherwise they could read each other's memory and you wouldn't have any security and it would be no good. And yeah, question two is describe undesirable outcome. A process, if a process could modify its own page tables, well, if it could modify its own page tables, it could read any memory it wants on your system. How can a process communicate using file descriptors if they're independent? So I thought they were, you know, each process was supposed to be independent. Well, file descriptors kind of, you have your own unique set of file descriptors but they more or less operate like pointers. So whatever they represent can be the same thing between two different processes. So that's how you can use them to communicate. The file descriptor numbers themselves are independent in a process. So if I close file descriptor three, doesn't mean my child has file descriptor three closed. That's where the independence stops. So can a newly created process communicate with an existing process as not directly related using pipes? Explain why or why not? Well, no, they can't because the only reason we were allowed to communicate using pipes was because whatever we forked, our child got the same copies of all the file descriptors as we had. And then it would use one side, we would use the other side. Otherwise, we can't share file descriptors like that. So if there's an already existing process that's unrelated to you, it already exists. There's no way to share file descriptors. Then, yeah, another one was like, aside from calling exit or exit group, is there another way to terminate the process? That was our fun. You can kill it using a signal, like SIG kill, which was fun. For scheduling processes, your friend says we should switch process after they execute few instructions to get the most possible response time. That's basically mean we context switch really, really, really, really frequently, which you wouldn't want to do because you want context switching to be like 1% at most of your total execution time because it's not helping you actually run any applications. So you'd be spending all your time context switching instead of doing any useful work. Yeah, here's a fun one. So here's another fun one. So you have some code that prints 1024 bytes from a file descriptor. Someone gives you these two solutions. So first solution is, well, I have a for loop that goes 1024 times and does a read system call of a single byte and prints it off. And in the second one, well, I just have a buffer that is 1024 characters and I do a single read system call and then just read all the bytes. So assuming read and print up are always successful, which of these two solutions would you prefer? Two, right? Any reason? Yeah, system calls are slow. So if you want your program to be fast, should reduce the number of system calls you make, doing one is going to be a lot faster than doing 1024. So here's, oh, here's a fun one. Let's see if we can get this without reading solution stuff. So this was based off people trying to debug their code. So here is the code in the child. It opens a file descriptor for something called DevNull. So that file descriptor, if you write bytes or read bytes to it, you get nothing. And if you write bytes to it, you also get nothing. So they opened Dev1, they duped two to it. And then they do a print F that says debugging and then do an exec LP. So you're trying to debug this program. You inserted the print statement at line A there, but you never see debugging. And you know that you verify that like LS happens and you S trace it and exec LP happens and a write system call happens. So why don't I see debugging on my terminal? Yeah, so print F just writes to file the descriptor one because that's standard out. So if I replace that, what duped two does, oops, is replace file descriptor one with this FD which represents DevNull. So I just replaced whatever standard out goes to and it goes into the void now. So even if that line executes, this would still do a write to one file descriptor one, but I don't see it anymore because now I change what file descriptor one points to it now no longer points to my terminal. So I wouldn't see the debugging message. Okay, second question, would line B execute? No, right? Why wouldn't it execute? Yeah, because exec LP, well, if it's successful, it replaces this process and starts running LS. So if you were super, super, super pedantic, you could say that line B might execute if I fail exec LP, in which case it could execute. Yeah, so standard output is just file descriptor one. Yeah, so if you saved what standard output one was before you could, or if you magically knew it represented the terminal, you could open up the terminal one, depends, right? But usually by default, like if we saw like LS of by default file descriptor zero, one and two, if you're using your terminal, I'll point to the exact same thing. It's just like a fake device to represent the terminal. All right, so, oh, another fun process question. Wow, so do we wanna see other questions or do this one? And all these exams also have solutions so you're not missing too much. It's pretty similar to the last one, although this one just has a weight, huh? All right, let's scan them quick and then we'll come back. So these are all related questions. How many processes get created? What's the state they're in, zombie? Oh, yeah, a scheduling question. So use a round robin scheduler with a qualm length of two and a priority queue. So this was a very straightforward question. It's just normal round robin. Then page tables, odd system with the page size of 256 bytes. 16-bit virtual addresses, page table entry size of 16 bytes. So this is a really odd one. And the system supports a 32-bit physical address. So how many page table entries could you fit on a single page? Well, our page size here is two to eight. You can bring a calculator again, but if you know powers of two, you don't really need a calculator. So through 16, well, that's just two to the four. So how many page table entries fit into a single page? Well, it's just the page size, which is this, divided by the page table entry size, which is this. So you get, oops. So if you just divide the two, you get two to the four, or 16. Yeah, how would you get one point for this? So if you wrote the equation and then wrote the numbers wrong. Yeah, it was more or less it was zero or two, and you should have got two. So generally the grading is pretty lenient. So let's see, if we use multi-level page tables, how many levels of page tables do we need? Show your work. So in this case, remember it was the two, two, two, two. So we had the virtual minus offset over index bits. And in this case, the number of bits I had for virtual addresses, well it says here we have 16 bit virtual addresses. So that would be 16, so there. And the number of bits you need for your offset just depends on your page size. It's essentially the exponent here, because while memory is byte addressable, so you need to be able to address every single byte within a page. So if my page size is two to the eight, that means I need eight bits to address every byte within my page, so my offset is eight. And then my index, well that comes from how many page table entries I can fit on a single page. It's two to the four, so I need to be able to address that many entries on a page. So the number of index bits I need is four. And you can see this is a weird system because I made the numbers nice, so eight divided by four equals two. Any questions about that? All right, and this I screwed up on the exam. I thought I was being funny, so I was trying to write a word like high, high, high. And then I didn't, for some reason it didn't dawn on me that H is not a valid hex character. So you could write eight, it's not a valid address because H is not a hex character. And then, yeah. So, oops, but if you assume that I wasn't an idiot and actually wrote F or something instead of trying to be funny, then it's a 24-bit address because each hex character represents four bits. So it's too large for our system, our system only deals with 16-bit numbers, so that's only like up to four hex characters. So then I said, oh, well, assume you have this virtual address and I need to translate it into physical address speed, whatever, assuming you only have valid page table entries, well, how would that work? So if I have two levels of page table entries, that means my root page table is an L1 and then it points to an L0 page table. So, also I made it nice so that this number here, those four bits that represent this hex digit, this is my L1 index, and then this two here is my L0 index. So this means that to translate it, while I would have a root L1 page table, that would be set up and register or something like that, and then to translate this, well, I need to go to that page table, see what page table entry is at index one, and then in that page table entry will be a physical page number, and that physical page will contain the entirety of the L0 page table I need to go to. So in that L0 page table, I look at whatever is at index two because of that virtual address, then I read the page table entry from there, I get the physical page number and that physical page number would be in this case feed, feed zero zero, and then I get the same offset because I don't need to change the offsets because we're using everything aligned to a page. So then I would get the address, something like that. Any questions? So this was a lot harder for them because they did, you might notice that like here, their virtual memory lab was lab five, which is way after this and yours was lab three. So you've already done this, so this should be at least somewhat easier for you guys than they had. So for this system, could I use a smaller page table entry size? If so, what's the smallest power of two size that would be appropriate? So if your system is, it said you had a 32 bit physical address, so your physical page number in order to use all that, well, it would be the entire physical address minus the offset. So in this case, our offset is only eight bytes or eight bits, sorry. So our physical page number must be 24. So at this point, they didn't really know the permissions that well. So they, I said, assume you would at least need a valid bit. So if you just need a valid bit, well, that brings you up to 25 bits and the next highest power of two is 32. So, you know, even if you had a custom bit, a read bit and a write bit, that's still less than 32. So we would still be fine. So we could fit the page table entry in only four bytes. So if for some reason, I let students say, if they, for some reason was like, yeah, we need 10 bits for permissions, then, well, then they would need 34. So the next power of two is 64, which doesn't change anything. That was fine if you wanted to justify it like that. As long as you don't say, I need like a hundred bits for flags or something silly. You say any reasonable number, that is okay. And then I said, oh, for your page table entry size above, assuming that, you know, we were able to shrink it, what would the benefits be? So in this case, if we shrink it, we can have like more page table entries per page size because our index, like we just shrunk it by a factor of one. So we could fit more page table entries in. But if you go through the math again and figure out everything, while you still need two levels of page tables, they're just not all full. So any questions about that one? All right, no one has any questions? This course is, that's blank. Yeah, that was it. So this course is easy. Everyone's gonna get like, everyone's gonna get the department to yell at me again that the grades are too high. All right, anything last minute stuff? Although I guess we also have Friday. All right, your quietness is concerning. All right, well, yeah, like this one. Today is the only day we're doing a review unless people want me to do more later. Yeah, so all these do have solutions too. So you can, of course, if there's any questions, just post it on Discord and I can go over it too. So yeah, if there's an overwhelming response later, but these all have solutions. So, and you can feel free to ask me about them any time I'll be around. All right, cool. Then we're done. So just remember, we're pulling for you. We're on this.