หัวข้อ 7 · 15 นาที
fork, exec, wait และ shell
นึกภาพก่อน
มีสองวิธีเปิดร้านสาขาใหม่:
- แบบ Windows: สร้างร้านใหม่จากศูนย์ตามแบบที่ระบุ ต้องบอกรายละเอียดทุกอย่างในคำสั่งเดียว
- แบบ UNIX: ถ่ายสำเนาร้านเดิมทั้งร้าน (
fork) แล้วให้ร้านสำเนา เปลี่ยนไปขายอย่างอื่น (exec)
แบบ UNIX ดูอ้อม แต่ช่วงระหว่าง fork กับ exec ร้านสำเนาปรับแต่งตัวเองได้ก่อนเปลี่ยนกิจการ
(เช่น เปลี่ยนว่า output จะไปที่ไฟล์ใด) โดยไม่ต้องมี parameter เพิ่มใน system call
Shell
Shell คือ job control system: ให้โปรแกรมเมอร์สร้างและจัดการชุดของโปรแกรมเพื่อทำงานบางอย่าง Windows, MacOS และ Linux มี shell ทั้งหมด ตัวอย่างการ compile โปรแกรม C:
cc -c sourcefile1.c
cc -c sourcefile2.c
ln -o program sourcefile1.o sourcefile2.o
แต่ละบรรทัด shell ต้องสร้าง process ใหม่เพื่อรันโปรแกรมนั้น แล้วรอให้เสร็จ
Windows: CreateProcess
system call ที่สร้าง process ใหม่เพื่อรันโปรแกรม มีขั้นตอน:
- สร้างและ initialize PCB ใน kernel
- สร้างและ initialize address space ใหม่
- Load โปรแกรม เข้า address space
- Copy argument เข้า memory ใน address space
- Initialize hardware context ให้เริ่มทำงานที่
start - แจ้ง scheduler ว่า process ใหม่พร้อมทำงาน
API จริง (แบบย่อ) รับ parameter 10 ตัว เช่น command line, การสืบทอด handle, environment, starting directory
UNIX: fork, exec, wait, signal
| system call | ทำอะไร |
|---|---|
fork | สร้าง สำเนา ของ process ปัจจุบัน และเริ่มให้มันทำงาน — ไม่มี argument |
exec | เปลี่ยน โปรแกรม ที่ process ปัจจุบันรันอยู่ |
wait | รอให้ process หนึ่งทำงานจบ |
signal | ส่งการแจ้งเตือนไปยังอีก process |
fork คืนค่าสองครั้ง
เรียก fork ครั้งเดียว แต่มันกลับมาสองที่: ใน parent และใน child ทั้งคู่ทำต่อจากบรรทัดถัดจาก fork ต่างกันที่ค่าที่ได้คืน:
| process | ค่าที่ fork คืน |
|---|---|
| child | 0 |
| parent | PID ของ child |
ตัวอย่างในสไลด์: parent มี PID 100 เรียก fork → เกิด child PID 101 → ใน parent ตัวแปร pid = 101 ส่วนใน child ตัวแปร pid = 0
การ implement fork
- สร้างและ initialize PCB ใน kernel
- สร้าง address space ใหม่
- Initialize address space ด้วย สำเนาของเนื้อหาทั้งหมด ใน address space ของ parent
- สืบทอด execution context ของ parent (เช่น ไฟล์ที่เปิดอยู่)
- แจ้ง scheduler ว่า process ใหม่พร้อมทำงาน
การ implement exec
- Load โปรแกรม เข้า address space ปัจจุบัน
- Copy argument เข้า memory ใน address space
- Initialize hardware context ให้เริ่มทำงานที่
start
เทียบสามอย่าง
| ขั้นตอน | CreateProcess | fork | exec |
|---|---|---|---|
| สร้าง PCB | ✓ | ✓ | – |
| สร้าง address space ใหม่ | ✓ | ✓ | – |
| เนื้อหาใน address space | load โปรแกรมใหม่ | copy จาก parent | load โปรแกรมใหม่ทับของเดิม |
| copy argument | ✓ | – (ไม่มี argument) | ✓ |
ตั้ง hardware context ที่ start | ✓ | – (สืบทอดจาก parent) | ✓ |
| แจ้ง scheduler | ✓ | ✓ | – |
fork + exec รวมกันได้ทุกขั้นของ CreateProcess สังเกตว่า exec ไม่สร้าง process ใหม่ PID เดิม PCB เดิม เปลี่ยนแค่โปรแกรมข้างใน
Shell ทำงานอย่างไร
char *prog, **args;
int child_pid;
// Read and parse the input a line at a time
while (readAndParseCmdLine(&prog, &args)) {
child_pid = fork(); // create a child process
if (child_pid == 0) {
exec(prog, args); // I'm the child process. Run program
// NOT REACHED
} else {
wait(child_pid); // I'm the parent, wait for child
return 0;
}
}
forkแล้วแยกทางด้วยค่าที่คืน: child ได้0จึงเข้าifส่วน parent ได้ PID ของ child จึงเข้าelse- child เรียก
execเพื่อกลายเป็นโปรแกรมที่ผู้ใช้สั่ง // NOT REACHED: ถ้าexecสำเร็จ โค้ดของ shell ใน child ถูกแทนที่ทั้งหมด บรรทัดหลังexecจึงไม่มีอยู่แล้ว- parent
waitจนกว่า child จะจบ แล้วจึงรับคำสั่งถัดไป
คำถามในสไลด์
สไลด์ถามสามข้อโดยไม่ได้ให้คำตอบ คำตอบข้างล่างมาจากตำราหลักและพฤติกรรมจริงของ UNIX
ตัวอย่างไล่ทีละขั้น
โจทย์: โปรแกรมนี้พิมพ์อะไร ถ้า parent มี PID 100 และ child ได้ PID 101
int pid = fork();
if (pid == 0)
printf("child sees %d\n", pid);
else
printf("parent sees %d\n", pid);
printf("done\n");
forkสร้างสำเนา ตอนนี้มีสอง process ทำต่อจากบรรทัดifเหมือนกัน- ใน child:
pid = 0→ พิมพ์child sees 0→ พิมพ์done - ใน parent:
pid = 101→ พิมพ์parent sees 101→ พิมพ์done - รวมมี 4 บรรทัด และ
doneถูกพิมพ์ 2 ครั้ง (ครั้งละ process) - ลำดับระหว่างบรรทัดของ parent กับของ child ไม่แน่นอน ขึ้นกับ scheduler
จุดที่มักพลาด
1. สลับค่าที่ fork คืน
child ได้ 0 parent ได้ PID ของ child ไม่มีใครได้ PID ของ parent จาก fork
2. คิดว่า exec สร้าง process ใหม่
exec แทนที่โปรแกรมใน process ปัจจุบัน process ใหม่มาจาก fork
3. คิดว่าโค้ดหลัง exec ถูกทำเสมอ
ถูกทำเฉพาะเมื่อ exec ล้มเหลว
4. คิดว่า fork รับชื่อโปรแกรม
fork ไม่มี argument มันแค่ copy process ปัจจุบัน
5. คิดว่า parent กับ child ใช้ตัวแปรร่วมกัน
child ได้ สำเนา ของ address space แก้ตัวแปรใน child ไม่กระทบ parent สิ่งที่สืบทอดร่วมคือ execution context เช่น ไฟล์ที่เปิดอยู่
ที่มา: 03-ProcessAndContextSwitch_Part-II_v2.pdf หน้า 24–32, 36