-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path412_notes
More file actions
1856 lines (1632 loc) · 61.9 KB
/
Copy path412_notes
File metadata and controls
1856 lines (1632 loc) · 61.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
8/29 (Discussion)
int * const blah --> the pointer cannot be changed
const int *blah --> the pointer is to a const int
consider:
arr = {1,2,3,4,5}
a = arr
c = a
++*a; --> dereferences a and then increments that value
*a++ --> dereferences a and then increments the pointer
*c = (*a)++; --> adds dereferenced c and a, then increments a
For macros, consider this:
#define MULT(a,b) (a * b)
--> This does NOT work with MULT(1+2,3+4) OOP
Try instead:
#define MULT(a,b) ((a) * (b))
gcc -D SAMPLE --> defines the given macro SAMPLE
gcc -E --> will show code after preprocessing
8/30 (Lecture)
intro stuff
8/31 (Discussion)
vagrant up --> initialize vm
vagrant ssh --> get into vm
cd /vagrant is a shared file (really wherever you run VM from becomes the
vagrant directory)
you should run vagrant ssh from the project folder
vagrant destroy (you might lose local changes WITHIN the vm not in shared dir)
vagrant halt --> will shutdown VM instance but not remove from disk
Both use "vagrant up" to start back up
You can run GeekOS from both local machine and VM
kassert can be helpful for finding errors
gdb: watch variable tracks variable changes
conditional breaks
9/6 (Lecture)
Project 1: Fork and exec
After fork, we have 2 user processes (user address space)
Each has a unique Kernel_Thread (KT)
Each KT has its own Stack -> you must decide what Stack is inherited
Each KT has a unique userContext (UC)
Each has an individual set of file descriptors (FD)
Each FD points into the same File Descriptor Table (FDT)
Global variables start out the same, but are updated separately
EIP is copied into new process
Spawn (maybe in user.c or userseg.c) has most of what we need
In GEEKOS, no stdin, stdout, stderr
User Process vs. Kernel
Process believes it has its own CPU
Process essentially believes there is infinite memory
Process believes there are "files" and "directories"
Process believes there is "Wall" between it and other processes
IPC - Inter-Process-Communication
pipe, signal, shared memory, message passing, network sockets
Virtual Memory
Any time the address in a register is different from the physical address
Can be used to provide illusion of more space
0xfffffff
________
stack (grows down)
________
heap (grows up)
________
data
________
txt
________
________
0x00000000
segmentation allows >1 instances of the same program
nm (executable) -> shows addresses of all symbols in executable
first usable page starts at 0x1000 because we want 0 to be reserved
for Null
Pointer Alignment
(int *) -> should be 4-byte aligned
should be a multiple of 4
(char *) -> should be 1-byte aligned
should be a multiple of 1
consider:
struct foo {int a; char b; int c;};
sizeof(foo) = 12 bytes (not 9 due to int 4-byte alignment)
9/7 (Discussion)
Project 0:
Ensure that everything is appropriately freed in Close
Try a test that repeatedly calls Pipe and Close
Project 1:
Mutex will probably be the way to go
Take a look at Spawn (in user.c)
Spawn unsurprisingly Spawns a user process
Each process has a kernel_thread associated with it
Each of these has a user_context
Jump around k_thread struct and User_Context struct (user.h)
Also Sys_Spawn -> look here also
Mutex in sync.c (I think)
gdb notes:
run "make dbg run;make dbg"
b "Sys_Spawn" --> breakpoing at Sys_Spawn function
s -> step into
n -> step over
c -> keep going till next breakpoint
printx --> print variable in hex
struct notation from c still works for print
9/8 (Lecture)
Project 0:
Too much output being printed by students
try :
fn() {
static int ive_complain;
if (!ive_complained) {
Print("waaa!");
ive_complained = 1;
}
fixed size buffer:
allocate little buffers in a list
allocate a single buffer and shift/advance (I shift)
advance would probably be better, but I should check changes
for shift I should use memmove, not memcpy
Process States:
Running:
to get to Waiting:
waiting for input(device)? child process to terminate? mutex?
to get to Runnable:
dispatcher (I think) can force an active process into the run queue
Runnable:
to get to Running:
piece of software (dispatcher) pops process off of run queue
Waiting (Blocked):
to get to Runnable:
when the thing we were waiting for happens
(unblocked/awakened from the wait queue)
for all process in Runnable (on run queue):
how do we decide which to run?
how do we manage the Run Queue?
- priorities on processes
* most complex last
* how long it's been waiting
- longer waits first, avoid starvation
* how much longer it will run
- longer run last, allow short jobs responsive
- can we do this? (external spec?, predicter?)
* how much resources it will use
- can be useful in event of conflict on resources
- possible justification to suspend process
* solving priority inversion
- priority inversion
* parent important, calls wait on child
* child not important (doesn't fit above criteria)
* high priority waiting on low priority
- possibly temporarily bump up a low priority process
* Avoiding starvation is particularly important
* Allowing short commands to be responsive is also important
Scheduler (three levels):
long-term scheduling:
deciding which process to run (or really start)
- associated with "batch"
* old-school approach when punch cards were a thing
* finish one process, move onto the next
* as opposed to time-sharing
- we start a job whenever we want (conventional for us)
medium-term scheduling
decide which processes to suspend
- frequently not done
short-term scheduling
decide which processes to give to the CPU next
a couple of milliseconds
our job is to separate jobs between I/O bound and CPU-bound
I/0 Bound:
block frequently
don't use much CPU
should be scheduled quickly
ensures the CPU-bound jobs don't have to wait very long
go to the "front" of the run queue
CPU-Bound
get pre-empted
make transistion from Running to Runnable
predictably use a lot of CPU
dont use other resources (significantly)
less likely to be interactive
they can stand to wait
go to the "end" of the run queue
scheduling in Geekos:
one queue, there are priorities but they are all the same
How to create a new process?
usulally via fork
except to start "init"
first user process
attaches login prompts to the console
starts up daemons (servers)
reaps all orphan zombies
zombie is a process that is dead (no exit status)
orphan has no parent
in GEEKOS, there is no "init", kernel starts shell
this is b/c Geekos sucks
fork():
return values:
0 - in the child
child_pid - in the parent
<0 - on error
prints before a fork CAN be printed by both child and parent
if buffer hasn't been flushed yet
buffer maintained in user space
How do processes die?
Exit()
return from main is an example of this
unhandled exception (i.e. SIGSEGV --> segfault, SIGPIPE)
side note: SIGSEGV and SOME other signals can be handled
signal from another process
i.e. SIGKILL from shell
side note:
hierarchy of signals to use to kill a process:
kill -INT <proc> (same as ^C)
kill -HUP <proc> (tells process to try to end appropriately)
kill -TERM <proc> ("-TERM" is optional)
kill -9 <proc> (unhandleable DIE DIE DIE)
9/12 (Discussion)
debugging:
make "dbgrun" in one window
make "dbg" in another
repeats last command on "enter"
"b file.c:LINE_NUM"
"info threads"
"thread apply 1 bt" (bt = backtrace)
"thread apply all bt" (bt = backtrace)
break on 90th iteration of loop?
"b syscall.c:914 if count > 90"
can't print MACROS in gdb :(
you can create a variable to reference the MACRO if you want
"print *struct_ptr" gives all things in struct
9/13 (Lecture)
the static keyword (in C)
on a local variable: lives beyond the function's execution
on a function defined in a header (.h) file:
- avoids confusing the linker with multiple definitions in
different .o files
- putting a static prototype in a header is essentially useless
- this idea assumes the body of the function in in the header
- Enable_Interrupts and Disable_Interrupts
one a function defined in a C (.c) file:
- avoids polluting the name space of functions (e.g. "index")
- asserts that a function cannot be called outside the file
(except via a function pointer)
- We can change a static keyword in GEEKOS, but be careful
* Don't copy and paste the code though, that's gross
Project 1:
What should be copied into the child process?
two pieces of user context should change:
- set of pointers that define the memory space (base ptr, etc)
* look at userseg.c and maybe spawn.c?
* might be called "LDT"
- file descriptors should stay the same but ref count in each
file should change
- stack ptr IN user context (Neil doesn't think it's used)
* Maybe at the beginning, but we prob shouldn't worry
kthread needs a new stack, and then esp somewhere in that page
When a process exists, when should we free the memory?
- we should keep the exit status
- it's up to us when it should actually get freed
Inter-process communication (IPC):
examples of IPC in use b/n processes on the same machine:
- popen (child process) to get input
- getting graphics to the window server
- syslog (centralized logging service)
* syslog process has total control over the logs
- process pool
* coordination via shared memory
- database
- signals sent during shutdown or power failure
Message passing vs. pipe:
message passing: pipe:
message (one block) byte-stream (|read| != |write|)
per-message destination (usually) connected (just one other end)
associated with microkernels not
supposed to be very fast not?
Why IPC and Processes instead of threads?
- consider the examples of IPC list
- isolation
* if one dies the rest are fine (one can die alone)
* if a thread crashes it will kill everyone
- reuse
* the pager "less," other combinations from the shell
- different kernel-level "aspect"
* vary permisions/user
* different priorities (unsure of whether this is in threads)
* different set of file descriptors
- threads don't get their own file descriptors
- an idle process can be paged out (rarely used)
- migrate to multiple machines easily
IPC (the ugly way):
f = fopen("a_file.txt", "w");
fwrite(.....,f);
fclose(f);
------------------------------------------------------------
while ((f=fopen("a_file.txt", "r"))==NULL) {
sleep(1);
}
fread(...);
- essentially how compiler communicates with linker
Shared memory approach:
- not done in GEEKOS
- imagine two processes with independent virtual address spaces
* imagine they have unique stacks in physical memory
* they both have a shared code segment in physical memory
- they could have a shared memory region
* not necessarily at the same point in their individual
virtual address space
* both point to the same point in physical memory
- very fast, no communication through kernel necessary
- the shared code-segment is different
* mapping the same read-only page does not amount to
"shared-memory" IPC
- if you fork, the top of the stack points to the same point
until one is attempted to be written to, at which point the
one written two will be allocated a new read-allowed page
- interface for this:
* shmget - may create
* shmctl - sets permissions
* shmat - "open"
* shmdt - "close"
- essentially we want a pipe like scheme for the shared memory
- how do we coordinate this shared memory?
* partition
- using software, decide which process should write which
part of the shared memory (set flags for read/write)
* sequence number
- define each message as having #, length, and body
- let's say 1 is writing, 2 is reading
- 2 keeps track of the last # read
- assuming they are sequential (or ordered in some way)
2 can appropriately know which to read
- 2 can have an int that allows 1 to know when the writer
can delete already read messages
* semaphore, mutex, condition variable, etc.
9/14 (Discussion)
Project 1: Fork
LDT (Local Descriptor Table):
This should be handled by create_context
I don't think we need to worry about anything more than copying it over
start_user_process vs. creating your own?
probably use built-in over user-created, but it's really up to you
user_context -> memory
allocated by built-in functions I believe
you can copy this for forking, probably not for exec-ing though
state pointer will point to beginning of "interrupt state" struct always
esp not as reliable
poke around in kthread.c
create_thread and whatnot
copying the kernel stack?
maybe disable interrupts? not sure about that
"copy" interrupt state
copy what you KNOW will still be there from the kernel stack
for exec, changing eip might be advised
kernel thread also has a reference count
initial reference count is 2 after fork (parent AND child)
be careful for non-waiting parents
i.e. children waiting count will be 1 if no waiting parent
git notes:
you should commit frequently, push less frequently
git status
git log
git branch "test_name"
git checkout "test_name"
git checkout "commit number"
git reset --hard "commit number" HEAD~n
completely deletes "n" most recent commits
git revert Head~n
revert previous "n" commits but keeps history
9/15 (lecture)
Fork is not recursive
- fork provides an interface to user code, but the child process starts as if new
Make_Runnable -> sticks child on run queue
To go to Running, Handle_Interrupt will do this (from Runnable)
Handle_Interrupt always calls GetNextRunnable
At the top of the kernel's (parent) stack:
Interrupt_State (all the registers)
kthreaad->esp should be set to state initially
pretty much only valid for this purpose (doesn't necessarily get updated w/ actual
esp)
The address at the bottom of the stack page
something like (0x00034000)
3 zeros at end, a couple at beginning
therefore, at all times
KASSERT((kthread->esp) & 0xFFFFF000 == kthread->stackPage)
user_context vs. kthread stack?
Sockets:
Unix Sockets
- on the same machine
- allows extra information and trust
* what user owns the other process?
- you can pass an open file through the socket
- on the same machine, this is probably more efficient
IP Sockets
- network communcation
- really can only know the address of the other endpoint
Datagrams
- messages
- packets (UDP: User Datagram Protocol)
- might not be "connected"
* you may specify the destination with every message (sendto())
Bytestreams
- sequence of bytes (TCP)
- connected
- similar to pipe
socket()
- creates a socket
- returns a file descriptor
- tells the kernel the type you want (Unix, bytestream, etc.)
SERVER SIDE:
bind()
- specifies the endpoint associated with a socket
- endpoint will probably be a path for Unix
- " " a TCP Port for IP
- if the caller doesn't specificy the port, one will be allocated for you
listen()
- express a willingness to accept connections
accept()
- takes a file "listening" descriptor
- returns a new file descriptor for an incoming connection
CLIENT SIDE:
connect()
- specifies a remote endpoint for the socket
- bytestream -> connection setup request
- datagram -> set the implicit destination
OTHER SOCKET FUNCTIONS:
read/recv/recvfrom
write/send/sendto
close
shutdown()
- I am done writing
- I am done reading
- I am done with both
Threads:
- shared address space
- own stack
* call pthread_create and what happens?
- malloc a new area beneath the first stack
- no virtual memory trickery
* what happens when a function from a pthread returns?
- return address to some routine that will keep EAX for later pthread_join
- threads are hard to debug because of shared address space
9/19 (Discussion)
Project 1:
you probably don't want to use a global lock for pipes
don't worry about synchronization on other types of files (not pipes)
Don't assume atomicity
9/20 (Lecture)
Project 1:
User process address space
stack at the end of user process address space
command line args must be put on the user stack
two parameters to main:
argc and argv
argv pushed onto user stack frame, followed by argc pushed
after those, return address is pushed onto stack
after that, old ebp is pushed
where do the strings that argv points to go?
doesn't really matter (we have a pointer)
however they are generaly put right above the stack
Register Dump:
What to do with Exception 13?
Machine tried to look at pointer it shouldn't
Which registers matter?
eip:
if in range 0x1000-0x3000 -> likely valid user instruction address
if in range 0x10000-0x40000 -> likely valid kernel instruction address
How to figure out what eip really is?
in make dbg
can run "dis (eip)"
gives which function and what instruction
"sort -n build/geekos/kernel.syms"
gives you the function
Interrupt number 6:
not really an instruction (or not one can be run in this privilege)
in gdb:
x /100 0x000bfcda
command to examine amount of memory regardless of type
thread apply all bt
similar to single threaded "where"
setjmp(buffer)
creates an image of current context, sticks it into buffer
includes register state, eip, etc.
store current context (registers and such) into a buffer
will return differnt value the first time
longjmp(buffer)
jump back to state represented by buffer
restor a stored context (jumping far)
linked-list of runnable threads
in each node is info about thread (start ptr, args, ret) and buffer
we use setjmp to save current state and put it on thread queue
then longjmp to new buffer for new thread
User-Level Threads:
kernel knows nothing about them
advantages:
user has control over scheduling and can avoid bad interactions
very fast creation/destruction of threads
disadvantages:
can't use multiple processors
complexity in avoiding blocking system calls
How many kernel threads should you have?
number of processors?
expected number of blocked threads
if large:
probably better to do something else
don't block
9/21 (Discussion)
Project 1:
run while(Fork()>=0);
if you can return to shell and run stuff after this you're good
kill unwaited-on child
each kthread has a "parent" field?
add a field to kthread? (at bottom)
you want a new ldt for child
Project 2: Signals
9/21 (Personal Notes)
Descriptor gives:
base address
limit address
privilege
type (read,write,etc)
Selector gives:
GDT vs. LDT
Privilege Mode
Index in G/LDT
9/22 (Lecture)
Interrupts:
lowlevel.asm --> Handle_Interrupt
first Save_Registers saves state
check that our pointer is to kernel data
(skip stuff that isn't immediately obvious)
kernel locking stuff (interrupts trylt disabled)
IMPORTANT:
get the address of the C handler function
interrupt table -> holds address of handler function
index in table is interrupt umber
then call handler
Macro Push_Current_Thread_PTR:
add thread to run queue
kthread->esp is reset to a vaild point in the stack
then Get_Next_Runnable is called
kthread->esp is set in lowlevel.asm
it gets used in this code when a process is put onto CPU
You must set esp in process creation so that this can use it
maybe also look at Switch_To_Thread()
when the registers are restored, actual esp moves up the stackpage
kthread->esp does nothing!
while a process is runnable:
kthread->esp has value
while a process is running:
kthread->esp refers to where the registers were stored the last
time it was interrupted
consider not messing with kthread->esp if possible
Project 2: Signals
Signals: IPC's that essentially act as interrupts for user code
array of functions that can be invoked from "outside" a process
return address above the signal handler will be a system call
we're now back in the kernel and can restore user context
we stash the interrupt state in the user stack so that handler
can reference this for system calls
seems like a security vulnerability
for signal table:
default: default behaviour
ignore
function ptr to handler
9/26 (Lecture)
Project 2:
consider using git remote add some_name (url)
within project 1 directory
to push to new repo, git push some_name
trampoline:
Where do you return after signal is dealt with in user code
trampline is the return address in kernel space where things
can be dealt with before return to user code
No signals to non-user processes
Sys_Signal
signal handler
allows user to change signal handlers
Sys_Kill
SIG_DFL, SIG_IGN --> casting 1,2 to (signal_handler)
signal handler does not exist in GEEKOS
kthread struct probably a good idea
Sys_RegDeliver
Store the return for later
Sys_ReturnSignal
NOT EXPECTED TO RETURN
the function does return, but it doesn't return to anything that
expects a return value from THIS system call
Sys_WaitNoPID
Generic version of wait (no pid supplied)
Complete_Handler
might be useful for finishing a handling (clean up)
not already called anywhere in GEEKOS
Check_Pending_Signal
Used by GEEKOS
Checks if there are any signals, doesn't do anything if there is
Setup_Frame
get process ready to run signal handler
remember current state
Sys_Signal will get a user address as a function pointer
Also a signal.c in /libc
Sig_Init
calls RegDeliver when a program starts up
registers a trampoline
Return_Signal
places 0x14 in eax
issues interrupt
number 14 system call gets you to Sys_ReturnSignal
Semaphore review:
semaphore {
counter = Init #;
wait queue = ...
}
p, wait, down
decrement counter, if 0 or negative,
it will put a process to sleep
v, signal, up
increment counter, if 0 or negative, it will wake up
watch synchronization videos (semaphore one)
in linux, spinlock on things within semaphore
9/27 (Lecture)
Scheduler Activations
Idea:
- allow user-lived code to choose which thread to run
- kernel decides when more or fewer (virtual) processors
are available
- "upcalls" (like signalls with parameters)
allow kernel to tell user code about events
user-only: many user threads to one kernel thread
kernel-only: one user thread to each kernel thread
we want: many user threads: some kernel threads (enough to be good)
- some is generally less than many
User-level runnable threads (this queue not visible to kernel)
- kernel will alert user level stuff of availability of processor
- user scheduled code will longjump to its chosen thread
- this idea is different than what we've talked about before
* allows for "better" optimization of scheduling (potentially)
- suspend(state) will push register "state" on the back of the
run queue
Don't call these functions (especially in multi-threading):
- strtok
* mangles its input (if you pass it a string, it will destroy it)
- replaces specific delimeters with "\0"
* remembers where it left off
- gethostbyname
* looks up an array of records associated with a URL
* returns a pointer to a shared buffer
* if it is called again, initial buffer is overwritten
- gethostbyname_r
* the "_r" tends to indicate this is no longer a problem
Race Condition:
- outcome of execution depends on what happens first
example:
volatile int counter = 0; // volatile means always keep in mem
for (i=0;i<1000;i++) {
counter++; (global, in two different threads)
}
- we will not get counter = 2000
- this is NOT an atomic instruction
- without volatile, we'd probably get counter = 1000
* compiler optimization will do ++ locally
Critical Section Problem:
- How do you provide synchronization on a real machine?
- 4 goals
a) Only one in the critical section at a time
b) Progress: a thread outside its CS can't block another that
wants in
c) Bounded waiting: shouldn't wait forever
d) no assumptions about speed
- Critical section refers to section containing a race condition
- really only an issue in multi-processor environment
- solutions:
* Disable interrupts
* have a lock variable (not a great solution)
Enter() {
while (lock != 0);
lock = 1;
}
Exit() {
// unlock
lock = 0;
}
- this is okay but now lock variable needs to be protected (a)
* have threads take turns (consider just 2 processes)
- Thread A
Enter() {
while (turn != "A");
}
Exit() {
turn = "B";
}
- Thread B
Enter() {
while (turn != "B");
}
Exit() {
turn = "A";
}
- doesn't cause the problem with (a), but it does break (b)
* Thread A must allow B to run and vice versa
* Peterson's Solution
- consider 2 processes (0 & 1)
Enter() {
flag[id] = true;
turn = other_id;
while (turn == other_id && flag[other_id]);
}
Exit() {
flag[id] = false
}
- doesn't work for more than 2 threads
- solves a, b, c, and d for those 2 processes
* Bakery Algorithm
- "take a number" scheme
* wait until everyone with a lower number has finished
Enter() {
entering[id] = true;
number[id] = 1 + MAX_OF_OTHER_NUMBERS;
entering[id] = false;
// N is number of threads
for(j=1; j<=N; j++) {
while (entering[j]);
while (number[j] != 0 && number[j] < number[i]);
}
}
Exit() {
number[id] = 0;
}
* works in a scheme with basically no processor support
9/28 (Discussion)
Project 2:
Depending on how you handle children, some of the tests may use spawn
if you defined something in fork, probably do the same in spawn
10/3 (Discussion)
Project 2:
you do need to pop your arguments back off in Complete_Handler
Enabling Interrupts?
probably don't have to worry about it
race condition on signal send?
Project 3:
per-cpu variables
per-cpu variable structure
cpu number, current thread it's running, run queue, GDT
one for each cpu
no paging, so some things might change
10/4 (Lecture)
Project 2: Signals
- There is a sneaky test that involves invoking kill from within a signal handler
* reset flag in setup process, not in completion handler
Hardware lock intstructions: (lock the bus)
1) TSL - test and set lock
- tsl eax, [memory_location]
* puts *memory_location into eax
* puts 1 into memory_location
* done ATOMICALLY
2) xchg - exchange
- same as tsl exceptnew value at memory_location is old value of eax
- addditional feature useful for lock-free data structures, doesn't help locks
spinlock from xchg:
See: lowlevel.asm
lock_function:
mov eax, 1
loop:
xchg eax, [lock_variable]
test eax
jnz loop
ret
unlock_function:
move eax, 0
xchg eax, [lock_variable]
ret
Producer/Consumer Bounded-Buffer problem:
producer creates stuff
- writes it to a buffer
consumer reads from buffer
* could be more than one consumer/producer
producer should write only when there is space in the buffer
- should stop if buffer is full
consumer should read only when there is data available
- should stop if buffer is empty
works well with semaphore and condition variables, not so much with mutexes
Initial condition:
- empty buffer
* consumer is blocked
* empty_buffers = N
* full_buffers = 0
treat buffer as array of "something" (characters, integers, pages, who cares)
semaphores:
full_buffers
empty_buffers
mutex:
m --> guards access to shared state
producer:
i = produce_item()
wait(empty_buffers)
wait(m)
insert(i)
signal(m)
signal(full_buffers)
consumer:
wait(full_buffers)
wait(m)
i = get_buffer()
signal(m)
signal(empty_buffers)
consume(i)
Condition Variables:
- often combined with monitors
* monitors are kind of like automatic mutex acquired on function entry/exit
- cv.wait()
* will ALWAYS wait (unlike a semaphore)
* will "release" the mutex/monitor to wait
* inherit mutex back when awakened
- cv.signal()
* if there's no one waiting, signal does nothing
* awakens at most one waiting thread
- cv.signal_all()
* awakens all (if any) waiting threads
Producer with c.v.:
i = produce_item()
m.wait(mutex)
while(count_full_buffers == N)
cv.wait(not_full_condition, Mutex)
insert(i)
// could add if (count_full_buffers == 1), not necessary
cv.signal(not_empty)
m.signal(mutex)
* mutex = grand mutex
* you shouldn't need while loop, second level of ensuring you should have mutex
Dining Philosophers Problem:
- Three states:
* Talk
* Hungry
* Eat
- Goes through in order, then loops back to top
- we maintain an array of philosophers)
test(philosopher_idx)
if (philosopher_idx -1) is not eating
and (philosopher_idx + 1) is not eating
and (philosopher_idx) is hungry
then
philosopher_idx = eating
cv.signal(philosopher_idx)
pickup(philosopher_idx)
philosopher_idx = hungry
while(philosopher_idx != eating)
test(philosopher_idx)
if(philosopher_idx == hungry)
cv.wait(philosopher_idx)
putdown(philosopher_idx)
philosopher_idx = talk
test(philosopher_idx - 1)
test(philosopher_idx + 1)
* There is no "busy waiting" -> only one while loop that has a wait in it
* anything we're doing in here that looks/changes a shared variable is
ALWAYS guarded by a mutex
Implementation of a Barrier:
- make all N threads wait until all reach the same point
phase_1()
barrier()
phase_2()
Initial Condition:
N: # of threads
S: semaphore associated with being blocked (value 0)
mutex
threads_in_barrier: 0
Barrier()
wait(mutex)
threads_in_barrier += 1
if (threads_in_barrier == N)