22 Temmuz 2019 Pazartesi

fork() sistem çağrısı

Not : Linux Kernel Development okunması gereken bir kitap.

Giriş
Bir uygulama code segment,data segment,stack segment ve heap segment alanlarından oluşur. fork() sistem çağrısı ile yeni uygulama için code segment hariç diğer tüm segmentlerin kopyası alınır.

Aşağıda bir uygulamanın kopyası alınırken hangi konulara dikkat edilmesi gerektiğine dair notlarım var.

fork kodlaması
fork() sistem çağrısı yapıldıktan sonra iki farklı uygulama aynı kodu çalıştırmaya devam ederler. parent uygulama ve child uygulama'nın nereden devam ettiği aşağıdaki şekilde gösteriliyor. parent uygulama için fork() sistem çağrısı >0 sonucunu alır. Child uygulama içinse 0 sonucunu alır
enter image description here

fork Process ID yerine BaşkaBir Şey Dönseydi Diyenler
Açıklaması şöyle.
Of course, the Right Thing would be to have fork() return a file descriptor, not a process ID.
Soru
Meraklısı şu soruyu çözebilir. Toplamda kaç tane process yaratılır ?
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(){
  int i;
  for (i = 0; i < 4; i++){
    fork();
  }
  return 0;
}
fork ve fail durumu
Açıklaması şöyle.
On success, the PID of the child process is returned in the parent, and 0 is returned in the child. On failure, -1 is returned in the parent, no child process is created, and errno is set appropriately.
fork() nihayetinde bir sistem çağrısı olduğu için başarısız olabilir. Kodlarken bu durumu göz önünde bulundurmak lazım. Örnek'te fork() sistem çağrısının < 0 değeri dönebileceği de göz önüne alınmış. Çağrı başarısız olursa errno hata kodu atanır. Hata koduna bakara sebep anlaşılabilir. Bazı genel sebepler yetersiz bellek olması (ENOMEM), kullanıcının process üst sınıra erişmesi (EAGAIN) gibi şeyler olabilir.

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
  int pid;
  pid=fork();

  if(pid<0)/* why is this here? */
  {
    fprintf(stderr, "Fork failed");
    exit(-1);
  }
  else if (pid == 0)
  {
    printf("Printed from the child process\n");
  }
  else
  {
    printf("Printed from the parent process\n");
    wait(pid);
  }
}
fork ve threadler
fork ve thread yazısına taşıdım.

fork ve heap

fork() çağrısından sonra her uygulama aynı değerlere sahip, farklı sayfalara sahip olurlar.

Not 1: Gerçekte fork() işlemini optimize etmek için bir çok işletim sistemi Copy-on-write mekanizmasını kullanmaktadır. Ancak ben bu yazıda bu mekanizmayı göz ardı ettim.

Not 2 : Copy-on-write mekanizaması sayesinde vfork() ve fork() arasında da belirgin bir fark kalmadı diye anlıyorum. what is the difference between fork() and vfork()? başlıklı yazı da bunu teyit ediyor.

Aşağıdaki şekli buradan aldım. Görüldüğü gibi code segment aynı yere işaret ederken, heap sayfaları farklı yerlere işaret ediyorlar.

fork ve heap,stack ve data segmentteki program değişkenleri
Program içinde kullanılan değişkenler fork() çağrısından sonra farklı sayfaları kullansalar bile halen aynı sanal adresleri kullanmaya devam ediyorlar. Böylece değişkene pointer varsa bile bir problem çıkmıyor. Örneğin aşağıdaki pointer kullanan kod parçası fork() yapıldıktan sonra bile sorunsuz çalışıyor.
int *p = &a;
a = a - 5;
printf("%d", *p)
Bir başka örnek şöyle
int i=0;
pid_t pid;

puts("Hello, World!");
puts("");

pid = fork();

if(pid)

    i=42;

printf("%p\n", &i);
printf("%d\n", i);
puts("");
Çıktıda değişken adresleri aynı iken değerlerinin farklı olduğunu görürüz.
Hello, World!

0x7fffc2490278
42

0x7fffc2490278
0

fork ve session
fork ile yaratılan yeni uygulama, kendini yaratan uygulama ile aynı terminale bağlıdır. Eğer yeni uygulamayı daemon haline getirmek istersek burada yazıldığı gibi  fork() ve setsid() metodlarını çağırmak gerekir.

fork ve orphan (yetim) kalma sonucu session değişmesi
Eğer ana uygulama, fork ile yaratılan yeni uygulamanın bitmesini beklemeden çıkarsa, yeni uygulama oprhan (yetim) kaldığı için "init" uygulamasının çocuğu olur. Yani yeni uygulama setsit() metodunu çağırmasa bile, session bilgisi değişir. Aşağıdaki şekilde "init"'in nasıl herşeyin atası olduğu görülebilir.

Şekilden de görüldüğü gibi init uygulamasının PID'si 1'dir. Buradaki örnekte bir child uygulamanın yetim kalmadan önce ve sonra getpid() metodunu çağırarak farklı PID numaraları aldığı görülebilir.

fork ve PID
Child uygulama, yeni bir PID (Process ID) numarası alır. Process ID değerini almak için getpid(), parent process id değerini almak için de getppid() metodları kullanılabilir.



Distinction between processes and threads in Linux sorusuna verilen şu cevaba dikkat etmek lazım.
What's considered a PID in the POSIX sense of "process", on the other hand, is called a "thread group ID" or "TGID" in the kernel.
Dolayısıyla PID ve TGID aslında aynı şeyler.


fork ve wait
Creating child processes/killing processes in C/UNIX sorusundan aldığım örnekte parent uygulamanın child'ı nasıl beklediğini görmek mümkün.

WIFEXITED
Bu bir macro ve wait(&child) çağrısından sonra, child uygulamanın nasıl çıktığını anlamamıza yarıyor. Eğer child normal çıkmışsa true dönüyor.

WIFSIGNALED
Bu bir macro ve wait(&child) çağrısından sonra, child uygulamanın nasıl çıktığını anlamamıza yarıyor. Eğer child uygulama handle etmediği bir signal yüzünden çıkmışsa true dönüyor.
wait(&child);
if(WIFSIGNALLED(status)){}//exited due to signal not handled

fork ve wait ile beklememe

Burada "double fork" yaparak, ana uygulamanın child uygulamanın sonucunu beklememesi gösterilmiş. Örnek:
Yukarıdaki yetim (orphan) kalma konusunda da açıklandığı gibi, annesi ölen bir uygulama, init'e bağlanır. init ise bir şekilde kendine bağlanan uygulamalar için wait() metodunu çağırıyor.

fork ve waitpid
waitpid yazısına taşıdım.

fork ve file descriptor
fork() çağrısından sonra her uygulama aynı file descriptorlarına sahip olurlar. Aynı dosyaya yazarlarsa birbirlerini etkileyebilirler. Bu konuyu Linux Dosya Sistemi başlıklı yazıdaki "Open File Table" veri yapısını anlatan bölümde görebilirsiniz. Eğer child uygulamanın file descriptorlarını görmesini istemiyorsak How to fork process without inheriting handles? sorusundaki çözüme bakabiliriz.
Burada da benzer bir açıklama var.


fork ve timer'lar
how to see if any timer thread is running or not başlıklı soruda timer'ların fork() çağrısından sonra yeni yaratılan uygulamaya geçmediği yazıyor.

fork ve exec ailesine ait sistem çağrıları
exec ailesine ait sistem çağrıları için exec ailesi başlıklı yazıya bakabilirsiniz.

C++/C ile gelen kolaylıklar

Aşağıda anlatılan her iki yöntemde de çalıştırılmak istenen program yeni bir shell içinde çalıştırılıyor.

1. std::system()
std::system yazısına taşıdım.

2. popen() 
popen metodu yazısına taşıdım.

3. posix_spawn
Bu metodu hiç kullanmadım. Burada bir kullanım örneği var. fork() + exec () çağrısı yapmak yerine tek bir çağrı ile bir uygulamayı başlatmayı mümkün kılıyor.

dup ve dup2
fork() ile sürekli kullanılan dup ve dup2 sistem çağrılarına da kısaca bakmak lazım.

dup2(f1,f2) ile f2 akımı, f1 akımına yönlendirilebiliyor. Daha sonra f1 kapatılıyor. f1'in kapatılması şart değil ancak, sistemde gereksiz yere kaynak ayrılmış olur, dolayısıyla kapatmak daha iyi. Aşağıda C kütüphanesindeki dup2 açıklaması var.

Örnek:

Örneğin error stream'i output stream'e yönlendirmek için aşağıdaki komutu kullanırız.
2>&1
Shell dup veya dup2 çağrılarından birini kullanarak error stream'i output stream'e yönlendirir ve her şey output stream'e yazılır. Output stream ise tmp1 dosyasına yönlendirildiği için tüm çıktı dosyaya yazılmış olur.
 ls existing-file non-existing-file > tmp1  2>&1
dup2'nin sınırı
Buradaki soruda da açıklandığı gibi dup2'ye ikinci parametre olarak verilebilecek fd akımınının bir üst sınırı bulunuyor. Bu üst sınır getrlimit() metodunun döndürdüğü struct rlimit'in rlim_cur alanından alınablir.


getpgrp
getpgrp() işin içine birden çok uygulama girince, bazen gereken metodlardan. Uygulamanın grubunu döndürür.
pid_t getpgid (pid_t pid );
Eğer parametre olarak 0 geçersek, kendi grup numaramızı alırız.
Örnek:


setpgid
setpgid ile uygulamanın grubu atanır.
pid_t setpgid (pid_t pid , pid_t pgrpId )
Aşağıdaki şekilde farklı gruplar görülebilir.

Eğer çağrının ilk parametresini 0 geçersek, kendi grup numaramızı atarız. Kendimize ait yeni bir grup başlatmak istersek, ikinci parametreye yine kendi program id numaramızı geçebiliriz. Örnek:
process->start(getenv("SHELL"), QStringList() << "-i");
setpgid(process->pid(),0);
Controlling terminal'in grubundan farklı bir gruptaki uygulama, terminale okuma/yazma yapmaya kalkarsa SIGTTIN sinyali alır ve suspend edilir.

Çok emin olmamakla beraber controlling terminal ile aynı gruptaki tüm uygulamalar Ctrl+C'ye basılınca SIGINT sinyalini alırlar

4 yorum:

  1. Eline sağlık. Yazının " parent uygulama için fork() sistem çağrısı 0 sonucunu alır. Child uygulama içinse >0 sonucunu alır " bu kısmı ters olmuş sanırım.

    YanıtlaSil
  2. Merhaba. Buna benzer bi ödevim var ilk sorunun kodları burda dahil. diğerlerini bana anlatabilirseniz çok sevinirim .Şimdiden çok teşekkür ediyorum. mustafayildiz.m@gmail.com mail atarsanız iletişime geçeriz

    1. Write a c program that creates a child process. Parent and child processes should print their names (I am child or I am parent) and process id’s respectively to the screen (for child it is 0 initially).

    2. Write a c program that creates a new process. Child should execute another program that is compiled previously (it may be any program that prints anything to the screen, you may write your own “hello world” application for that). Demonstrate your work.

    3. Modify question 2 so that your program may read the child program name from the user.

    4. Modify question 3 so that parent process continuously reads child process names from the user in an infinite loop. It will be your own terminal.

    YanıtlaSil