Process API Programming

ฟังก์ชัน Getpid(), getppid()

ในเชิงการเขียนโปรแกรมสามารถที่จะเรียกดูหมายเลขของโปรเซสได้โดยเรียกใช้ฟังก์ชันของภาษาซี/ซีพลัสพลัส ได้แก่ฟังก์ชัน getpid() สำหรับหมายเลขตัวที่ถูกสร้างและฟังก์ชัน getppid() สำหรับหมายเลขตัวสร้างโปรเซส ดังตัวอย่างข้างล่างนี้

#include <stdio.h>
#include <unistd.h>

int main(void)
{
    printf("My pid = %d. My parent's pid = %d\n", getpid(), getppid());
    return 0;
}

แต่ละครั้งที่รันโปรแกรม showid ก็จะแสดง pid ที่แตกต่างกันไป (เพิ่มขึ้น) แต่ยังคงเป็น parent pid หมายเลขเดิม

$ gcc -o showpid showpid.c -Wall
$ ./showpid
My pid = 854. My parent's pid = 381
$ ./showpid
My pid = 855. My parent's pid = 381
$ ./showpid
My pid = 856. My parent's pid = 381
$ ./showpid
My pid = 857. My parent's pid = 381
$ ps x
...
 381 p2 S 0:01 -sh (csh)
...
$ 

ภายในระบบปฏิบัติการลีนุกซ์ชุดเครื่องมือหลักสำหรับการจัดการโปรเซสดังแสดงในแผนผังข้างล่างนี้โดยจะเรียกใช้ System Call ตัวอย่างเช่น การสร้างโปรเซสจะเรียกใช้ fork() เป็นต้น

แผนผังแสดงชุดฟังก์ชันการจัดการโปรเซส

โดยมีหลักการคือเมื่อโปรเซสใหม่เกิดขึ้น ซึ่งเรียกว่าโปรเซสลูก (child process) จะมีหลายเลขประจำโปรเซสเพิ่มต่อมาจากหมายเลขของโปรเซสที่สร้างหรือเรียกว่าโปรเซสแม่ (parent process) ตัวโปรเซสใหม่จะทำการคัดลอกค่าของหน่วยความจำ (code, globals, heap และ stack) ของโปรเซสแม่ นอกจากนั้นทั้งโปรเซสแม่และโปรเซสลูกจะใช้ทรัพยากรร่วมกัน

กลไกการสร้างโปรเซสลูก

การทำงานของทั้งสองโปรเซสจะทำงานไปพร้อมๆกัน (concurrency) โดยโปรเซสแม่จะรอให้โปรเซสลูกทำงานเสร็จสิ้น จากรูปข้างล่างแสดงการคัดลอกพื้นที่ของตำแหน่งหน่วยความจำเสมือน (virutal address space)

การคัดลอกหน่วยความจำของโปรเซสลูก

ฟังก์ชันและตัวแปรที่เกี่ยวข้อง

  • ไลบารีที่เกี่ยวข้อง unistd.h

  • ฟังก์ชัน pid_t getpid(void);

    • จะส่งค่าหมายเลขโปรเซสของตัวโปรเซสที่เรียกใช้

  • ฟังก์ชัน pid_t getppid(void);

    • จะส่งค่าหมายเลขโปรเซสแม่ของตัวโปรเซสที่เรียกใช้

  • ฟังก์ชัน int fork(void);

    • จะสร้างโปรเซสใหม่ขึ้นมา โดยจะคัดลอกข้อมูลต่างๆของหน่วยความจำของโปรเซสที่เรียกใช้ฟังก์ชันนี้ (โปรเซสแม่) โดยจะส่งค่าหมายเลขโปรเซสใหม่ไปยังโปรเซสแม่ แต่จะส่งค่าศูนย์ไปยังโปรเซสลูกเอง

ตัวอย่างที่ 1

การเรียกใช้งาน fork() เพื่อสร้างโปรเซสลูก

ผลการรันโปรแกรมเมื่อโปรเซสแม่ได้เข้าครอบครอง CPU ผลที่ออกมาจะแสดงหมายเลขโปรเซสที่ถูกสร้างขึ้น ซึ่งในกรณีนี้คือหมายเลข 915 โดยที่โปรเซสแม่ (simpfork) จะมีหมายเลข 914 และ bash shell จะมีหมายเลข 381 ซึ่งเมื่อโปรเซสแม่ออกจากการครอบครอง CPU แล้ว ตัวโปรเซสลูกที่เกิดใหม่ (หมายเลข 915) ก็จะเข้าไปครอบครอง CPU ต่อ จึงทำให้มีการส่งค่ากลับมาจาก fork() เท่ากับศูนย์ (0) โดยที่หมายเลขโปรเซสหลักก็จะเป็นเลขของมันเอง (915) แล้วตามด้วยหมายเลขโปรเซสแม่ ซึ่งในที่นี้ก็คือ โปรแกรม simpfork

แต่อย่างไรก็ตามในบางครั้งเมื่อมีการรันโปรแกรม simpfork แต่ละครั้งการแสดงผลอาจจะไม่เป็นตามลำดับซะทีเดียว กล่าวคือ โปรเซสลูกอาจจะได้เข้าครอบครอง CPU ก่อนโปรเซสแม่ ก็เป็นไปได้ ดังตัวอย่างข้างล่างนี้

ตัวอย่างที่ 2

เพื่อคำสั่งหน่วงเวลาประมาณ 5 วินาที (sleep(5);) ไว้ในตัวโปรเซสลูก ซึ่งจะทำให้โปรเซสแม่ทำงานสิ้นสุดไปก่อน โดยไม่ได้รอให้โปรเซสลูกเสร็จก่อน

เมื่อทำการรันโปรแกรมขึ้นมา โปรเซสลูกก็จะหลับไปชั่วขณะประมาณ 5 วินาที แต่เมื่อตืนขึ้นมาปรากฏว่าตัวโปรเซสแม่ได้ทำงานเสร็จสิ้นไปเรียบร้อยแล้ว จึงทำให้ค่าหมายเลขโปรแกรมเมื่อใช้คำสั่ง getppid() ไม่สามารถส่งค่ากลับของโปรเซสแม่เดิม (simpfork2) ได้ ดังนั้นโปรเซสลูกจึงกำพร้าแม่ทันที และถูกขึ้นไปอยู่ภายใต้การดูแลแทนด้วยโปรเซสตัวแรกของระบบที่มีหมายเลขโปรเซส 1 ซึ่งรู้จักกันในนาม init

ตัวอย่างที่ 3

เมื่อมีการสร้างโปรเซสลูกขึ้นมา พื้นที่ในหน่วยความจำของโปรเซสลูกจะถูกคัดลอกมาจากโปรเซสแม่ หลังจากที่มีการเรียก fork() แล้วทั้งสองโปรเซสก็จะมีพื้นที่หน่วยความจำสำหรับเก็บตัวแปร j และ K แยกกันไป โดยไม่กระทบกัน ดังตัวอย่างโปรแกรมข้างล่างนี้

เมื่อรันโปรแกรม simpfork3 ดังข้างบน จะสังเกตว่าผลลัพธ์ที่เกิดขึ้นคือ ค่าในตัวแปร j และ K ของแต่ละโปรเซสจะเปลี่ยนแปลงแยกออกจากกัน

เพื่อให้เห็นพฤติกรรมในการคัดลอกพื้นที่ในหน่วยความจำจากโปรเซสแม่ได้ชัดเจนขึ้น สามารถทดสอบรันโปรแกรมใหม่อีกครั้งโดยให้แสดงผลลัพธ์ไปเก็บลงในไฟล์แทน ดังตัวอย่างข้างล่างนี้

จากผลลัพธ์จะสังเกตเห็นได้ว่า ภายในไฟล์ output จะเก็บข้อความซ้ำกันเกิดขึ้น เนื่องจากว่าในระหว่างที่มีการเรียกคำสั่ง fork() นั้นข้อความที่จะถูกพิมพ์ออกไปยังไฟล์ (standard output) ด้วยคำสั่ง printf("Before forking: j = %d, K = %d ", j, K); นั้นจะถูกพักเก็บไว้ชั่วคราว (buffer) เอาไว้ก่อน ซึ่งถ้ายังไม่เต็ม buffer (ที่มีขนาดประมาณ 4KB ถึง 8KB) ก็ยังไม่ส่งออกไปยังไฟล์ก่อน แต่ในขณะนั้นเองตัวโปรเซสลูกก็ได้ทำการคัดลอกค่าหน่วยความจำทั้งหมดของโปรเซสแม่ไว้แล้ว ซึ่งก็ติดค่าที่พักเก็บไว้ใน buffer นั้นด้วยเช่นกัน ดังนั้นเมื่อโปรเซสลูกทำงานและใช้คำสั่ง printf("After forking, child: j = %d, K = %d\n", j, K); ข้อมูลเดิมที่ค้างอยู่ใน buffer ที่ถูกคัดลอกมาจึงถูกเขียนลงไฟล์ซ้ำอีกครั้งนั่นเอง

ตัวอย่างที่ 4

แสดงตัวอย่างเมื่อทั้งโปรเซสแม่และโปรเซสลูก ต้องเข้าใช้ทรัพยากรร่วมกัน ตัวอย่างเช่น การเข้าถึงไฟล์ เป็นต้น

จากโปรแกรมข้างต้น เมื่อรันโปรแกรม simpfork4 แล้ว โปรเซสแม่จะเริ่มทำดารสร้างไฟล์ใหม่ชื่อว่า tmpfile หลังจากนั้นก็จะเริ่มสร้างโปรเซสใหม่ (โปรเซสลูก) ด้วยคำสั่ง fork() โดยทั้งสองโปรเซสต่างก็เข้าใช้ไฟล์นี้ในการเขียนร่วมกัน

ฟังก์ชันและตัวแปรที่เกี่ยวข้อง

  • ฟังก์ชัน int execv(char *programName, char *argv[]);

    • จะระบุชื่อโปรแกรม (programName) ที่จะนำเข้ามาบรรจุในพื้นที่ภายในโปรเซสที่เรียกใช้ ในกรณีที่โปรเซสเดิมมีโปรแกรมตัวเก่าอยู่ ก็จะถูกแทยที่ด้วยโปรแกรมตัวใหม่ (programName) ทันที สำหรับตัวพารามิเตอร์ตัวถัดมาคือ argv จะเป็นตัวอะเรย์สำหรับเก็บดัชนีชี้ชุดอะเรย์ของข้อความ โดยฟังก์ชัน exec นี้จะมีด้วยกันถึง 6 แบบ

  • ฟังก์ชัน void exit(int returnCode);

    • เป็นฟังก์ชัน system call ที่จะสั่งให้โปรเซสสิ้นสุดการทำงาน โดยค่า returnCode จะถูกส่งกลับไปยังโปรเซสแม่ ในกรณีที่โปรเซสแม่กำลังคอยให้การทำงานของโปรเซสลูกเสร็จสมบูรณ์

  • ฟังก์ชัน int wait(int *returnCode);

    • เป็นฟังก์ชัน system call ที่จะส่งผลให้โปรเซสที่เรียกใช้ system call ตัวนี้ จะต้องทำการรอจนกระทั่งแต่ละโปรเซสที่ถูกสร้างโดยโปรเซสตัวนี้ทำงานให้เสร็จสิ้นทั้งหมดเสียก่อน โดยค่าที่ system call ตัวนี้จะส่งกลับมาจะเป็นค่าของหมายเลขโปรเซสที่เสร็จสิ้นการทำงาน และรหัสที่ส่งค่ากลับมาจะถูกเก็บอยู่ในตัวแปรพอยท์เตอร์ returnCode

    • ไลบารีที่เกี่ยวข้อง sys/wait.h

การสิ้นสุดของโปรเซสเกิดขึ้นได้หลายกรณี ตัวอย่างเช่น

  • โปรเซสดำเนินการชุดคำสั่งจนถึงบรรทัดสุดท้าย (last statement) ของฟังก์ชัน main() โดยทั่วไปจะเป็นส่งค่ากลับเป็นศูนย์ (exit (0);)

    • มีการสิ้นสุดโปรเซสที่ผิดพลาด (error exit) โดยตั้งใจ ซึ่งจะเป็นการส่งค่ากลับที่ไม่ใช่เลขศูนย์ เช่นใช้คำสั่ง exit (2); หรือ exit (-1); เป็นต้น

    • มีการสิ้นสุดของโปรเซสที่ล้มเหลว (fatal exit) โดยไม่ตั้งใจ เช่นในกรณีการคำนซรทางคณิตศาสตร์ เช่นกรณีการหารด้วยศูนย์ (divided by zero) หรือกรณีเกิดความผิดพลาดในหน่วยใช้งานหน่วยความจำ เป็นต้น

    • มีการสิ้นสุดของโปรเซสในกรณีที่มีโปรเซสอื่นทำการฆ่า (kill) หรือสั่งให้หยุดและสิ้นสุดการทำงานโปรเซส

ตัวอย่างที่ 5

แสดงตัวอย่างโปรแกรมการสร้างโปรเซสลูกตามลำดับที่กำหนดดังรูปข้างล่าง โดยใช้ฟังก์ชัน wait()ในการควบคุมการเกิดโปรเซสตามลำดับที่กำหนด

เมื่อรันโปรแกรมตัวโปรเซสแม่ (A) จะสร้างโปรเซสลูกตามลำดับ B-->D และ C-->E

ตัวอย่างที่ 6

แสดงตัวอย่างโปรแกรมที่มีการเรียกใช้ฟังก์ชัน exec(), wait() และ exit()

เมื่อรันโปรแกรมตัวโปรเซสแม่จะสร้างโปรเซสลูกเพื่อให้ทำการรันคำสั่ง ls จนกว่าจะทำงานเสร็จสิ้น โดยการใช้คำสั่ง wait(NULL); เพื่อรอโปรเซสลูก

ตัวอย่างที่ 7

แสดงตัวอย่างโปรแกรมที่มีการเรียกใช้คำสั่งที่ไม่มีอยู่ในระบบปฏิบัติการ

ตัวอย่างที่ 8

แสดงตัวอย่างการเขียนโปรแกรมในลักษณะ shell อย่างง่ายเพื่อรับคำสั่งที่ป้อนเข้ามา (command line) แล้วทำการสร้างโปรเซสลูกในการดำเนินการคำสั่งนั้น โดยทั้งสองตัวอย่างข้างล่างจะขียนในรูปแบบภาษา C และภาษา C++

ตัวอย่างโปรแกรมในรูปแบบภาษา C++

Last updated

Was this helpful?