C Fork Kullanımı - Fork ile Yeni İşlem Başlatma (fork, exec, wait, exit, pid_t, getpid)

C dilinde yeni bir işlem yaratmanın en ilkel yöntemi fork sistem çağrısını(syscall) kullanmaktır.

Fork sistem çağrı numarası: 57

unistd.h header dosyası içerisinde tanımlıdır.

Fork ile yaratılan işlemin(process) özellikleri:

  1. Yeni oluşturulan işlemin kendi (PID)process id'si vardır.
  2. Bir işlem(process) içerisinden başka bir işlem(process) başlatmak forking olarak adlandırılır.
  3. Fork işlemi çocuk(child) işlem(process) başlatmaktır.
  4. Parent ve child aynı anda çalışabilirler.
  5. Ancak programımızda parent ve child processin aynı anda çalışmaması belirtebiliriz.
  6. Programımıza child process'in işini bitirmesini beklemesini wait ya da waitpid fonksiyonları ile belirtebiliriz.
  7. wait ve waitpid fonksiyonları programın neden sonlandığına dair limitli bilgi verir.
  8. Bu bilgiler exit status kodlarıdır.
  9. fork sistem çağrısından dönen pid değerine göre parent process'in mi yoksa child process'in mi işlemde olduğunu öğrenebiliriz.
  10. Exec fonksiyonu kullanılarak başka bir process başlatılabilir.
  11. Exec ile başka bir program çalıştırıldığında eski process image'ı yok olur.
  12. Yerine çalıştırılan programın process image'ı gelir.
  13. Eğer program sonlanırsa eski process image'ına dönmez onun yerine program tamamen sonlanır.

Kullanımı:

fork();

ya da

pid_t pid;
pid = fork();

Yukarıdaki tanımda fork ile yeni bir process oluşturulmuştur.

pid_t = process id type

pid_t veri türü içerisinde processlerin pid değerini tutabilen veri türüdür.

Oluşturduğumuz pid değişkenine fork() işleminden dönen değeri atıyoruz böylece program akışında child ve parent processleri ayırt edebiliriz.

Process Kimliğini Belirlemek - Process Identification (getpid, getppid, pid_t)

Fork işlemine geçmeden önce getpid ve getppid fonksiyonlarını öğrenmemiz fork'un nasıl çalıştığını anlamamıza yardımcı olacaktır.

Getpid(): Çalışan programımızın pid değerini döndürür.

Getppid(): Çalışan programımızın üzerindeki parent process'in pid değerini döndürür.

Bu fonskiyonlar unistd.h header dosyası içerisindedir bu yüzden bu fonksiyonları çalıştırmadan önce bu dosya sayfaya include edilmelidir.

Örnek verirsek:

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

int main(void){

	pid_t pid;
 	pid = getpid();

	printf("%s %d\n", "Process ID:", pid);

	return 0;
}

Çıktı:

Process ID: 9500

Programımız çalıştırdık ve PID değeri olarak 9500 numaralı PID değeri ile çalıştığını gördük.


Parent Processin ID'sini Almak - getppid()

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

int main(void){

	pid_t pid, ppid;
 	pid = getpid();
	ppid = getppid();

	printf("%s %d\n", "Process ID:", pid);
	printf("%s %d\n", "Parent PID:", ppid);

	return 0;
}

Çıktı:

Process ID: 9500
Parent PID: 8947

Programımız çalıştığında kendi process id'sini ve parent(ebeveyn) process'in id numarasını ekrana yazdırıyor. Görüldüğü üzere programımızın PID değeri 9500 parent processin ise 8947.


Bulunduğumuz terminalin(shell) PID değerine ps programı ile bakalım.

$ ps
 PID TTY          TIME CMD
8947 pts/0    00:00:00 bash
9636 pts/0    00:00:00 ps

Görüldüğü üzere programımız doğru bir şekilde çalışıyor. ps programının çıktısında bash programının PID değeri 8947 ve bizim programımızda bu bash programı altında çalıştığından programımız parent processin ID'sini aynı verdi.


Aslında bash processide başka bir processin altında çalışan bir başka program. Bash processinin üstünde çalışan programları(parent processes) görmek için pstree programını ya da ps programını kullanabiliriz.

$ pstree -s -p 8947
systemd(1)───xfce4-panel(1247)───xfce4-terminal(8943)───bash(8947)───pstree(10071)

PID değeri 1 olan process systemd bu process system daemon diye adlandırılır. Windowstaki servisler linux ortamında daemon olarak adlandırılır.

Systemd processi sistem açılışında init processi ile başlar. Systemd, sistem processlerini yöneten bir daemondur.

Fork ile Process Oluşturmak

Fork işlemi program içerisinde yeni bir child process oluşturabilmemizi sağlar.

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

int main(void){

	pid_t pid, getid;
	getid = getpid();
	printf("%s %d\n", "Process ID:", getid);

	pid = fork();
	//Eğer fork başarılı olursa program üst satırdan sonra bir kez daha çalışacak
	printf("%s %d\n", "Fork(Child) PID:", pid);

	return 0;
}

Çıktı:

Process ID: 1769
Fork(Child) PID: 1770
Fork(Child) PID: 0

Fork() fonksiyonunun döndürdüğü değerleri incelersek. Önce 1770 ve sonra 0 değerini döndürüyor. İlk değer fork işleminin başarılı olduğunu ve fork işleminin PID değerini gösteriyor. 0 değeri ise programın fork() satırından itibaren ikinci kez çalıştığında bir daha yeni bir fork oluşturmak yerine önceki fork işleminin başarılı olduğunu belirten 0 değerini döndürüyor. Böylece parent processte mi yoksa child processte mi olduğumuzu ayırt edebiliriz.

Bu yeni child processin id'si genellikle bulunulan processin id'sinin bir fazlası olur.

Fork Fonksiyonu Nasıl Çalışır?

pid = fork(); satırında program yeni bir child process oluşturur.

Dönen değerler:

Forktan dönen pid değeri
0  -> Child başarıyla oluşturulmuştur.
-1 -> Fork işlemi başarısız.
Dönen sonuç 0 ve -1 den farklı ise oluşturulan child processin pid değerini döndürür.

İlk örneğimize dönersek programımız 1769 pid değeriyle çalışmaya başlıyor. pid=fork(); satırında yeni bir child process oluşturuyoruz. Bu process pid değeri olarak 1770 değerini alıyor. Evet halen asıl programımız içerisindeyiz ve kodlarımız fork satırından itibaren çalışacaktır.

Asıl programımızdaki kodlar çalıştırıldıktan sonra program fork satırından itibaren tekrar çalıştırılır. Bu sefer fork() satırı 0 değerini döndürür böylece kısır döngüye girip sürekli fork oluşturulmaz. 0 değeri oluşturmaya çalıştığımız fork işleminin başarılı bir şekilde gerçekleştiğini belirtir ve program fork satırından itibaren bir daha çalışır.

Bu noktada child procesin içerisinde olduğumuz için getpid() fonksiyonu bize oluşturduğumuz child processin PID değerini verecektir.

Fork örnek:

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

int main(void){

	pid_t pid, getid;
	getid = getpid();
	printf("(Parent program) PID: %d\n",getid);
	printf("Bu satır parent(asıl) program tarafından oluşturuldu.\n\n");

	pid = fork();
	//Eğer fork başarılı olursa program burdan sonra bir kez daha çalışacak
	getid = getpid();
	printf("Process PID: %d\n", getid);

	return 0;
}

Çıktı:

(Parent program) PID: 2200
Bu satır parent(asıl) program tarafından oluşturuldu.

Process PID: 2200
Process PID: 2201  -> Bu satır child processten

Yukarıdaki örnekte görüldüğü gibi program ilk önce normal bir şekilde çalıştı ve tüm kodları çalıştırdı. Son satır hariç tüm çıktı parent(asıl) programımızdan üretilmiştir. Programdaki tüm kodlar çalıştırıldıktan sonra fork satırından itibaren program tekrar çalıştırılıyor. Getpid fonksiyonu ile aldığımız pid değeri artık child process içerisinde olduğumuzu gösteriyor.

Parent ve Child İşlemlerini Ayırmak

Fork işleminin fork() fonksiyonuyla başladığını belirtmiştik. İlk kez bu satıra geldiğimizde oluşturulan child processin pid değeri döner. Ve program bu fork satırından itibaren tekrar çalıştırmak istediğinde fork fonksiyonu tekrar çalıştırılır. Ancak kısır döngüye girmeyerek yeni child oluşturmaz onun yerine 0 değerini döndürür ve child process içerisinde olduğumuzu belirtir.

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

int main(void){

	pid_t pid, getid;
	getid = getpid();
	printf("(Asıl program) PID: %d\n",getid);
	printf("Bu satır asıl program tarafından oluşturuldu.\n\n");

	pid = fork();
	//Eğer fork başarılı olursa program burdan sonra bir kez daha çalışacak

	if(pid == 0){
	//fork() satırı 0 değeri döndürdüyse child processteyiz
	//ve bu if blogu çalışacak.
		getid = getpid(); //child processin PID dönecek
		printf("Child processteyiz.\n");
		printf("Child process PID: %d\n", getid);
	}
	else if(pid == -1){
		printf("Fork Failed\n");
	}
	else{
		getid = getpid(); //Parent processin PID dönecek
		printf("Parent processteyiz.\n");
		printf("Parent process PID: %d\n", getid);
	}
	printf("Fork PID: %d\n\n",pid);
	return 0;
}

Çıktı:

(Asıl program) PID: 2474
Bu satır asıl program tarafından oluşturuldu.

Parent processteyiz.
Parent process PID: 2474
Fork PID: 2475

Child processteyiz.
Child process PID: 2475
Fork PID: 0

Processlere İş Yaptırmak

Fork ile child process oluşturduk şimdi child processimize iş yaptıralım.

Fork Child Process İşlemleri

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

#define   MAX_COUNT  5

void  ChildProcess(void);                /* child process prototype  */
void  ParentProcess(void);               /* parent process prototype */

void  main(void)
{
     pid_t  pid;

     pid = fork();
     if (pid == 0){
          ChildProcess();
     }
     else{
          ParentProcess();
     }
}

void  ChildProcess(void)
{
     int   i;
     for (i = 1; i <= MAX_COUNT; i++)
          printf("   Bu satır child processten, deger = %d\n", i);
     printf("   *** Child process bitti ***\n");
}

void  ParentProcess(void)
{
     int   i;
     for (i = 1; i <= MAX_COUNT; i++)
          printf("Bu satır parent processten, deger = %d\n", i);
     printf("*** Parent process bitti ***\n");
}

Çıktı:

Bu satır parent processten, deger = 1
Bu satır parent processten, deger = 2
Bu satır parent processten, deger = 3
   Bu satır child processten, deger = 1
Bu satır parent processten, deger = 4
   Bu satır child processten, deger = 2
Bu satır parent processten, deger = 5
   Bu satır child processten, deger = 3
   Bu satır child processten, deger = 4
   Bu satır child processten, deger = 5
*** Parent process bitti ***
   *** Child process bitti ***

Normalde ilk önce parennt process içerisindeyiz ve parent bitmeden child processin devreye girmemesi gerektiğini düşünüyoruz. Ancak child process kendi işlemini bitirmek için parenta kesinti yaşatıp işini yapmaya çalışır. Bu durum parent ve child arasında sürer.

İşlemlerin(Process) Tamamlanması Beklemek (wait)

Parent processin başladığı yere wait(NULL) satırını ekleyerek child processin işlemini bitirmesini ve daha sonra parent processin çalışmasını sağlayabiliriz. Ya da child processin çağrıldığı yere wait(NULL) ekleyerek parent processin işlemini bitirene kadar child processi bekletebiliriz.

       #include <sys/types.h>
       #include <sys/wait.h>

wait fonksiyonu yukarıdaki headerlar içerisindedir.

Aşağıdaki örnekte child process bitene kadar parent processi bekletiyoruz böylece processler birbirlerini kesintiye uğratmazlar.

Örnek wait ve fork kullanımı:

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

#define   MAX_COUNT  5

void  ChildProcess(void);                /* child process prototype  */
void  ParentProcess(void);               /* parent process prototype */

void  main(void)
{
     pid_t  pid;

     pid = fork();
     if (pid == 0){
          ChildProcess();
     }
     else{
          wait(NULL);
          ParentProcess();
     }
}

void  ChildProcess(void)
{
     int   i;
     for (i = 1; i <= MAX_COUNT; i++)
          printf("   Bu satır child processten, deger = %d\n", i);
     printf("   *** Child process bitti ***\n");
}

void  ParentProcess(void)
{
     int   i;
     for (i = 1; i <= MAX_COUNT; i++)
          printf("Bu satır parent processten, deger = %d\n", i);
     printf("*** Parent process bitti ***\n");
}

Çıktı:

  Bu satır child processten, deger = 1
  Bu satır child processten, deger = 2
  Bu satır child processten, deger = 3
  Bu satır child processten, deger = 4
  Bu satır child processten, deger = 5
  *** Child process bitti ***
Bu satır parent processten, deger = 1
Bu satır parent processten, deger = 2
Bu satır parent processten, deger = 3
Bu satır parent processten, deger = 4
Bu satır parent processten, deger = 5
*** Parent process bitti ***

Şimdi parent process, child processin işini bitirmesini bekleyecek ve sonra parent yapılacaktır. Artık programı istediğimiz kadar yeniden başlatalıp processler birbirlerini kesinti uğratmadan sırayla yapılacaktır.

Programdan Çıkış Yapmak - Exit() Fonksiyonu

Exit fonksiyonlarını kullanabilmek için stdlib.h sayfaya inlcude edilmelidir.

Normal Sonlandırma

Exit fonksiyonu ile programın içerisinden processi sonlandırabiliriz.

exit(status kodu);

Yukarıdaki tanımlamada exit fonkskiyonunun nasıl kullanılacağı gösterilmiştir.

Exit Status(Durum) Kodları

Exit fonksiyonu programı sonlandırırken sınırlı miktarda bir bilgiyi üst programa gönderir. Bu bilgiler exit status kodları olarak adlandırılır.

0 - Programın başarılı bir şekilde sonlandığını belirtir.
1 - Programın hata ile sonlandığını belirtir.

Exit status kodları 0 ile 255 arasında istediğimiz bir sayı olabilir. Yukarıdaki tanımlamalar bir standart olmasada çoğu program bu değerleri kullanırlar.

Exit Status Macroları

Exit status kodlarını istediğimiz gibi belirtebiliriz demiştik. Aynı zamanda bu değerleri içlerinde barındıran MACROlar mevcuttur.

EXIT_SUCCESS - 0 döndürür başarılı sonlandırmayı ifade eder.
EXIT_FAILURE - 1 döndürür ve başarısızlığı ifade eder.

Örnekle inceliyecek olursak:

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

int main(void){

	pid_t pid, getid;
	getid = getpid();
	printf("PID: %d\n\n",getid);

	pid = fork();
	if(pid == 0){
		getid = getpid();
		printf("Child process PID: %d\n", getid);
		exit(EXIT_SUCCESS);
		printf("Bu satır yazdırılmıyacak!\n");
	}
	else if(pid == -1){
		printf("Fork Failed\n");
	}
	else{
		wait(NULL);
		getid = getpid();
		printf("Parent process PID: %d\n", getid);

	}

	printf("Fork PID: %d\n",pid);
	exit(EXIT_FAILURE);
	return 0;
}

Çıktı:

PID: 5385

Child process PID: 5386
Parent process PID: 5385
Fork PID: 5386

$ echo $?
1

Child processimizi SUCCESS = 0 başarılı durumla sonlandırırken parent yani asıl programımızı FAILURE = 1 ile sonlandırıyoruz.

echo $? komutu ile son çalıştırılan programın hangi exit status kodu ile sonlandığını öğrenebiliriz. Çıktıda FAILURE kodu olan 1 görünüyor. Program hata ile sonlandığını bildiriyor. Tabi ki bunu biz yaptık programımız gayet başarılı bir şekilde çalıştı.

Child processin son satırındaki printf satırı exit fonksiyonundan sonra geldiği için çalıştırılmadan process sonlandırılıyor.