Bir Sürecin Yaşamı

Bir Sürecin Yaşamı

Emeği Geçenler : Erdem Artan,
Siteye Eklenme : 18-10-2011
Yayımlandığı Sayı : 34
[eDergi 34. Sayı]
[34. Sayının Çevrimiçi Konuları]
[Tüm Çevrimiçi Konular]

Giriş
Günümüzde bilgisayar sistemleri, evcilleştirilmesi kolay olmayan birer yaratık halini almaya başladılar. Günümüz dünyasında, işletim sistemleri, bir kişinin herşeyi derinlemesine öğrenmesi için çok büyük ve karmaşık bir nitelikte. Sistem programcılığına merak salmış birçok kişi a.out gibi bir dosyayı çalıştırmak üzere ./a.out komutunu verdiğinde, sistemde neler olduğu konusunda genel bir profil çizemez. Bu makale, sistem programcılığı adaylarının olayı kavraması için fikir oluşturmak ve gerekli detayları vermeyi amaçlıyor. Bu makaleden sonra programcı veya programcı adayının daha fazla bilgi için daha detaylı kitaplara başvurması, daha kolay olacaktır.

Bir Sürecin Doğuşu
Bir süreç, bir programın çalıştırılması ile oluşur. Dolayısıyla sürecin oluşmasından şöyle birkaç adım gerisinden başlayalım. Bir program, programcı ona ihtiyaç duyduğu zaman doğar. Dolayısıyla bir süreç yaratabileceğimden ve yol boyunca neler olup bittiğini açıklayabileceğimden, şu anda bir program yazmaya ihtiyacım var. Aşağıdaki örnek kod, makalenin geri kalanında çeşitli kavramları açıklamak için kullanılacak:

#include <stdio.h>
#include <math.h>

int main()
{
       float d;
       d = cos(20);
       printf("%f\n", d);
}

Bu kod, sadece bir sayının kosinüs değerini buluyor ve bulunan değeri yazdırıyor. Bu programı derleyerek, bir sürecin yolculuğunu takip edebileceğimiz, çalıştırılabilir bir dosya elde edebiliriz:

gcc ornek.c -lm

Bu komut bize a.out adında bir dosya çıktısı verecek. Burada -lm parametresi, math kütüphanesine bağlantı sağlamak için kullanıldı. Şimdi oluşturulan bu çalıştırılabilir dosyayı çalıştırırsak, amaçladığımız şekilde bize bir süreç oluşturmalı.

Program ve Kabuk
Kabukta ./a.out komutu verilerek a.out dosyası çalıştırıldığında, kabuk öncelikle fork() sistem çağrısını kullanarak kendi sürecini oluşturur. Bu fork() sistem çağrısı, yeni bir süreç oluşturacaktır. Bu yeni süreç, execv() sistem çağrıları seti üzerinden verilen çalıştırılabilir imajlar ile kendi üstüne yükleyecektir. Kabaca, kabuğun ne yaptığı aşağıdaki listelemeden incelenebilir:

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

int shell_exec (char *command)
{
       pid_t pid;

       pid = fork();
       if (pid == Innocent {
               execlp(command, command, NULL);
       }
       /* ana süreç çalışmaya devam eder */
       return 0;
}

int main (int count, char **command)
{
       if (count < 2)
               printf("Çalıştırmak için komut gerek\n");

       return shell_exec(command[1]);
}

Yukarıdaki kod, kabuğun veri yolu, izinler, iş denetimi ve dahasını içeren yaptığı işlerin büyük ölçüde basitleştirilmiş halidir.

fork() sistem çağrısı
Bildiğimiz gibi sistem çağrıları, bizi, kullanıcı tarafından çekirdek tarafına alır. Daha önce de bahsettiğimiz gibi bu makalede, belli ve ilginç ayrıntılara yer vereceğiz. Bu bölümde işleyeceğimiz şeyler, bir sürecin başlangıcı sırasında çekirdekte meydana gelen değişik şeylerle alakalı. Son kod listelemesinde de görüldüğü gibi, kabuk bir fork() (çatal) yapıyor ve komut imajını yeni oluşturulan alt sürecin adres aralığına yüklemek için sistem çağrılarının exec (çalıştır) ailesini çağırıyor. fork() sistem çağrısı çağrıldığında, çekirdek aşağıdaki durumlar gerçekleşene dek çalışmakta olan sürecin bir kopyasını oluşturur:

  •     fork() yeni bir yığın oluşturur ve açık dosya tanımlayıcıları gibi paylaşımlı kaynakları kopyalar.
  •     çekirdek çağrılan sürecin kaynak sınırlarını kontrol eder. (ulimit)
  •     çalıştırma zamanları gibi süreç istatistiklerini sıfırlar.
  •     sürece yeni bir süreç numarası verilir ve yeni oluşturulan süreç çalıştırılmaya başlanır.

Bu bağlamda bir copy-on-write* prensibi mevcuttur. İdeal olarak, alt süreç ve üst süreç (fork() sistem çağrısını çağıran) farklı veri alanlarına sahip olmalıdırlar. Fakat verimlilik için Linux, alt süreç için yeni bir alan oluşturmaz, fakat süreçlerin biri üzerine yazmaya başlayana dek, üst sürecinkiyle aynı alanı kullanır.

Şekilde de gösterildiği gibi, biri alt sürecin numarası ile üst süreç içinde, diğeri sıfır değeri ile alt süreçte olmak üzere fork() iki kez dönüyor.

Yeni oluşturulan süreç, eşi olmayan bir süreç numarası (PID) ile tanımlanır. Bu süreç üst süreç ile aynı süreç grubuna aittir. Grup numarası kabukta iş kontrolü için kullanılır. Bunların yanında oturum numarası denilen bir numara da mevcuttur. Aynı gruptaki tüm süreçler, süreç setsid() sistem çağrısını çağırmadıkça genelde aynı oturum numarasına sahiptirler. Herhangi bir andaki süreç numarası ve üst sürecinin numarası ps komutu ile öğrenilebilir.

ps -e
$ ps -f
UID        PID  PPID  C STIME TTY          TIME CMD
guDa     15447 17953  0 02:00 pts/0    00:00:00 ps -f
guDa     17953  1344  0 Jul15 pts/0    00:00:00 /bin/bash

Bir kabukta çalıştırılan tüm komutların üst süreçleri, kabuğun kendisidir.

exec() Sistem Çağrısı

Başarılı bir fork() işleminin ardından alt süreç, kullanıcı tarafından girilen komutu, yani bizim verdiğimiz a.out dosyasını çalıştırmaya başlayacaktır. Bu çalıştırma exec() sistem çağrısı ile gerçekleştirilir. Bu sistem çağrısının görevi, sürecin adres aralığını çalıştırılabilir imaj ile doldurmak ve kontrolü ona vermektir. exec() sistem çağrısı şu fonksiyonları gerçekleştirir:

  •     Ana ve çocuk süreç tarafından paylaşılan dosyalar paylaşılmaz, yeni bilinmeyen bir çalıştırılabilir dosya, kabukla dosya paylaşıyor olmamalı.
  •     Çalıştırılabilir dosya açılır ve dosya gerçekten çalıştırılabilir mi ve mevcut kullanıcı için izinleri açık mı diye izinleri kontrol edilir.
  •     Dosyaya aktarılan tüm seçenekler, sınır aşımı kontrolünden geçerler.
  •     Dosya önceki kontrollerin tümünden geçerse, çalıştırılabilir dosyanın ELF üst bilgisi kontrol edilir.
  •     Dosya mevcut sürecin adres aralığında hafızaya alınır ve yeni kod çalıştırılmak üzere sıraya alınır.

Buraya kadar, fork() sistem çağrısıyla oluşturulan yeni sürecin yerini, yeni bir imajın aldığını öğrendik. Bu aynı PID değeri, farklı örneklerde farklı imajların olduğu anlamına geliyor. Bu, kabuk simulatörüne küçük bir ara koyularak kolayca gözetlenebilir. İlk verdiğimiz örneğe ekleyeceğimiz delay() fonksiyonu sayesinde sürecin işini hemen tamamlaması durdurulabilir ve ps komutuyla değişiklikleri izleyebiliriz:

...
 fork();
 sleep(5);
...
$ ./shell-exec ./a.out &
$ ps             # hemen çalıştırma
 PID TTY          TIME CMD
3939 pts/3    00:00:03 bash
24893 pts/3    00:00:00 shell-exec
24896 pts/3    00:00:00 shell-exec
24985 pts/3    00:00:00 ps
$ ps             # 5 saniye sonra
 PID TTY          TIME CMD
3939 pts/3    00:00:03 bash
24896 pts/3    00:00:00 a.out
24998 pts/3    00:00:00 ps

Buraya kadar adres aralığının ne olduğundan bahsetmeden yeni program imajının alt sürecin adres aralığına yerleştiğini öğrendik. Linux'ta tüm süreçler, bir sürecin çalışabileceği sanal bir adres aralığındadırlar. Program ayrıca kolaylaştırma ve güvenlik için çeşitli alanlara bölünebilir.

Program Bölümleri

Tüm programlar birçok bölüme ayrılırlar. Genel olarak bunlar:

  •     Çalıştırılabilir kodu içeren kod segmenti
  •     Yığın segmenti
  •     Şunları oluşturan veri segmenti:
  •     BSS: Başlatılmamış veri
  •     Heap: Çalışma zamanı sırasında ayrılmış hafıza
  •     Data: Başlatılmış veri

Bunlar sadece öntanımlı olanlar, ikili dosyalarda çok daha fazlası olabilir. Örneğin bizim örnek kodumuzun bölümlerini aşağıdaki komut ile görebilirsiniz. Çıktısı uzun olduğu için dergimizde yer vermemeye karar verdik:

$ objdump -h a.out

İkili olarak derlenmiş birçok bölüm ya boştur ya da hata ayıklama bilgisi üretiyordur. Bölümlerden en önemlileri .text, .bss, .data bölümleridir. .text bölümü çalıştırılabilir dosya yönergelerini içermektedir. Bu veri objdump komutu ile görülebilir.

$ objdump -d -j .text a.out

Bir sonraki bölüm olan .bss, başlatılmamış veriyi içerir. C standardı, başlatılmamış genel değişkenlerin sıfır olarak ayarlanması gerektiğini söyler. Dolayısıyla ikili sistemde alanı sıfırlarla boşa harcamak yerine, çekirdek tarafından sağlanan hafıza her zaman sıfır olarak başlatılacağı için sadece .bss bölümünün genişliğini içerir. İkili sistemde hazırlanmayan diğer bölümler de yığın ve alt yığın (stack ve heap) bölümleri gibi çalışma süresince erişilebilir. Çeşitli bölümlerde sunulan semboller nm komutuyla görülebilir.

$ nm a.out

Sembolün türü ikinci sütunda yazılmaktadır. B ya da b türün BSS, d ya da D sembolün .data bölümünde olduğunu gösterir.

Dinamik Bağlayıcı ve Yükleyici

İlk örnekte verdiğimiz, bir sayının kosinüs değerini veren kodu, gcc ile derlemiş ver derlerken -lm seçeneği ile math.h kütüphanesine dinamik bağlayıcı ile bağlamıştık:
gcc sample-source.c -lm

Herhangi bir dosya file komutu ile tanımlanabilir. File komutunu a.out dosyası üzerinde çalıştırırsak, bize çalıştırılabilir dosyanın türü hakkında bazı bilgiler verecektir:

$ file a.out
a.out: ELF 64-bit LSB executable, x86-64, version 1
(SYSV), dynamically linked (uses shared libs), for
GNU/Linux 2.6.31, not stripped

Dosya tanımlaması, derlenen dosyanın 64bit makine için, paylaşımlı kütüphane kullanan ELF (Çalıştırılabilir ve Bağlanabilir Tür) türünde bir çalıştırılabilir dosya olduğunu anlatıyor. ELF çalıştırılabilir dosyasına bağlanan tüm paylaşımlı kütüphaneleri öğrenmek için ldd komutu kullanılabilir:
$ ldd a.out
linux-vdso.so.1 =>  (0x00007fff1d368000)
libm.so.6 => /lib64/libm.so.6 (0x0000003cede00000)
libc.so.6 => /lib64/libc.so.6 (0x0000003cece00000)
/lib/ld-linux-x86-64.so.2 (0x0000003ceca00000)

Çıktı bize, dört farklı paylaşımlı kütüphanenin kullanıldığını gösteriyor. İlk kütüphane, çekirdek içinde bulunan ve dolayısıyla disk üzerinde bulunmayan, tüm süreçlerin adres boşluğunda bulunan sanal ELF kütüphanesidir. C kütüphanesi tarafından kullanılan birtakım fonksiyonları içerir. libm.so dosyası derlerken kullandığımız math kütüphanesini ifade etmektedir. Bir sonraki ise standart C kütüphanesidir. Son kütüphane ise programın çalıştırılmasından itibaren diğer kütüphaneleri kullanmasına yardımcı olan dinamik yükleyicidir.

Bu yazı aslında, program derlemesi ve bağlanması konu başlığıyla devam ediyor. Ancak konu başlığı ile çok yakından ilgisi olmadığından yer vermemeyi uygun görüyorum. İlgilenenler, bu yazının orijinal kaynağı olan, http://fossix.org sitesinde santosh tarafından yazılan http://fossix.org/program-compilation-intro  adresli yazıyı okuyabilirler.

Bu yazı, orijinal yazının lisansı olan Creative Commons BY-SA 2.5 India Lisansı şartları altında kullanılabilir.

* Bu ifadenin tam Türkçe karşılığını bulamadığını düşünüldüğünden olduğu gibi bırakılmıştır. Ayrıntılı bilgi en.wikipedia.org/wiki/Copy-on-write adresinden edinilebilir.



Bu yazının lisansı Pardus-Linux.Org eDergi 34. Sayı'daki lisansı ile aynıdır. Lisanslar hakkında bilgiyi lisanslar sayfamızda bulabilirsiniz.

Etiketler :

Yorumlar