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() เพื่อสร้างโปรเซสลูก

// simpfork.c
#include <stdio.h>
#include <unistd.h>
main()
{
    int i;
    printf("simpfork: pid = %d\n", getpid());
    i = fork();
    printf("Did a fork. It returned %d. getpid = %d. getppid = %d\n",
        i, getpid(), getppid());
}
$ gcc -o simpfork simpfork.c -Wall
$ ./simpfork
simpfork: pid = 914
Did a fork. It returned 915. getpid = 914. getppid = 381
Did a fork. It returned 0. getpid = 915. getppid = 914

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

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

$ ./simpfork
simpfork: pid = 928
Did a fork. It returned 0. getpid = 929. getppid = 928
Did a fork. It returned 929. getpid = 928. getppid = 381 

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

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

// simpfork2.c
#include <stdio.h>
#include <unistd.h>
main()
{
        int i;
        i = fork();
        if (i == 0) {
                printf("Child. getpid() = %d, getppid() = %d\n", getpid(), getppid());
                sleep(5);
                printf("After sleeping. getpid() = %d, getppid() = %d\n",
                        getpid(), getppid());
        } else {
                printf("Parent exiting early...\n");
        }
}
$ gcc -o simpfork2 simpfork2.c -Wall 
$ ./simpfork2
Child. getpid() = 1301, getppid() = 1300
Parent exiting now
After sleeping. getpid() = 1301, getppid() = 1

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

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

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

// simpfork3.c
#include <stdio.h>
#include <unistd.h>

int K;

main()
{
    int i;
    int j;
    j = 200;
    K = 300;
    printf("Before forking: j = %d, K = %d ", j, K);
    i = fork();

    if (i > 0) { /* Delay the parent */
        sleep(1);
        printf("After forking, parent: j = %d, K = %d\n", j, K);
    } else {
    j++;
    K++;
    printf("After forking, child: j = %d, K = %d\n", j, K);
    }
}
$ gcc -o simpfork3 simpfork3.c -Wall
$ ./simpfork3
Before forking: j = 200, K = 300
After forking, child: j = 201, K = 301
After forking, parent: j = 200, K = 300

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

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

$ simpfork3 > output
$ cat output
Before forking: j = 200, K = 300
After forking, child: j = 201, K = 301
Before forking: j = 200, K = 300
After forking, parent: j = 200, K = 300

จากผลลัพธ์จะสังเกตเห็นได้ว่า ภายในไฟล์ 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.c
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <unistd.h>

main() {
	int i;
	int seekp;
	int fd;
	char *s1;
	char s2[1000];

	fd = open("tmpfile", O_WRONLY | O_TRUNC | O_CREAT, 0666);

	s1 = "Before forking\n";
	write(fd, s1, strlen(s1));

	i = fork();

	if (i > 0) {
		sleep(1); /* Delay the parent by one second */
		s1 = "Parent";
	} else {
		s1 = "Child";
	}

	seekp = lseek(fd, 0, SEEK_CUR);
	sprintf(s2, "%s: After forking: Seek pointer = %d\n", s1, seekp);
	write(fd, s2, strlen(s2));
}
$ gcc -o simpfork4 simpfork4.c -Wall 
$ ./simpfork4
$ cat tmpfile
Before forking
Child: After forking: Seek pointer = 15
Parent: After forking: Seek pointer = 55

จากโปรแกรมข้างต้น เมื่อรันโปรแกรม 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()ในการควบคุมการเกิดโปรเซสตามลำดับที่กำหนด

// simpfork5.c
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>

int main() {

	pid_t pid1, pid2, pid3, pid4;
	printf("Parent[A] of all: %d\n", getpid());

	pid1 = fork();

	if (pid1 == 0) {   // Child Process, lets say B.
		printf("Child[B] with id: %d and its Parent id: %d \n", getpid(),
				getppid());
		pid2 = fork();
		if (pid2 == 0) { // Child process, lets say D.
			printf("Child[D] with id: %d and its Parent id: %d \n", getpid(),
					getppid());
		}
	}
	if (pid1 > 0) {
		pid3 = fork();
		if (pid3 == 0) { // Child process, lets say C.
			printf("Child[C] with id: %d and its Parent id: %d \n", getpid(),
					getppid());
			pid4 = fork();
			if (pid4 == 0) { // Child process, lets say E.
				printf("Child[E] with id: %d and its Parent id: %d \n",
						getpid(), getppid());
			}

		}
	}
	for (int i = 0; i < 3; i++)
		wait(NULL);
}

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

$ gcc -o simpfork5 simpfork5.c -Wall 
$ ./simpfork5
Parent[A] of all: 27385
Child[B] with id: 27386 and its Parent id: 27385
Child[C] with id: 27387 and its Parent id: 27385
Child[D] with id: 27388 and its Parent id: 27386
Child[E] with id: 27389 and its Parent id: 27387

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

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

// simpfork6.c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char *argv[]) {
	int pid;
	/* fork another process */
	pid = fork();
	if (pid < 0) { /* error occurred */
		fprintf(stderr, "Fork Failed\n");
		exit(-1);
	} else if (pid == 0) { /* child process */
		execlp("/bin/ls", "ls", NULL);
	} else { /* parent process */
		/* parent will wait for the child to complete */
		wait(NULL);

		printf("Child Complete -- parent now exiting.\n");
		exit(0);
	}
	return 0;
}

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

$ gcc -o simpfork6 simpfork6.c -Wall 
$ ./simpfork6
simpfork6 simpfork6.c
Child Complete -- parent now exiting.

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

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

// simpfork7.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

void main() {
	int n, m, status;

	n = fork();
	if (n == -1)
		exit(1);
	else {
		if (n != 0) /* Parent Process */
		{
			printf("-p--  value of fork %d  PID = %d \n", n, getpid());
			printf("-p--  Parent PID = %d \n", getppid());
			m = wait(&status);
			printf("-p--  first exit status = %d \n", status);
			status = status >> 8;
			printf("-p--  exit status of child process = %d\n", status);
			printf("-p--  child PID that just stop = %d\n", m);
			fflush(stdout);
			m = execl("com", "com", NULL);
			printf(" Return to parent process ** ,m=%d \n", m);
			exit(0);

		} else {
			printf("\n");
			printf("\n");
			printf("-c--  value of fork = %d \n", n);
			printf("-c--  this PID = %d and Parent PID = %d\n "\
, getpid(),
					getppid());
			sleep(4);
			fflush(stdout);
			m = execl("com", "com", NULL);
			exit(2);
		}
	}

}
$ gcc -o simpfork7 simpfork7.c -Wall 
$ ./simpfork7
-p--  value of fork 4463  PID = 4462 
-p--  Parent PID = 2646 
.
.
.
-c--  value of fork = 0 
-c--  this PID = 4463 and Parent PID = 4462
-p--  first exit status = 512 
-p--  exit status of child process = 2
-p--  child PID that just stop = 4463
 Return to parent process ** ,m=-1 

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

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

// simpshell.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

/* a simple define for convenience */
#define TRUE 1

int main() {
	/* C declaration before any function call */
	int pid, i = 0;
	char tab[256], *s;

	/* the main loop of our shell */
	while (TRUE) {
		/* the printf to print strings and numbers */
		printf("prompt %d> ", i);
		/* "flush" the standard output */
		fflush(stdout);

		/* read the command line */
		s = gets(tab);

		if (s == NULL) { /* <Control+D> pressed */
			fprintf(stderr, "Bye bye\n");
			exit(0);
		}

		/* one process running */
		pid = fork();
		/* 2 processes running */
		printf("I'm running\n");

		switch (pid) /* where are we ? */
		{
		case 0: /* in the child! */
			printf("In the child\n");
			/* exec command */
			execlp(tab, tab, NULL);
			/* executed only if the exec command fails */
			perror("exec");
			exit(0);
			break;
		case -1: /* in... the parent: fork has failed no wait!*/
			perror("fork");
			break;
		default: /* in the parent, the fork worked! */
			printf("In the parent.. wait\n");
			wait(0);
			i++; /* for the next command */
		}
	}
	/* end of the loop */

	return 0;
}
$ gcc -o simpshell simpshell.c -Wall 
$ ./simpshell
prompt 0> ls
I'm running
In the parent.. wait
I'm running
In the child
pipe1.c  pipe3.c    programA   signal   simpfork7    simpshell Other_Multi-Socket.zip chat_socket   simpshell.c  simpshell.cc 
5287 done
prompt 1> date
I'm running
In the parent.. wait
I'm running
In the child
Fri Sep 12 07:42:28 ICT 2014
5288 done
prompt 2> pwd
I'm running
In the parent.. wait
I'm running
In the child
/home/wiroon/os
5289 done

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

// simpshell.cc
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <iostream>

using namespace std;

/* a simple define for convenience */
#define TRUE 1

int main() {
	int pid, i = 0;
	char tab[1024];

	/* the main loop of our shell */
	while (TRUE) {
		/* print strings and numbers */
		cout << "prompt " << i << "> ";

		/* read the command line */
		cin >> tab;

		if (tab[0] == 0) { /* <Control+D> pressed */
			cerr << "Bye bye\n";
			exit(0);
		}

		/* one process running */
		pid = fork();
		/* 2 processes running */
		cout << "I'm running\n";

		switch (pid) /* where are we ? */
		{
		case 0: /* in the child! */
			cout << "In the child\n";
			/* exec command */
			execlp(tab, tab, NULL);
			/* executed only if the exec command fails */
			perror("exec");
			exit(0);
			break;
		case -1: /* in... the parent: fork has failed no wait!*/
			perror("fork");
			break;
		default: /* in the parent, the fork worked! */
			cout << "In the parent.. wait\n";
			cout << wait(0) << " done\n";
			i++; /* for the next command */
		}
	}
	/* end of the loop */

	return 0;
}
$ g++ -o simpshell simpshell.cc -Wall 
$ ./simpshell
prompt 0> ls
I'm running
In the parent.. wait
I'm running
In the child
pipe1.c  pipe3.c    programA   signal   simpfork7    simpshell Other_Multi-Socket.zip chat_socket   simpshell.c  simpshell.cc 
5287 done
prompt 1> date
I'm running
In the parent.. wait
I'm running
In the child
Fri Sep 12 07:42:28 ICT 2014
5288 done
prompt 2> pwd
I'm running
In the parent.. wait
I'm running
In the child
/home/wiroon/os
5289 done

Last updated

Assoc. Prof. Wiroon Sriborrirux, Founder of Advance Innovation Center (AIC) and Bangsaen Design House (BDH), Electrical Engineering Department, Faculty of Engineering, Burapha University