İşletim Sistemi 101 – #4 (Process)

Program vs. Process

Görev Yöneticisi’ni açtığımızda ya da “ps”, “top” gibi uygulamaları çalıştırdığımızda karşımıza çıkan liste, bilgisayarımızda o an çalışmakta olan “şeylerin” bir kısmını bize gösterir. Bu “şeylerin” bazılarının isimleri bize tanıdık gelir.

Windows işletim sisteminde çalışan bazı process'ler.
Windows işletim sisteminde çalışan bazı process’ler.

İsimler gayet bilindik. Bunlar, bilgisayarımızdaki programlar. Ne olduğuna dair en ufak fikrimiz olmayan şeyler de görebiliriz bu listede. Onlar da bilgisayarımızdaki programlar. Değil mi? Değil. Herhangi bir program, çalıştırılma süreçlerine girdiyse artık bir “process” olmuştur.

ProgramProcess
Komut setiÇalıştırılan komut seti
Pasif durumdaAktif durumda
Diskten silinene kadar dururKapatılınca ölür
Diskte yer tutarCPU, RAM, disk, I/O aygıtları gibi kaynakları kullanır
Program ve Process karşılaştırması

Giriş yazımda, eski sistemlerin sadece bir program çalıştırabildiğini (job) belirtmiştim. Günümüzdeki sistemler ise Aynı anda birden fazla kullanıcıya hizmet edebilmekte ve bu kullanıcıların işlerini (task) ve hatta iş parçalarını (thread) çalıştırabilmektedir.

Process Nedir?

Yukarıda belirttiğim gibi, bir program çalıştırıldığında “process” olur. Process’ler çalışmaya başladığında beraberinde birçok kavramı da getirir. “text segment”, “data segment”, “heap”, “stack”. Bu kavramlara değineceğiz.

Bir programın çalışabilmesi için, tamamının ya da bir kısmının RAM’e getirilmesi gerekmektedir. RAM’de tutulan process verileri için aşağıdaki görseli inceleyebilirsiniz:

RAM’de process görüntüsü (Operating System Concepts Essentials kitabından alınmıştır.)

Şimdi bu arkadaşları tek tek ele alalım.

Text Segment

Code segment^1 ya da text segment, bir programın çalıştırılabilir kodlarıdır (obje kodları). Aşağıda örneklerde göreceğiniz kodlar, text segment’te değildir! O kodlar, derleyiciye girip derlendikten sonra çalıştırılabilir kodlara dönüştürülür. Bir paket hâline getirilir ve “program”a dönüşür. Programı çalıştırdığınız zaman derlenmiş kodlar RAM’e yüklenir. İşte o kodlar text segment’i oluşturur.

Data Segment

Herhangi bir fonksiyon içinde tanımlanmamış, global olarak her yerden erişilebilen değişkenler data segment‘te^2 yer alır. Aşağıdaki örnek kodda; “data_segment1”, “data_segment2” ve “ben_de_datayim” değişkenleri, data segment’e örnektir. Önemli not: “data_segment1” örneğinde olduğu gibi, herhangi bir ilk değer verilmemiş değişkenlerin “uninitialized data segment“te (diğer adıyla BSS) olduğunu belirten kaynaklar da mevcuttur.

#include <stdio.h>
#include <stdlib.h>

//Global tanımlı değişkenler, data segment'te yer alır:

int data_segment1;
int data_segment2 = 42;
char ben_de_datayim[] = "ali";

int main(void)
{
    //Data segment'te yer alan veriler:
    printf("Data segment üyeleri:\n");
    printf("%d\n", data_segment1);
    printf("%d\n", data_segment2);
    printf("%s\n", ben_de_datayim);
    return 0;
}

Stack

Fonksiyonlar içinde oluşturulan ve belirli bir kapsam içinde kalan, fonksiyonun çalışması bitince ise silinen değişkenlerimiz “stack^3 içerisinde tutulur.

Aşağıdaki örnekte, parametre olarak verilen bir değişkenin karesini hesaplayıp geri döndüren bir kod göreceksiniz. Burada “karesiniAl” fonksiyonu içindeki “kare” değişkeni, o fonksiyon çağırılana kadar bellekte herhangi bir yer işgal etmemektedir. Ne zaman ki “karesiniAl” fonksiyonu çağırılır, o zaman “kare” değişkeni de bellekte kendisine yer aramaya başlar.

Tipik bir FPS oyununda “sağlık” fonksiyonunu düşünelim. Bir mermi isabet ettiğinde, isabet eden yere göre canınız azalır. Bu durumda “vurulma” fonksiyonu içerisindeki değerler, siz vurulana kadar oluşturulmayacak. Ne zaman vurulursanız, vurulduğunuz yere göre, “hasar” değişkeni bir değer alacak. Yeni can değeriniz hesaplanacak, geri döndürülecek ve stack’ten tahsis edilen yer boşaltılacak. Benzer şekilde farklı bir oyunda, takımınızdaki “healer” canınızı yükseltebilir. Sonrasında item’larını ya da skill’lerini geliştirip tekrar geldiğinde, canınızı yükseltme oranı değişkenlik gösterecektir. Bu yeni değişkenler “heal” fonksiyonunda oluşturulabilir “healing” işi bittikten sonra stack’ten silinebilir. Tabii ki bunlar sadece örnek. Kodu yazan kişi, bambaşka bir tasarım yapmış olabilir.

#include <stdio.h>
#include <stdlib.h>

//Global tanımlı değişkenler, data segment'te yer alır:

int data_segment1;
int data_segment2 = 42;
char ben_de_datayim[] = "ali";

int main(void)
{
    //Data segment'te yer alan veriler:
    printf("Data segment üyeleri:\n");
    printf("%d\n", data_segment1);
    printf("%d\n", data_segment2);
    printf("%s\n", ben_de_datayim);

    //karesiniAl fonksiyonu çalıştırıldığı anda, "kare" adında yeni bir değişken ortaya çıkıyor.
    //Bu değişken, karesiniAl fonksiyonunun işi bittikten sonra kaybolacak.
    //Fonksiyonlar içindeki local değişkenler stack'te tutulur:
    //Stack'te yer alan veriler:
    printf("%d sayısının karesi: %d\n", data_segment2, karesiniAl(data_segment2));


    return 0;
}

int karesiniAl (int sayi) {
  int kare = sayi * sayi;
  return kare;
}

Stack Buffer Overflow

Arkadaşımız geldi, hoş geldin diyelim 🙂

Bir process, kendisine ayrılan stack alanına sığmayacak miktarda veriyi stack’e yazmaya çalışırsa, bir noktadan sonra kendisine tahsis edilen bellek alanı dolar ve stack buffer overflow ya da stack overflow dediğimiz olay yaşanır.^4

Örnek:

Aşağıdaki kod, komut satırından alacağı argümanı bir karakter dizisinin içine atmayı deniyor. Fakat oluşturulacak dizinin boyutu önceden belirlenmiş olmasına rağmen gelen parametrenin uzunluğu kontrol edilmiyor. Önce kodu, sonrasında çıktıyı paylaşıyorum:

#include <stdio.h>
#include <string.h>

void stackoverflow (char *metin)
{
   //12 karakter uzunluğunda bir metin oluşturuyoruz:
   char c[12];

   //Fonksiyona parametre olarak verilen veriyi, c içerisine kopyalamayı deniyoruz.
   //Fakat parametrenin uzunluğunu kontrol etmedik. Ya sığmazsa?

   strcpy(c, metin);  //Burada patlayacağız.
}

int main(int argc, char **argv)
{
   //stackoverflow metodunu, komut satırından gelen ilk argümanı göndererek çağırıyoruz.
   stackoverflow(argv[1]);
   return 0;
}
[root@localhost ~]# vi stack.c
[root@localhost ~]# gcc stack.c -o stack
[root@localhost ~]# ./stack ali
[root@localhost ~]# ./stack ABCDEFGHIJKLMNOP
Parçalama arızası

Önemli bir nokta. Sırf bu hatadan dolayı çok ciddi siber saldırılara maruz kalabilirsiniz. Örneğin Internet Download Manager yazılımında yer alan bir açığı incelemek isterseniz: Internet Download Manager 6.37.11.1 – Stack Buffer Overflow (PoC)

Heap

Process’in çalışırken dinamik olarak talep ettiği bellek alanları, heap^5 üzerinde tutulur. Büyük bir array ya da struct tutmak istediğimizde; bunu stack üzerinde oluşturmak yerine heap üzerinden kendimiz tanımlamayı ve işimiz bittiğinde bu alanı boşa çıkarmayı isteyebiliriz. Bu yaklaşım, hem bellek yönetimini daha etkili hâle getirebilir hem de stack overflow ihtimalimizi düşürebilir. Ayrıca, oluşturulacak değişkenleri uzun süre saklamak istiyorsak ve boyutlarının değişkenlik göstereceğini düşünüyorsak yine heap kullanırız.

Fakat ömrü kısa olacak, fonksiyon bitince ilişiğimizin kesileceği, nispeten küçük boyutlu değişkenler için stack tercih ederiz.

Bir Process’in Yaşam Döngüsü

Process’ler başlangıcından bitişine kadar farklı durumlar yaşayabilir. Bu durumların her birine “process state” diyoruz. Bir process’in state’i, işletim sistemi için oldukça önemlidir. State’ler, process’lerin yönetimi için elzem bir bilgidir.

Bazı sistemlerde, tüm process’ler cihaz boot edildiğinde otomatik olarak çalıştırılabilir. Bu tip sistemler; genelde az sayıda ve spesifik işler yapan sistemlerdir. Örnek olarak bir kahve makinesini ya da mikrodalga fırını verebiliriz. Kişisel bilgisayarlarımız, sunucular, oyun konsoları gibi cihazlarda ise durum daha farklıdır. Process’lerin bazıları, sistem boot edildiğinde çalışmaya başlar. Fakat kullanıcının taleplerine göre yeni process’ler oluşturulabilir. Bilgisayarınızı açtıktan sonra bir web sayfasını ziyaret etmek için Firefox yazılımını çalıştırmanız gibi.

Process’ler çalıştıkça, state’leri değişebilir. Bu state’lerin değişimi ve takibi, işletim sisteminin sorumluluğundadır. Aşağıda bu state’leri kısaca açıklamaya çalıştım:

  • New: Process ilk oluşturulduğunda sahip olduğu state’tir. Bazı kaynaklarda “created” olarak da geçer.
    • Bir process’in oluşturulması; verilerin diskten okunması, RAM’e yazılması (text, data, stack, heap atanması), process ID tanımlanması, Process Control Block’ta (PCB) ilgili kayıtların açılması gibi adımları içerir.
  • Ready: Process’in oluşturulma sürecinin tamamlanması, beklediği etkileşimin gerçekleşmesi gibi olaylardan sonra; çalışmaya hazır şekilde beklediği state’tir. Bazı kaynaklar “waiting” ile aynı tutsa da, bazıları waiting state’i ayrı bir başlık olarak inceler.
    • Bir process, çalışmaya devam ederken işletim sistemi tarafından beklemeye alınabilir. Zaten böyle olmasaydı, aynı anda sadece bir process çalışır ve o process kapatılana kadar başka hiçbir iş yapamazdık (Çok çekirdekli, çok CPU’lu sistemleri ayrı tutuyorum.). Dolayısıyla ready state’i, process’in yeni olduğunu garanti etmez. 5 aydır çalışmakta olan bir process’de ready state görmeye devam edebilir.
  • Running: Process, işletim sistemi tarafından CPU çekirdeğine getirilmiştir ve çalışmaktadır. Bir CPU çekirdeğinde yalnızca bir process çalışabilir. Bu çalışma süreci, milisaniye hatta nanosaniye düzeyinde olabilir. Müzik dinlediğiniz process, saniye’nin – örneğin! – 300’de 1’i kadar bir oranda çalışıp sonrasında duruyor, hemen ardından başka bir process çalışıyor, sonra bir başkası, belki yüzlercesi. Sonra yine sizin müzik çalıyor. CPU’ların saniyede milyarlarca işlem yaptığını düşününce, bu değişimleri fark etmememiz gayet normal. Fakat olay apayrı bir boyut değil mi sizce de?
  • Waiting: Process, herhangi bir I/O ya da etkileşim bekliyordur. Örneğin, bir metin editörü açtınız. Öylece bekliyorsunuz. Bu process’in iş yapabilmesi için kullanıcının klavyesinde bir şeylere basması gerekir ve sistemin bizi bekleyecek hâli yok. Bu process’i waiting state’e alır. Siz bir tuşa bastığınızda interrupt request^6 gönderirsiniz. Ya da process’in diskten bir veri okuması gerekiyordur. CPU hızı, HDD’ye göre yüzlerce kat fazladır. Dolayısıyla, process arkada sakin sakin bekletilir. İşi bitince tekrar ready state’e geçirilir. Zamanı gelince de çalıştırılır. Bu durum, bazı kaynaklarda “blocked” state olarak da geçer.
  • Terminated: Process’in çalıştırılması tamamlanmıştır. Bu state’e geçen process’lerle ilgili temizlik süreçleri başlatılır.

Process Ağacı

Process’ler arasında parent – child (ebeveyn – çocuk) ilişkisi vardır. Bir process, diğer process’leri doğurabilir. Örneğin, aşağıdaki örnekte “bash” process’inin “ps” process’ini doğurduğunu görüyoruz. Bash, bu işletim sisteminde kullandığım kabuk yazılımı. Makine boot edildikten sonra kendiliğinden çalışan process’lerden biri. ps ise, sistemde çalışan process’leri görüntülemeye yarayan bir araç. Aslında gayet mantıklı, ps çalıştırmak için bash’i kullandım. ps, bash’in child process’i oldu.

[root@localhost ~]# ps --forest
PID TTY TIME CMD
1240 pts/0 00:00:00 bash
1260 pts/0 00:00:00 \_ ps

Daha detaylı bir çıktıyı inceleyelim (bu çıktıda birçok satırı sildim):

[root@localhost ~]# ps aux --forest
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 2 0.0 0.0 0 0 ? S 23:07 0:00 [kthreadd]
root 4 0.0 0.0 0 0 ? S< 23:07 0:00 \_ [kworker/0:0H]
root 5 0.0 0.0 0 0 ? S 23:07 0:00 \_ [kworker/u2:0]
root 989 0.0 0.4 112924 4320 ? Ss 23:07 0:00 /usr/sbin/sshd -D
root 1236 0.0 0.5 158916 6060 ? Ss 23:10 0:00 \_ sshd: root@pts/0
root 1240 0.0 0.2 115544 2064 pts/0 Ss 23:10 0:00 \_ -bash
root 1261 0.0 0.1 155608 1948 pts/0 R+ 23:16 0:00 \_ ps aux --forest

Process ID’si (PID) 2 olan bir process görüyoruz. Kernel Thread Daemon (khreadd). Sistemde çalışan ilk process’lerden biri. Kendine bağlı bazı “worker“lar açmış. Bu process’ler, yeni açılacak process’leri çalıştırmakla yükümlü olan process’ler. Bir de Secure Shell Daemon (sshd) process’ini görüyoruz. “root” kullanıcısı ile bir SSH oturumu başlatılmış. O SSH oturumunda “bash” process’i çalıştırılmış. Bash’in altında ise “ps” çalıştırılmış.

Kısa bir bilgi: Eğer bir process’i doğuran parent process beklenmedik bir şekilde kapanırsa, child process’lerine kapanma sinyalini gönderemez. Bu durumda child process’ler, öksüz kalır (orphan process)^7. Çalışması tamamlanmış olmasına rağmen parent process’inden gerekli sinyalleri alamayan child process’ler ise, zombiye dönüşür (zombie process)^8. İşletim sistemi, bu durumlarda kalan process’i de yönetmek zorundadır.

Process Control Block (PCB)

Hadi düşünelim. Bir işletim sistemisiniz. Saniyede yüzlerce process çalıştırmak zorundasınız. Bu process’lerle ilgili neler bilmek, işinizi kolaylaştırırdı?

  • Process’lere özel bir isim vermek, process’leri birbirinden ayırma konusunda epey fayda sağlardı. Kimlik numaranız gibi düşünebilirsiniz. Buna Process ID (PID) deriz.
  • Her process’in state’ini tutmalıyız. Kim hazır, kim çalışıyor, kimler bekliyor, kimlerin işi tamamlandı? Bu konuyu da zaten bir önceki bölümde detaylandırdık.
  • Process çalışıyordu. Sonra duraklattık. Şimdi sıra tekrar ilgili process’e geldi. Eee? Baştan mı çalıştıracağız? Kaldığı yerden devam ettirmeliyiz değil mi? “Kaldığı yer”, program counter (PC) ya da instruction pointer (IP) olarak ifade edilir.
  • Process’in bilgileri (mesela kodları) diskten talep edildi. Disk buffer’ına geldi. Oradan RAM’e yazıldı. RAM’den işlemciye getirildi ve işlemcinin önbelleğine (cache memory) yazıldı. Son olarak, işlemci hızına en yakın bellek olan CPU register’ına getirildi. Çalıştırıldı. Derken süre doldu. Sıra tekrar bu process’e geldiğinde hangi verileri kullandığımızı hatırlamazsak, bütün bu süreç başa sarar. Dolayısıyla PCB üzerinde CPU register verilerini de saklamalıyız. Böylelikle bir sonraki turda, bu verileri doğrudan RAM’den ya da cache memory’den getirebiliriz. Disk’in yavaşlığı ile uğraşmak zorunda kalmayız. CPU seçiminde cache bellek önemini fark ediyor muyuz?
  • Peki bu process’in stack, heap gibi sınırları neresiydi? RAM’de hangi aralığı bu process’e ayırmıştık? Ne kadarlık alan ayırmıştık? Bu bilgileri de tutmamız gerekiyor. Şu soruyu sorabilirsiniz, “Process’in verileri zaten RAM’de duruyor. Neden aklımızda tutmak zorunda kalalım ki? Zaten belirli bir yerde duruyor.”. Peki ama o yer neresi? RAM üzerinde hangi adres aralığının hangi process’e ait olduğunu bilmek zorundayız. Ek olarak, işletim sistemleri ihtiyaç hâlinde bazı process’lerin verilerini RAM’den alır ve diske yazar. Yani process bir sonraki turunda kendini RAM’in bambaşka bir yerinde bulabilir. GNU/Linux sistemlerde sırf bu iş için ayrılmış özel disk partition’ları vardır ki buna “swap (takas) alanı” deriz. Hep verdiğim bir örnek. Bilgisayara gelince GB’ler düzeyinde RAM isteyen GTA V oyunu, Xbox 360 konsollarda da çalışıyor. Xbox 360’ın RAM miktarı ise 512 MB. Bu yazılımın, sırf Xbox 360 müşterilerine de satılabilmesi için nasıl kırpıldığını ve optimize edildiğini görebiliyor musunuz? O zaman biraz daha çaba sarf etseler, bilgisayarlarda da 2 – 4 GB RAM’de rahatlıkla çalışacak hâle getirirler değil mi? Getirmezler. RAM üreticileri RAM satacak çünkü 🙂

Yukarıda saydığım örnekler, PCB’de tutulan verilerin sadece bir kısmı. Bu process’i kim çalıştırdı? Önceliği nedir? Ne kadar kaynak tüketti? Ama sanırım olay anlaşılmıştır. Process’leri yönetebilmek için, process’lerle ilgili tuttuğumuz kayıtlar bütününe Process Control Block diyebiliriz. Bazı kaynaklar bu kayıtlara “Process Table” der.

Sonuç Olarak

Bütün bu yazının konusu “çift tıkladım açtım”. Ve daha hiçbir şey olmadı. Sadece çalışan şeyin ne olduğunu ve nasıl tutulduğunu anlamaya çalıştık. CPU’ya nasıl geldi, veriyollarını nasıl kullandı, diskte nasıl bulundu, RAM’e nasıl yazıldı, öncelikler neye göre ve nasıl belirlendi, içinden elektrik akımı geçen bir şey nasıl bu işleri başardı?

Bilgisayarları sevin, Bilgisayar Bilimi’ne emek veren insanları sayın. Umarım faydalı olabilmişimdir.

Bağlantılar

1 – Code segment, https://en.wikipedia.org/wiki/Code_segment

2 – Data segment, https://en.wikipedia.org/wiki/Data_segment

3 – Call stack, https://en.wikipedia.org/wiki/Call_stack

4 – Stack buffer overflow, https://en.wikipedia.org/wiki/Stack_buffer_overflow

5 – Memory management #Dynamic memory management, https://en.wikipedia.org/wiki/Memory_management#HEAP

6 – Interrupt request (PC architecture), https://en.wikipedia.org/wiki/Interrupt_request_%28PC_architecture%29

7 – Orphan process, https://en.wikipedia.org/wiki/Orphan_process

8 – Zombie process, https://en.wikipedia.org/wiki/Zombie_process

Referans

1 – Operating System Concepts Essentials, Abraham SILBERSCHATZ (Yale University), Peter Baer GALVIN (Corporate Technologies, Inc.), Greg GAGNE (Westminster College)

2 – Modern Operating Systems, Andrew S. TANENBAUM, Herbert BOS (Vrije Universiteit)