> UNIX/Linux sistemlerinde Prosesler Hakkında Temel Bilgiler: Daha önce de anlatıldığı üzere çalışmakta olan bütün programlara "proses" denmektedir. Dolayısıyla her bir prosesin, sistem genelinde o an için tek olan, tam sayısal bir "ID" değeri vardır. Sistem ilk "boot" edildiğinde ki "boot" kodu "0" numaralı "ID" ye ilişkin oluyor. Daha sonra "1" numaralı "ID" ye sahip başka bir proses meydana getiriliyor. Bu noktadan sonra da diğer prosesler hayata gelmeye başlıyor. İş bu "1" numaralı "ID" ye sahip prosese ise "init" prosesi denirken, artık bu noktadan sonra "0" "ID" numaralı proses kullanım dışı kalıyor, kullanılmıyor. Dolayısıyla "0" numarası da kullanılmamaktadır. Öte yandan "1" numaralı proses artık hayatını yaşamaya devam etmektedir. Hayatı biten prosesin "ID" değeri, yeni oluşturulacak proseslerce kullanılabilir fakat bilinmeyen bir "t" anında her proses kendisine has "ID" değerine sahiptir. Proseslerin "ID" değerleri, o prosese ait olak kontrol bloklarına erişmek için bir anahtar görevindedir. Buradaki mekanizma "hash-table" biçimindedir. Proseslerin "ID" değerleri sistemden sisteme değişebileceği için UNIX/Linux türevi sistemlerde "pid_t" tür eş ismi ile temsil edilmektedirler. Bu isim ise "unistd.h" ve "sys/types.h" içerisindedir. Standartlarca bu tür "işaretli" bir tür olmak zorundadır. O an çalışan prosesin "ID" değerini elde etmek için "getpid" isimli fonksiyonu kullanabiliriz. İş bu fonksiyonun prototipi aşağıdaki gibidir; #include pid_t getpid(void); Bu fonksiyon başarısız olamaz. Bir POSIX fonksiyonudur. * Örnek 1, #include #include #include int main() { /* # OUTPUT # 5145 5145 */ pid_t pid; pid = getpid(); /* * Arka plandaki türün ne olacağı garanti altına alınmadığından, * sadece işaretli bir tam sayı olabileceğinden dolayı, * en kötü senaryo düşünülerek "long long" türüne dönüştürülmüştür. */ printf("%lld\n", (long long)pid); /* * Bazı derleyiciler, "long long" türünden de büyük işaretli tam sayı * tanımlamış olabilirler. İş bu türlerin eş ismi ise C99 ile dile * eklenen "intmax_t" türüdür. Yukarıdaki dönüşüme alternatif olarak * bu türe de dönüşüm yapabiliriz. */ printf("%jd\n", (intmax_t)pid); return 0; } Bir proses, başka bir proses tarafından hayata getirilir. Sistem "boot" edilirken hayata gelen ilk proses "0" numaralı olmasına karşın, "1" numaralı proses hayata gelmeden evvel hayatı bitmektedir. Dolayısıyla bütün proseslerin atası "1" numaralı ve "init" isimli prosestir. Bu "change" olayına da "swapper" ya da "pager" denmektedir. İşletim sistemi proseslere "ID" değeri verirken, en son hayata gelen prosesin "ID" değerinin bir fazlasını vermektedir. Maksimum rakama ulaştığında, bütün "ID" listesini en baştan gezip hayatı bitenlerinkini vermeye başlıyor. Maksimum proses "ID" değeri ise sistemden sisteme değişiklik göstermektedir. Anımsanacağı üzere, prosesler başka prosesler tarafından sistem fonksiyonları kullanılarak hayata getirilirler. Bu durumda hayata gelen prosese "child process (alt proses)" denirken, onu hayata getirene ise "parent process(alt proses)" denmektedir. Üst prosesleri aynı olan proseslere ise "sibling process (kardeş proses)" denilmektedir. Fakat kardeşlik bazı sistemlerde, bazı konularda önem arz etmektedir. Bir prosesin üst prosesinin "ID" değerini ise "getppid" isimli POSIX fonksiyonunu kullanarak öğreneiliriz. Bu fonksiyon ise "unistd.h" başlık dosyasında bildirilmiştir. Fonksiyonun prototipi de aşağıdaki gibidir; #include pid_t getppid(void); Bu fonksiyonun da başarısız olması beklenemez çünkü her prosesin bir üst prosesi vardır. Bir proses hayata geldiğinde, onu hayata getiren prosesten bağımsız çalışmaktadır dolayısıyla ömrü, onu hayata getirenden evvel de bitebilir sonrasında da. Evvelce bitmesi durumunda alt prosesimiz artık "orphan process (yetim/öksüz proses)" olacaktır ve işletim sistemi tarafından kendisine yeni bir üst proses atanacaktır. Yeni atanılan bu proses ise "1" numaralı "ID" değerine sahip, "init" isimli, prosestir. * Örnek 1, #include #include int main() { /* # OUTPUT # pid : 439 ppid: 438 */ pid_t pid, ppid; pid = getpid(); printf("pid : %lld\n", (long long)pid); ppid = getppid(); printf("ppid: %lld\n", (long long)ppid); return 0; } Bir sistemde proseslerin sahip olabileceği maksimum "ID" değeri, bir prosesin hayata getirebileceği maksimum proses adedi ve o an çalışabilecek maksimum proseslerin adetleri limitler ile sınırlandırılmıştır. Bu limitler kalıcı olarak değiştirilebildiği gibi anlık olarak da değiştirilmektedir. Bu limitler sistemin kapasitesine, sistemdeki bileşenlerin (örn. RAM, HDD) büyüklüğüne göre de değişmektedir. Yani her sistemde bu limitler aynı değildir. Örneğin, Linux sistemlerinde "/proc/sys/kernel" dizini içerisindeki: >> "threads-max" isimli dosyada, aynı anda var olabilecek toplam proseslerin sayısı belirtilmiştir. Bu sayı o an hayatta olan proses ve "thread" lerin toplamıdır (Linux sisteminde "thread" ler de prosesler olarak ele alınırlar). Unutmamalıyız ki bu dosyada belirtilen rakam, o sistemin özelliklerine göre değişiklik göstermektedir. >> "pid_max" isimli dosyada, bir prosesin alabileceği maksimum "ID" değeri yazmaktadır. İşletim sistemi, bu değerden sonra tekrar baştan başlamak suretiyle, hayatı biten proseslerin "ID" değerini yeni prosesler için kullanacaktır. Fakat bu değer ARTMAMAKTADIR. 32-bit ve 64-bit sistemlerde değişiklik göstermektedir. Öte yandan, belli bir kullanıcının hayata getirebileceği toplam proses ve "thread" değeri, "getrlimit" ya da "ulimit -u" kabuk çağrısı ile elde edilebilir. Fakat unutmamalıyız ki "root" kullanıcılar ya da yeterli yetkiye sahip diğer kullanıcılar, bu tip kısıtlamalara maruz kalmamaktadır. Bu limit değerlerinin bir sonraki "boot" anına kadar değiştirmek için ilgili dosyaları açıp yeni limit değerlerini girerek yapılabilir ya da "sysctl" kabuk komutu kullanılabilir. Fakat kalıcı değişiklik istiyorsak, sistem "boot" edilirken başvurulan bazı konfigürasyon dosyalarını değiştirmemiz lazım. Örneğin, "/etc/sysctl.conf" dosyasına yeni limitler girmek. Artık sistem her açıldığında, bu limitler ile açılacaktır. Ek olarak "kernel parameters" yoluyla da değiştirilebilir. >> "kernel parameters": Nasıl ki bizlerin yazdığı programlar komut satırı argümanları alıyorsa, "kernel" de komut satırından argüman almaktadır. "kernel" de kendini "initialize" ederken bu geçilen parametreleri kullanmaktadır. > UNIX/Linux sistemlerinde proseslerin hayata getirilmesi: Bu tip sistemlerde bir prosesi hayata getirmenin yegane yolu "fork" isimli POSIX fonksiyonunu çağırmaktır. Bu fonksiyon ise ilgili sistemlerdeki sistem fonksiyonlarını çağırmaktadır. Örneğin, Linux sistemlerinde "sys_fork" / "sys_clone" isimli sistem fonksiyonları çağrılmaktadır. "fork" fonksiyonunun prototipi aşağıdaki gibidir; #include pid_t fork(void); "fork" fonksiyonu aslında şöyle bir temayı yapmaktadır; bir klonlama makinesine bir kişi girdiğinde, çıkışta birbiri ile aynı olan iki kişi olacaktır. Klonlama sonrasında bu iki kişinin GEÇMİŞİ AYNI OLACAKTIR. Fakat gelecekte başlarına neler geleceği kesin değildir ve bu iki kişi hayatlarını birbirinden bağımsız bir şekilde sürdüreceklerdir. İşte bizim programımızın akışı bu tema gibidir; Yani, akış "fork" fonksiyonuna giriyor ve çıkıyor. Fakat "fork" içerisinde halihazırda mevcut olan prosesin kontrol bloğunun birebir kopyası çıkartılıyor. Sadece bir iki nokta birbiri ile kopyalanmamaktadır. "fork" fonksiyonundan çıktıktan sonra birbirinden ayrı iki proses olarak hayatlarını devam ettiriyorlar. BU DA DEMEKTİR Kİ BU PROSESLERE AİT OLAN BELLEK ALANLARI DA BİRBİRİNDEN BAĞIMSIZDIR. ÇÜNKÜ PROSESİN KONTROL BLOĞU KOPYALANIRKEN, PROSESLERE AİT BELLEK ALANLARI DA KOPYALANMAKTADIR. "fonk" fonksiyonundan hem yeni hayata gelen hem de kopyalanan proses çıkmaktadır. Alt prosesin ömrü, "fork" fonksiyonundan çıkmadan evvelki son andan itibaren başlamıştır. Bellek alanları birbirinden ayrı olduğu için, birinin yaptığı değişikliği diğeri görmeyecektir. Burada unutmamamız gereken nokta, "fork" fonksiyonundan sonraki kod parçacıkları her iki proses tarafından da çalıştırılacaktır(varsayılan durumda). Fakat bizler "if" blokları ile proseslerin yapacağı işleri ayırabiliriz. Pekiyi bizler bunu nasıl başaracağız? "fonk" fonksiyonunu çağıran prosese üst proses, klonlama işlemi sonrasında hayata gelen prosese de alt proses denmektedir. Fakat üst proses, "fork" fonksiyonundan çıkarken, alt prosesin "ID" değeri ile geri dönmektedir. Alt proses ise "0" "ID" ile geri dönmektedir ama bu demek değildir ki alt prosesin "ID" değeri "0" dır. Sadece bu fonksiyonun geri dönüş değeri sıfır ise alt proses, değil ise üst proses anlamındadır. Alt prosesin "ID" değeri ayrıdır. "fork" fonksiyonu BAŞARISIZ OLABİLİR. Bu durumda "-1" ile geri dönmektedir. * Örnek 1, "fork" fonksiyonunun kullanımı: #include #include #include #include void exit_sys(const char* msg); int main() { /* # OUTPUT # Hello World! Hello World! */ pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); printf("Hello World!\n"); /* * Bu fonksiyonu çağırma sebebimiz, iki proses arasında bir gecikme * sağlayarak, çıktının daha okunabilir olmasını sağlamaktır. */ sleep(1); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Tipik bir "fork" fonksiyonunun kullanımı. Aşağıdaki kalıbı kullanmamız tavsiye edilmektedir: #include #include #include #include void exit_sys(const char* msg); int main() { /* # OUTPUT # [1456] : Process ID of master process. [1460] : Fork retun value. ------------------------------------------------------- [1456] : Process ID of master process. [1455] : Parent process ID of master process. ------------------------------------------------------- Hello World! [0] : Fork retun value. ++++++++++++++++++++++++++++++++++++++++++++++++++++++ [1460] : Process ID of slave process. [1456] : Parent process ID of slave process. ++++++++++++++++++++++++++++++++++++++++++++++++++++++ Hello World! */ pid_t pid; printf("[%lld] : Process ID of master process.\n", (long long)getpid()); if((pid = fork()) == -1) exit_sys("fork"); printf("[%lld] : Fork retun value.\n", (long long)pid); if(pid != 0) { /* * Üst proses, alt prosesin "ID" değeri ile klonlamadan çıktığı için, * bu bloktaki kodlar üst proses tarafından koşulacaktır. */ puts("-------------------------------------------------------"); printf("[%lld] : Process ID of master process.\n", (long long)getpid()); printf("[%lld] : Parent process ID of master process.\n", (long long)getppid()); puts("-------------------------------------------------------"); } else { /* * Alt proses, sıfır ile klonlamadan çıktığı için, * bu bloktaki kodlar da alt proses tarafından koşulacaktır. */ puts("++++++++++++++++++++++++++++++++++++++++++++++++++++++"); printf("[%lld] : Process ID of slave process.\n", (long long)getpid()); printf("[%lld] : Parent process ID of slave process.\n", (long long)getppid()); puts("++++++++++++++++++++++++++++++++++++++++++++++++++++++"); } printf("Hello World!\n"); /* * Bu fonksiyonu çağırma sebebimiz, iki proses arasında bir gecikme * sağlayarak, çıktının daha okunabilir olmasını sağlamaktır. */ sleep(1); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 3, #include #include #include #include void exit_sys(const char* msg); int g_x = 100; int main() { /* # OUTPUT # g_x = 100 g_x = 100 */ pid_t pid; printf("g_x = %d\n", g_x); if((pid = fork()) == -1) exit_sys("fork"); if(pid != 0) { g_x = 1000; } else { /* * Klonlama işlemi sonrasında proseslerin bellek alanları da * kopyalandığı için, "g_x" değişkenininden iki tane vardır. * Dolayısıyla aslında değiştirilen üst prosesin içerisindeki * "g_x" değişkenidir. */ printf("g_x = %d\n", g_x); } sleep(1); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Burada unutmamamız gereken nokta, "fork" fonksiyonundan sonra aynı kodlardan iki tane olmasıdır. Dolayısıyla iki tane "pid" değerinin olması ve her prosesin kendi "pid" değişkenine yeni değeri yazması söz konusudur. Alt proses hayatına "fork" fonksiyonunun sonunda hayata geldiği için de tekrardan "fork" çağrısı yapılmamakta, sonsuz döngüye girmemektedir. Son olarak, "fork" fonksiyonundan çıkan her iki fonksiyonun hangisinin ilk çıktığının bir garantisi yoktur. Bu, işletim sisteminin çizelgeleyici algoritmalarına bağlıdır. * Örnek 1, #include #include int main() { /* # OUTPUT # Common code... Common code... Common code... Common code... Common code... Common code... Common code... Common code... */ /* * Aşağıdaki "fork" çağrısı sonrasında iki prosesimiz vardır. * process * / \ * process process */ fork(); /* * Yukarıdaki çağrıdan sonra meydana gelen iki prosesin her birisi * aşağıdaki çağrıdan dolayı klonlanacaktır. Dolayısıyla artık dört * tane prosesimiz vardır. * process * / \ * process process * / \ / \ * process process process process */ fork(); /* * Yukarıdaki çağrıdan sonra meydana gelen dört prosesin her birisi * aşağıdaki çağrıdan dolayı klonlanacaktır. Dolayısıyla artık sekiz * tane prosesimiz vardır. * process * / \ * process process * / \ / \ * process process process process * / \ / \ / \ / \ * process process process process process process process process */ fork(); printf("Common code...\n"); sleep(1); return 0; } * Örnek 2, #include #include int main() { /* # OUTPUT # a : 3 a : 3 a : 3 a : 3 a : 3 a : 3 a : 3 a : 3 */ int a = 0; /* * Aşağıdaki klonlama işlemi öncesinde 'a' değişkeninin değeri '1' oldu. * Klonlama sonrasında iki tane 'a' değişkeni oldu ve değerleri '1'. */ ++a; fork(); /* * Aşağıdaki klonlama işlemi öncesinde 'a' değişkeninin değeri '2' oldu. * Klonlama sonrasında dört tane 'a' değişkeni oldu ve değerleri '2'. */ ++a; fork(); /* * Aşağıdaki klonlama işlemi öncesinde 'a' değişkeninin değeri '3' oldu. * Klonlama sonrasında sekiz tane 'a' değişkeni oldu ve değerleri '3'. */ ++a; fork(); printf("a : %d\n", a); sleep(1); return 0; } Bu örneklerde de görüldüğü üzere klonlama işlemi sonrasında her bir proses aynı koda sahip fakat artık birbirleri arasında bir bağ kalmamıştır. Klonlama işlemi sırasında proseslerin kontrol bloklarının da birbirine kopyalandığından bahsetmiştik. Peki iş bu kontrol blokları içerisinde bir gösterici tarafından gösterilen dosya betimleyici tablosunun durumu nasıl olacak? Bildiğiniz üzere iki tür kopyalama türü vardır. Bunlardan birisi "Deep Copy(Derin Kopyalama)", diğeri ise "Shallow Copy(Sığ Kopyalama)". İkisi arasındaki fark; derin kopyalama yaparken gösterici tarafından gösterilen bütün nesnelerin de birer kopyalarının çıkartılmasıyken, sığ kopyalamada sadece göstericinin birer kopyasının çıkartılması ve böylece iki göstericinin aynı nesneyi göstermesidir. "fork" işlemi sırasında da prosesin "Process Control Block" ğu ve bu blok içerisindeki gösterici tarafından gösterilen "File Description Table" ın birebir kopyası çıkartılıyor. Yani bu noktaya kadar derin kopyalama yapılıyor. Artık iki tane "Process Control Block" ve "File Description Table" var. Fakat bu "File Description Table" lar tarafından gösterilen "file" türünden nesnelerin KOPYASI ÇIKARTILMIYOR. Dolayısıyla her iki "File Description Table" içerisinde bulunan "fd" ler, aslında tek bir "file" türünden nesneyi gösteriyor. Yani bu nokta için sığ kopyalama yapılıyor. Günün sonunda alt ve üst prosesler aynı "file" türden nesneyi gösteriyorlar. Dolayısıyla bu "file" içerisindeki referans sayaçları da bir artıyor. Bu da şu manaya geliyor; alt ya da üst prosesten birisi, dosya konumlandırıcısını değiştirirse, diğeri artık yeni konumu görecektir(dosyayı açtıktan sonra "fork" işleminin yapıldığı varsayılmıştır). * Örnek 1, Klonlama sonrasında her iki proses de aynı "file" nesnesini göstermektedir. #include #include #include #include void exit_sys(const char* msg); int main() { /* # q.txt # Ahmet Kandemir Pehlivanli */ /* # OUTPUT # Kandemir */ int fd; if((fd = open("q.txt", O_RDONLY)) == -1) exit_sys("open"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); if(pid != 0) { lseek(fd, 5, SEEK_SET); } else { char buffer[10 + 1]; ssize_t result; if((result = read(fd, buffer, 10)) == -1) exit_sys("read"); buffer[result] = '\0'; puts(buffer); } close(fd); sleep(1); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Buradan da diyebiliriz ki o an çalışmakta olan bütün prosesler aslında aynı "file" türünden nesneleri göstermektedir. * Örnek 1, #include #include #include #include void exit_sys(const char* msg); int main() { /* # OUTPUT # OK OK */ pid_t pid; /* * Standart C dosya fonksiyonları tamponlu çalışmaktadır ve "stdout" ise * UNIX/Linux standartlarına göre satır tamponludur. Yazının sonunda * '\n' karakteri olmadığı için, yazı hala tamponda bekletilmektedir. */ printf("OK"); if((pid = fork()) == -1) exit_sys("fork"); /* * Klonlama işlemi sonrasında tamponlar da kopyalanmaktadır. Dolayısıyla * artık iki tamponumuz var. Aşağıdaki çağrıdan dolayı tampona '\n' * karakteri geleceğinden, tampon boşaltılacaktır. İş bu sebepten * dolayı ekrana iki defa basılmıştır. */ printf("\n"); sleep(1); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Eskiden "thread" kavramı olmadığı için işin bir kısmını "fork" ile hayata getirdiğimiz proseslere yaptırtıyorduk. Fakat "thread" konusu artık hayatımızın bir parçası olduğu için böyle senaryolarda "thread" leri kullanmaktayız. > UNIX/Linux sistemlerinde proseslerin sonlandırılması: Proseslerin sonlandırılması da yine sistem fonksiyonları tarafından yapılmaktadır. Linux dünyasında bu sistem fonksiyonunun adı "sys_exit". Bir POSIX fonksiyonu olan "_exit" ise prosesleri sonlandırmaktadır. Standart C fonksiyonu olan "exit" de prosesleri sonlandırmaktadır. Fakat "exit" ile "_exit" fonksiyonlarını birbiri ile karıştırmamalıyız. Standart C fonksiyonu olan "exit", Unix/Linux sistemlerinde yine "_exit" fonksiyonunu çağırmaktadır ki "_exit" ise "sys_exit" isimli sistem fonksiyonunu çağırmaktadır. Fakat Standart C fonksiyonu, "_exit" çağrısından önce bir takım işlemleri de yerine getirmektedir. Örneğin, iş bu fonksiyon "stdio" tamponlarını boşaltıyor, bütün "stdio" dosyalarını "fclose" ile kapatıyor ki bu fonksiyon da aslında arka planda yine "close" isimli POSIX fonksiyonunu çağırmaktadır. Tabii diğer yandan "_exit" fonksiyonu da prosesi sonlandırmadan evvel işletim sistemi düzeyinde açılmış olan ve "File Description" lar tarafından gösterilen "file" nesnelerini kapatıyor. Nihayetinde "exit" fonksiyonu, "_exit" fonksiyonunu çağırmaktadır. Fakat sizin de fark edeceğiniz üzere Standart C fonksiyonu, C standartlarında yapılan işlemleri de ele almaktadır. POSIX fonksiyonu olan ise yine POSIX standartlarında yapılan işlemleri ele almaktadır. "_exit" fonksiyonunun prototipi aşağıdaki gibidir; #include void _exit(int status); Fonksiyon, parametre olarak prosesin "exit code" almaktadır. POSIX standartlarında da belirtildiği üzere, "_exit" fonksiyonu "stdio" tamponlarını boşaltmamaktadır. Çünkü hatırlarsanız, bu tamponlu mekanizma tamamiyle Standart C kütüphanesinin oluşturduğu bir mekanizma. Fakat "open" ile açtığımız dosyaları bu fonksiyon kapatmaktadır. İş bu fonksiyon, başarılı sonlanmalar için "0" ile ama başarısız sonlanmalar için "non-zero" bir değer ile dönmektedir. Standart C fonksiyonu olan "exit" ise aşağıdaki parametrik yapıya sahiptir; #include void exit(int status); Yukarıda da belirtiğimiz üzere, "exit" fonksiyonu prosesi sonlandırmadan evvel bir takım işlemler yapmaktadır. Bu işlemlere, C kütüphanelerinin hayata gelirken yaptığı işlemlerin tekrar geri verilmesi de eklenebilir. Yani "exit" fonksiyonu, Standart C kütüphanelerinin içi gereklidir. İş bu "exit" fonksiyonu, ayrıca, "atexit" ile kayıt altına alınmış fonksiyonları, kayıt sırasına ters olarak çağırmaktadır. "atexit" fonksiyonu ise aşağıdaki parametrik yapıya sahiptir; #include int atexit(void (*func)(void)); İş bu fonksiyon, başarılı olma durumunda "0" ile aksi halde "non-zero" ile geri dönmektedir. Bu fonksiyonun işlevi, bu fonksiyon ile kayıt altına alınan fonksiyonları tekrardan çağırmak içindir. * Örnek 1, #include #include void foo(void) { printf("foo\n"); } void bar(void) { printf("bar\n"); } int main() { /* # OUTPUT # bar foo */ atexit(foo); atexit(bar); /* * "exit" fonksiyonu, "atexit" ile kayıt altına alınanları * ters sıraya göre çağıracaktır. Yani ilk olarak "bar", * sonrasında ise "foo" çağrılacaktır. */ exit(EXIT_SUCCESS); return 0; } C standartlarına göre, eğer bir program içerisinde hiç "exit" çağırmaz isek, programın akışı "main" fonksiyonundan çıktıktan sonra "main" fonksiyonunun geri dönüş değeri ile "exit" fonksiyonu çağrılmaktadır. Yani "exit" her halükarda çağrılmaktadır. * Örnek 1, #include #include void foo(void) { printf("foo\n"); } void bar(void) { printf("bar\n"); } int main() { /* # OUTPUT # bar foo */ atexit(foo); atexit(bar); /* * "main" içerisinde elle "exit" çağrısı yapmadık fakat C standartlarınca * programın akışı "main" fonksiyonundan çıktıktan sonra, "main" in geri * dönüş değeri ile "exit" çağrılıyor. "exit" çağrıldığı için "atexit" * ile kayıt altına alınanlar da yine ters sıra ile çağrılıyor. */ return 31; } Fakat C dilinde proses sonlandırmalar "exit" fonksiyonu ile birlikte "abort" fonksiyonuyla da yapılmaktadır. İş bu fonksiyon "abnormal" bir sonlanmaya yol açmaktadır. Yani "exit" fonksiyonunun aksine "stdio" tamponlarını BOŞALTMAMAKTADIR. "abort" fonksiyonu da aşağıdaki parametrik yapıya sahiptir; #include void abort(void); Bu fonksiyonu, programda ciddi sorunlar meydana geldiğinde çağırabiliriz. Bu fonksiyon, UNIX/Linux sistemlerinde, "SIGABRT" biçimindeki bir sinyal oluşmasına yol açıyor. İş bu sinyal ise prosesin sonlanmasına yol açıyor. * Örnek 1, #include #include #include void foo(void) { printf("foo\n"); } void bar(void) { printf("bar\n"); } int main() { /* # OUTPUT # */ atexit(foo); atexit(bar); printf("Hello World!"); /* * İş bu C fonksiyonu "abnormal" biçimde sonlandığı * için yine ekrana bir şey basılmamıştır. Ek olarak * kayıt altına aldığımız diğer fonksiyonlar da * çağrılmamıştır. */ abort(); return 0; } Linux sistemleri için prosesleri sonlandırmayı özetlemek gerekirse; exit() ----> _exit() ----> sys_exit Pekiyi iş bu "exit code" kavramı nedir? Açıkçası işletim sistemi, "_exit" içerisine yazdığımız rakam ile hiç ilgilenmemektedir. İşletim sistemi sadece bu rakamı üst prosese iletmek ile ilgileniyor. Yani bizim prosesimizi hayata getiren üst proses, bizim prosimizin sonlanırken kullandığı "exit code" u öğrenmek isterse, bunu kendisine iletiyor. Bu bahsi geçen rakam, üst proses ile alt proses arasındaki ilişkide anlam kazanıyor. O zaman buradan şunu çıkartabiliriz; "exit" fonksiyonu arka planda "_exit" fonksiyonunu çağırmakta ve çağırırken de bizim "exit" e geçtiğimiz değeri kullanmakta. Öte yandan işletim sistemi bu rakamın ne olduğu ile ilgilenmemekte. Dolayısıyla "exit" e geçtiğimiz rakamdan bağımsız bir şekilde, "exit" fonksiyonu yine tamponları boşaltma vs. işlemleri YERİNE GETİRMEKTEDİR. "exit" fonksiyonu çağrıldığında ilk olarak "atexit" ile kayıt altına aldığımız fonksiyonlara ters sıra ile çağrı yapmakta, daha sonra "tmpfile" fonksiyonu ile oluşturulan geçici dosyaları siler, ilgili tamponları boşaltır ve bunları kapatır. * Örnek 1, #include #include #include void foo(void) { printf("foo"); } void bar(void) { printf("bar\n"); } int main() { /* # OUTPUT # Hello World!bar foo */ atexit(foo); atexit(bar); printf("Hello World!"); /* * Burada gerçekleşen şey şudur; * i. "Hello World!" yazısı tampona alınır. * ii. Programın akışı "main" den çıktıktan sonra "exit(0)" çağrısı yapılır. * iii. "atexit" ile kayıt altına alınanlar ters sıra ile çağrılır. * iv. Tampona "bar\n" yazısı da alınır. "stdout" dosyası satır tamponlamalı * olduğu için "Hello World!bar\n" yazısı tampon boşaltıldığı için ekrana çıkar. * v. Tampona "foo" yazısı alınır. * vi. "stdout" tamponu boşaltılacağı için "foo" yazısı da ekrana basılır. */ return 0; } * Örnek 2, #include #include #include void foo(void) { fprintf(stderr, "foo"); } void bar(void) { fprintf(stderr, "bar"); } int main() { /* # OUTPUT # barfooHello World! */ atexit(foo); atexit(bar); printf("Hello World!"); /* * Burada gerçekleşen şey şudur; * i. "Hello World!" yazısı "stdout" tamponuna alınır. * ii. Programın akışı "main" den çıktıktan sonra "exit(0)" çağrısı yapılır. * iii. "atexit" ile kayıt altına alınanlar ters sıra ile çağrılır. * iv. "bar" yazısı "stderr" tamponuna alınır. * v. "foo" yazısı da "stderr" tamponuna alınır. * vi. "stderr" dosyası bu sistemlerde sıfır tamponlamalı olduğu için * ekrana "barfoo" yazısı çıkar. * vii. Diğer tamponlar da boşaltılacağı için ekrana "Hello World!" yazısı * çıkar. */ return 0; } > Alt proseslerin sonlanmalarının beklenmesi ve bunların "exit code" larının elde edilmesi: Bu işlem POSIX sistemlerinde iki fonksiyon ile mümkündmür. Bunlar sırasıyla "wait" ve "waitpid" isimli fonksiyonlardır. "waitpid" fonksiyonu, işlevsellik açısından "wait" fonksiyonunu da kapsamaktadır. >> "wait" fonksiyonu: İş bu fonksiyon üst prosesi bloke etmektedir ta ki alt proses sonlanana kadar ve aşağıdaki parametrik yapıya sahiptir; #include pid_t wait(int* status); Parametrelerinden de anlaşılacağı üzere argüman olarak "int" türden bir değişkenin adresini almaktadır. İş bu fonksiyon, çağrıldıktan sonra, ilk sonlanacak alt prosesi beklemektedir. Örneğin, peş peşe "fork" işlemi gerçekleştirelim. İlk sonlanan alt prosesi bu fonksiyon kaale alacaktır. Ek olarak, iş bu fonksiyon, kendisini çağıran "thread" i BLOKE ETMEKTEDİR. Bu bloke işlemi ilk alt proses sonlanana kadar sürmektedir. Fakat bu fonksiyonu çağırdığımız an, halihazırda sonlanmış bir fonksiyon varsa, bu fonksiyon bloke işlemini GERÇEKLEŞTİRMEZ. İş bu fonksiyonun geri dönüş değeri, ömrü biten proses aittir. Yani başarı durumunda "exit code" u alınan prosesin "ID" değeri ile geri döner. Bu fonksiyona argüman olarak "int" türden değişkenin adresinin geçildiğinden bahsetmiştik. İşte alınan "exit code" bu değişkenin içerisine yazılmaktadır fakat POSIX standartlarınca kaçıncı bitlerde "exit code" olduğu, hangi bitlerde hangi bilgilerin tutulduğu garanti altına alınmamıştır. Bunun için makrolar yazılmıştır. Unutmamalıyız ki prosesler "abnormal" biçimde, sinyal mekanizması kullanılarak, sonlanabilir. Bu durumda bu prosesler için "exit code" oluşmayacaktır. Bir prosesin "exit code" u temin edebilmek için, prosesin normal biçimde sonlanmış olması gerekmektedir. İş bu makrolar; >>> "WIFEXITED" : Bir prosesin normal mi "abnormal" mi sonlandığı bilgisini döndürmektedir. Normal sonlanmalarda "non-zero" ile geri dönmektedir. Bu makroya, "wait" fonksiyonuna geçilen "int" türden nesne geçilir ve onun bitlerine bakar. "abnormal" sonlanmalarda ise "0" ile geri dönmektedir. >>> "WIFSIGNALED" : Bir prosesin "abnormal" biçimde, bir sinyal mekanizmasından ötürü, sonlanıp sonlanmadığını sorgulamak için kullanılır. "abnormal" ise "0", aksi halde "non-zero" ile geri döner. >>> "WIFSTOPPED": Bir proses "SIGSTOP" sinyali ile geçici süreyle durdurulmuş olabilir. Bu durumu sorgulamak için kullanılır. >>> "WEXITSTATUS": Bir prosesin "exit code" unu çekmek için kullanılır fakat bunun için prosesin normal biçimde sonlanması gerekmektedir. "wait" fonksiyonuna argüman olarak NULL geçmemiz durumunda, ilk alt proses sonlanana kadar bekleyecektir. Yani "exit code" istemiyorum demek de diyebiliriz. "wait" fonksiyonu çağrıldığında, ilgili üst proses tarafından hayata getirilen alt proses yoksa, ya da fonksiyona geçersiz bir adres fonksiyon BAŞARISIZ OLABİLMEKTEDİR. C standartlarına göre "exit code" için "int" türü denmiştir. Bu fonksiyondaki önemli nokta, prosesin sonlanmasını beklemektir. * Örnek 1, Alt proses beklenmektedir: #include #include #include #include void exit_sys(const char* msg); void child_process(void); int main() { /* # OUTPUT # parent started running... parent resumes running... child is running... 0 child is running... 1 child is running... 2 child is running... 3 child is running... 4 child exited with exit code: 100 parent is about to end... */ printf("parent started running...\n"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); /* Alt proses sonlanacağı için, "else" bloğuna gerek duymadık. */ if(pid == 0) child_process(); /* Alt proses sonlanacağı için bu aşamada sadece üst proses vardır. */ printf("parent resumes running...\n"); int stat; if(wait(&stat) == -1) exit_sys("wait"); if(WIFEXITED(stat)) printf("child exited with exit code: %d\n", WEXITSTATUS(stat)); printf("parent is about to end...\n"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void child_process(void) { for(int i = 0; i < 5; ++i) { printf("child is running... %d\n", i); sleep(1); } exit(100); } * Örnek 2, Alt proses beklenmemektedir: #include #include #include #include void exit_sys(const char* msg); void child_process(void); int main() { /* # OUTPUT # parent started running... parent resumes running... child exited with exit code: 0 parent is about to end... child is running... 0 */ printf("parent started running...\n"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); /* Alt proses sonlanacağı için, "else" bloğuna gerek duymadık. */ if(pid == 0) child_process(); /* Alt proses sonlanacağı için bu aşamada sadece üst proses vardır. */ printf("parent resumes running...\n"); int stat; /* if(wait(&stat) == -1) exit_sys("wait"); */ if(WIFEXITED(stat)) printf("child exited with exit code: %d\n", WEXITSTATUS(stat)); printf("parent is about to end...\n"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void child_process(void) { for(int i = 0; i < 5; ++i) { printf("child is running... %d\n", i); sleep(1); } exit(100); } * Örnek 3, "abnormal" sonlanma durumunda: #include #include #include #include void exit_sys(const char* msg); void child_process(void); int main() { /* # OUTPUT # parent started running... parent resumes running... child is running... 0 child is running... 1 child is running... 2 child is running... 3 child is running... 4 parent is about to end... */ printf("parent started running...\n"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); /* Alt proses sonlanacağı için, "else" bloğuna gerek duymadık. */ if(pid == 0) child_process(); /* Alt proses sonlanacağı için bu aşamada sadece üst proses vardır. */ printf("parent resumes running...\n"); int stat; if(wait(&stat) == -1) exit_sys("wait"); if(WIFEXITED(stat)) printf("child exited with exit code: %d\n", WEXITSTATUS(stat)); printf("parent is about to end...\n"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void child_process(void) { for(int i = 0; i < 5; ++i) { printf("child is running... %d\n", i); sleep(1); } abort(); } * Örnek 4, Alt proses hemen sonlanmış ise: #include #include #include #include void exit_sys(const char* msg); void child_process(void); int main() { /* # OUTPUT # parent started running... parent resumes running... parent is running... 0 child is about to end... parent is running... 1 parent is running... 2 parent is running... 3 parent is running... 4 child exited with exit code: 31 parent is about to end... */ printf("parent started running...\n"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); /* Alt proses sonlanacağı için, "else" bloğuna gerek duymadık. */ if(pid == 0) child_process(); /* Alt proses sonlanacağı için bu aşamada sadece üst proses vardır. */ printf("parent resumes running...\n"); for(int i = 0; i < 5; ++i) { printf("parent is running... %d\n", i); sleep(1); } int stat; if(wait(&stat) == -1) exit_sys("wait"); if(WIFEXITED(stat)) printf("child exited with exit code: %d\n", WEXITSTATUS(stat)); printf("parent is about to end...\n"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void child_process(void) { printf("child is about to end...\n"); exit(31); } "wait" fonksiyonu, birden fazla sonlanmamış varsa ilk sonlananı ele alacaktır. Eğer birden fazla sonlanmış var ise hangisini ele alacağınının bir garantisi YOKTUR. "exit code" ile hangi proses olduğunu öğrenebiliriz. Ek olarak, ne kadar "fork" yaptı isek o kadar "wait" YAPMALIYIZ. * Örnek 1, #include #include #include #include #define NCHILDS 5 void exit_sys(const char* msg); void child_process(int val); int main() { /* # OUTPUT # parent is waiting for childs to exit... child exited with exit code: 101 child exited with exit code: 100 child exited with exit code: 102 child exited with exit code: 103 child exited with exit code: 104 */ pid_t pids[NCHILDS]; printf("parent is waiting for childs to exit...\n"); for(int i = 0; i < NCHILDS; ++i) { if((pids[i] = fork()) == -1) exit_sys("fork"); if(pids[i] == 0) child_process(100 + i); } int stat; for(int i = 0; i < NCHILDS; ++i) { if(wait(&stat) == -1) exit_sys("wait"); if(WIFEXITED(stat)) printf("child exited with exit code: %d\n", WEXITSTATUS(stat)); } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void child_process(int val) { /* "rand" fonksiyonu bütün alt proseslerde aynı değeri üretecektir. */ sleep(rand() % 10 + 1); exit(val); } * Örnek 2, #include #include #include #include #define NCHILDS 5 void exit_sys(const char* msg); void child_process(int val); int main() { /* # OUTPUT # parent is waiting for childs to exit... child exited with exit code: 100 child exited with exit code: 104 child exited with exit code: 101 child exited with exit code: 102 child exited with exit code: 103 */ pid_t pids[NCHILDS]; printf("parent is waiting for childs to exit...\n"); for(int i = 0; i < NCHILDS; ++i) { if((pids[i] = fork()) == -1) exit_sys("fork"); if(pids[i] == 0) child_process(100 + i); } int stat; for(int i = 0; i < NCHILDS; ++i) { if(wait(&stat) == -1) exit_sys("wait"); if(WIFEXITED(stat)) printf("child exited with exit code: %d\n", WEXITSTATUS(stat)); } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void child_process(int val) { srand(val); sleep(rand() % 10 + 1); exit(val); } >> "waitpid" fonksiyonu, "wait" fonksiyonunun daha gelişmiş versiyonudur. Fonksiyonun parametrik yapısı aşağıdaki gibidir; #include pid_t waitpid(pid_t pid, int *stat_loc, int options); İş bu fonksiyonun birinci parametresi, hangi alt prosesi beklemek istiyorsak, onun proses "ID" değeridir. Bu parametre bir kaç biçimde geçilebilir; >>> Eğer bu parametreye "-1" den de küçük değerlerin geçilmesi durumunda, o değerin mutlak değerini "Group ID" olarak sahip olan prosesi ele alacaktır. Yani, "-13560" gibi bir sayı girmemiz durumunda proses "Group ID" değeri "13560" olan proses ele alınacaktır. >>> Eğer bu parametreye "-1" geçilirse, tamamen "wait" gibi davranır. >>> Eğer bu parametreye "0" geçilirse, "Group ID" değeri, bu fonksiyonu çağıran üst prosesin "Group ID" değerine sahip prosesi ele alır. >>> Eğer pozitif bir sayı geçilirse ki en normal durum budur, proses "ID" değeri bu sayı olan prosesi ele alacaktır. Eğer bu parametreye geçilen "ID" değerleri, o prosesin alt prosesine ait değilse başarısız olacaktır. İkinci parametre, "exit code" un yerleştirileceği "int" türden nesnenin adresidir. Yine bu parametreye de NULL adres geçilebilir. Bu durumda alt proses beklenir fakat "exit code" bilgisi elde edilmez. Üçüncü parametre çok önemli değil, genellikle "0" geçilir ama bazı seçenekleri de "bit-wise OR" ile almaktadır. Bu üçüncü parametre şunlardan birisi olabilir; >>> "WEXITED" : İllaki bitmiş prosesleri ele alacaktır. >>> "WSTOPPED" : Bir sinyal ile durdurulan ("stop" edilen) prosesleri ele alıyoruz. >>> "WNOWAIT" : "Ben bekleme yapmak istemiyorum. Sadece sonlanmış varsa onu almak, aksi halde fonksiyon başarısız olsun" anlamındadır. >>> "WNOHANG" : Bu durumda, eğer alt proses henüz sonlanmamışsa, bekleme yapmaz ve başarısızla sonuçlanır. >>> ... Aşağıda pekiştirici örneklere yer verilmiştir; * Örnek 1, #include #include #include #include #define NCHILDS 5 void exit_sys(const char* msg); void child_process(int val); int main() { /* # OUTPUT # parent is waiting for childs to exit... child exited with exit code: 100 child exited with exit code: 101 child exited with exit code: 102 child exited with exit code: 103 child exited with exit code: 104 */ pid_t pids[NCHILDS]; printf("parent is waiting for childs to exit...\n"); for(int i = 0; i < NCHILDS; ++i) { if((pids[i] = fork()) == -1) exit_sys("fork"); if(pids[i] == 0) child_process(100 + i); } /* * Aşağıda, alt prosesler farklı zamanlarda sonlanacaktır fakat onları * ele alırken bizim istediğimiz sıraya göre ele alacağız. */ int stat; for(int i = 0; i < NCHILDS; ++i) { if(waitpid(pids[i], &stat, 0) == -1) exit_sys("wait"); if(WIFEXITED(stat)) printf("child exited with exit code: %d\n", WEXITSTATUS(stat)); } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void child_process(int val) { srand(val); sleep(rand() % 10 + 1); exit(val); } * Örnek 2, #include #include #include #include #define NCHILDS 5 void exit_sys(const char* msg); void child_process(int val); int main() { /* # OUTPUT # parent is waiting for childs to exit... child exited with exit code: 100 child exited with exit code: 104 child exited with exit code: 101 child exited with exit code: 102 child exited with exit code: 103 */ pid_t pids[NCHILDS]; printf("parent is waiting for childs to exit...\n"); for(int i = 0; i < NCHILDS; ++i) { if((pids[i] = fork()) == -1) exit_sys("fork"); if(pids[i] == 0) child_process(100 + i); } /* * Aşağıda, alt prosesler farklı zamanlarda sonlanacaktır. "wait" etkisi * oluşturduğumuz için ilk sonlananı ele alıyoruz. */ int stat; for(int i = 0; i < NCHILDS; ++i) { if(waitpid(-1, &stat, 0) == -1) exit_sys("wait"); if(WIFEXITED(stat)) printf("child exited with exit code: %d\n", WEXITSTATUS(stat)); } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void child_process(int val) { srand(val); sleep(rand() % 10 + 1); exit(val); } BİR PROGRAMCI, "fork" İŞLEMİ İLE HAYATA GETİRDİĞİ HER ALT PROSESİ "wait" / "waitpid" FONKSİYONLARI İLE BEKLEMESİ İYİ BİR TEKNİKTİR. AKSİ HALDE HORTLAK PROSESLER OLUŞACAKTIR. > Hortlak Prosesler ("zombie processes"): "exit" fonksiyonu çağrısından sonra işletim sistemi ilgili prosese ait olan bellek alanını bellekten tamamyile kaldırmaktadır. Dolayısıyla bu prosesin oluşturduğu bütün kaynakları da yok etmektedir. O prosese ait "exit code" ise prosesin kontrol bloğu içerisine yazmaktadır. Fakat işletim sistemi proses kontrol bloğunu ortadan kaldıramıyor. Çünkü işletim sistemi şöyle düşünüyor; Üst proses, "wait" ya da "waitpid" fonksiyonu ile bu "exit code" u henüz almamış ama her an isteyebilir düşüncesiyle beklemektedir. Dolayısıyla proses kontrol bloğu yok edilmeyen fakat ilgili prosese ait bellek alanları vb. şeyleri yok edilen prosesler "zombie" proses denmektedir. Yani özetle alt prosesin bitmesi ve üst prosesin "wait" yapmaması sonucu alt prosesin "zombie process" olarak anılma durumudur. Proseslerin kontrol blokları "kernel" içerisin büyük yer kaplıyorlar. Dolayısıyla "zombie process" durumu uzun vadede tehlikeli sonuçlara neden olacaktır. Sistemde "leak" oluşacaktır. Pekiyi üst proses alt prosesten daha evvel sonlanırsa ne olacak? Bu durumda alt proses öksüz durumda kalacak ve işletim sistemi tarafından üst proses olarak "1" numaralı "ID" değerine sahip "init" isimli proses atanıyor. Bu durumda "zombie" proses OLUŞMAYACAKTIR. Pekiyi şu senaryoda neler olacak? Alt proses sonlandı. Üst proses de "wait" yapmadı fakat o da sonlandı. Bu durumda işletim sistemi artık prosesin kontrol bloğunu yok edecektir. Bundan sebeple diyebiliriz ki "zombie" proses oluşması için gereken şartlar şunlardır; >> Üst proses yeni bir alt proses hayata getirecek. >> Alt proses bitmesine rağmen, üst proses "wait" fonksiyonları ile iş bu alt prosesi beklemeyecek. Bu tip proseslerin önüne geçmenin bir kaç yolu vardır: >> En doğal yolu, üst prosesin "wait" fonksiyonlarını çağırmasıdır. >> Bir diğer yöntem ise sinyal mekanizmalarından faydalanmaktır. Çünkü "wait" fonksiyonları üst prosesin donmasına sebebiyet vermektedir. Alt proses tamamlandığında üst prosese bir sinyal gönderiyor. Eğer üst proses "wait" sırasında bloke olmak istemiyorsa, alt proses tarafından gönderilen bu sinyali İŞLEMELİDİR. >> Son olarak alt proses hayata geldiğinde işletim sistemine bir söz vermeliyiz: "ben bu alt prosesin "exit code" bilgisini almayacağım. İşletim sistemi de alt proses sonlanır sonlanmaz ona ait alanları yok etmektedir. Öte yandan, PROSESİMİZİN ÖMRÜ ÇOK KISA İSE, "zombie" KAVRAMINA TAKILMASAK DA OLUR. Şimdi bu hususun zararlarına değinelim. "zombie process" oluşmasının şu dezavantajları vardır; >> Üst prosesin ömrü fazla değilse, genellikle üst prosesin "zombie process" oluşturması ciddi bir soruna yol açmaz. Ancak üst proses uzun süreli çalışıyorsa, "zombie process" ler önemli bir sistem kaynağının boşa harcanmasına yol açabilir. >>> "zombie process" lere ait proses "ID" değerleri, o proses "zombie process" olmaktan kurtulana kadar, sistem tarafından kullanılamamaktadır. Dolayısıyla zamanla proses "ID" lerin tükenmesine yol açabilir. Pekiyi bizler "zombie process" oluşmasını engellemek için sadece "wait" fonksiyonlarını mı kullanabiliriz? Anımsanacağınız üzere "wait" fonksiyonları çağrıldığında, üst proses "block" edilecektir. Halbuki bazı uygulamalarda üst proses için bu istenmeyen bir durumdur. Böyle senaryolarda elimizde iki adet çözüm vardır fakat ikisi de sinyal yöntemleri ile ilgilidir; >> Alt proses bittiğinde, "SIGCHLD" sinyalinde üst proses "wait" fonksiyonlarını çağırırmak. >> Alt prosesin "exit code" bilgisini almak istemediğimizi işletim sistemine söylersek, alt proses işi bittiğinde ona dair kaynaklar işletim sistemi tarafından boşaltılacaktır. Şimdi de aşağıdaki örneklere bakalım: * Örnek 1, Aşağıdaki örnekte üst proses toplamda 5 saniye çalışmıştır. Bu sürece "wait" fonksiyonlarını da çağırmadığı için kısa süreliğine de olsa alt prosesler "zombie process" durumundadır. Bunu görüntüleyebilmek için başka bir "shell" kabuk programını açmalı, o anki çalışma dizinini iş bu programınki haline getirmeli ve son olarak "ps -u" komutunu çalıştırmalıdır. Ekrana çıkan bilgilerden, "COMMAND" satırında "defunct" ile nitelenen proses "zombie process" tir. O satırın, "STAT" sütununda da "Z+" yazacaktır. Eğer üst proses sonlandıktan sonra aynı kabuk komutunu tekrar çalıştırdığımız zaman ilgili "zombie process" lerin yok olduğunu da göreceğiz. Eğer üst proses senelerce çalışmaya devam etseydi, "zombie process" durumunda olan iş bu alt proses de hayatta kalmaya devam edecektir. Görüldüğü gibi ömrü kısa olan üst prosesler için "zombie process" durumu göz ardı edilebilir. #include #include #include void exit_sys(const char* msg); int main() { /* # OUTPUT # parent process continues to run: 0 child process is about to terminate... parent process continues to run: 1 parent process continues to run: 2 parent process continues to run: 3 parent process continues to run: 4 */ pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); if(pid != 0) { /* Üst Proses */ for(int i = 0; i < 5; ++i) { printf("parent process continues to run: %d\n", i); sleep(1); } } else { /* Alt Proses */ printf("child process is about to terminate...\n"); exit(EXIT_SUCCESS); } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } > "exec" işlemleri: Bir grup fonksiyona verilen ortak isimdir. Farklı parametrik yapılar ile özünde aynı işi yapmaktadırlar. İş bu fonksiyonlar "unistd.h" başlık dosyasında bildirilmiştir. Fakat bu fonksiyonların hepsi arka planda "execve" isimli sistem fonksiyonuna uygun parametreler ile çağrı yapmaktadır. "exec" fonksiyonları "Environment Variables of Processes(Proseslerin Çevre Değişkenleri)" konusu ile yakın ilişki içerisindedir. Çevre değişkenleri konusuna bir sonraki bölümde değinilmiştir. Şimdi iş bu "exec" fonksiyonları esasında bir program dosyasını çalıştırmaktadırlar. Benzer isimde bir grup fonksiyondan oluşmaktadır. Parametrik olarak aralarında farklılıklar vardır. "exec" fonksiyonlarının prototipleri şunlardır; #include extern char **environ; int execl (const char *path, const char *arg0, ... /*, (char *)0 */); int execle (const char *path, const char *arg0, ... /*, (char *)0, char *const envp[]*/); int execlp (const char *file, const char *arg0, ... /*, (char *)0 */); int execv (const char *path, char *const argv[]); int execve (const char *path, char *const argv[], char *const envp[]); // Sistem Fonksiyonu int execvp (const char *file, char *const argv[]); int fexecve(int fd, char *const argv[], char *const envp[]); Bu fonksiyonlara ek olarak, GNU-C kütüphanesinde ve Linux sistemlerine özgü, aşağıdaki "exec" fonksiyonu daha vardır ki prototipleri aşağıdaki gibidir; #include extern char **environ; int execvpe(const char *file, char *const argv[], char *const envp[]); int execveat(int dirfd, const char *pathname, const char *const argv[], const char *const envp[], int flags); Aslında UNIX/Linux sistemleri iş bu "exec" fonksiyonlarının hepsini sistem fonksiyonu olarak bulundurmamaktadır. Örneğin, Linux sistemlerinde "execve" isimli fonksiyon bir sistem fonksiyonu olacak şekilde yazılmıştır. Diğerleri ise iş bu sistem fonksiyonu çağıracak şekilde tasarlanmışlardır. Yine Linux'a özgü olarak "execveat" isimli fonksiyon da bir sistem fonksiyonu olacak şekilde yazılmıştır. "exec" fonksiyonları, proseslerin yaşamlarına başka bir kodla devam etmelerini sağlamaktadır. Çünkü bir "exec" çağrısı sonucunda prosesin kontrol bloğunda ciddi bir değişiklik yapılıyor fakat tamamiyle değiştirilmişyor. Ek olarak o prosesin bellekteki alanı boşaltılıyor ve bu fonksiyona argüman olarak geçilen programın kodları iş bu bellek alanına yükleniyor. POSIX standartlarında bulunan "exec" fonksiyonlarından, >> Bünyesinde 'l' karakteri içerenler komut satırı argümanlarını bir liste biçiminde almaktadır. >> Bünyesinde 'v' karakteri içerenler komut satırı argümanlarını bir "vector" biçiminde almaktadır. >> Bünyesinde 'p' karakteri bulunanlar, argüman olan isimleri, prosesin "PATH" çevre değişkeninde de aranacağını belirtmektedir. >> Bünyesinde 'e' karakteri barındıranlar ise argüman olan isimleri prosesin çevre değişkenlerinde aranacağını belirtmektedir. Linux sistemlerinde bulunan aşağıdaki "exec" fonksiyonları, arka planda "execve" isimli sistem fonksiyonunu çağırmaktadır: -> "execl" -> "execv" -> "execlp" -> "execvp" -> "execle" Benzer şekilde aşağıdaki fonksiyon ise "execveat" isimli sistem fonksiyonunu çağırmaktadır: -> "fexecve" Tüm "exec" fonksiyonları başarı durumunda geri dönmezler. Kendilerinin işlevi prosesi başka bir prosese çevirmek. Dolayısıyla günün sonunda yine tek bir proses var. Ancak ve ancak başarısız durumunda "-1" değerine geri dönerler ve "errno" değişkeni uygun bir şekilde değiştirilir. Dolayısıyla "exec" fonksiyonu genelde alt prosesler tarafından çağrılmaktadır ki bizim üst prosesimiz varlığını devam ettirsin. İş bu sebepten ötürüdür ki "fork-exec" ekseriyetle birlikte kullanılır. "exec" fonksiyonlarının incelenmesi: >> "execl" fonksiyonu, en çok kullanılan versiyondur. Prototipi aşağıdaki gibidir; #include int execl(const char *path, const char *arg0, ... /*, (char *)0 */); Fonksiyonun birinci parametresi çalıştırılacak olan "executable" dosyanın yol ifadesidir. Göreli olabileceği gibi mutlak bir yol ifadesi de olabilir. Tabii göreli olması durumunda, bu fonksiyonu çağıran prosesin "Current Working Directory" konumundan itibaren arama yapılacaktır. Fonksiyonun ikinci parametresi ise çalıştırılacak olan ilgili dosyanın alacağı komut satırı argümanlarıdır. İş bu dosyayı "shell" programından çağırırken yazdıklarımızı bir liste haline getirip bu parametreye geçiyoruz. Fonksiyon, başarı durumunda geri dönmeyecektir fakat başarısız durumda "-1" ile geri dönecektir. Fonksiyonun üçüncü parametresine istediğimiz kadar argüman geçebiliriz. Fakat unutmamalıyız ki en sonki parametre mutlaka "(char*)0" biçiminde olmalıdır, eğer üçüncü parametreye argüman geçmişsek. Çünkü geçilen argümanlar "int" türünden küçük ise "integer promotion", "float" türünden ise "double" türüne dönüştürülür. İş bu sebepten dolayı ve NULL sembolik sabitinin arka planda neye karşılık geldiği bilinmediğinden ki bazı sistemlerde "0" a ama bazı sistemlerde "void *" a denk gelmektedir, NULL sembolik sabitini "char*" türüne bizzat dönüştürmeliyiz. Son olarak bu fonksiyonun birinci parametresi ile ikinci parametresinin aynı olması bir zorunluluk değil fakat iyi bir alışkanlıktır. * Örnek 1, Aşağıdaki çıktıyı elde edebilmek için "mample" isimli çalıştırılabilir bir dosyanın olması gerekmektedir: /* sample.c */ #include #include #include void exit_sys(const char* msg); int main(void) { /* * Prosesin "Current Working Directory" konumunda olan "mample" isimli program * çalıştırılacaktır. Birinci, ikinci, üçüncü ve dördüncü parametreler sanki * aynı programı "shell" programıyla çalıştırmışız gibi "mample" programına * geçilecektir. */ if(execl("mample", "mample", "ali", "veli", "selami", (char*)NULL) == -1) exit_sys("execl"); /* * Prosesin akışı bu noktaya hiç bir zaman gelmeyecektir. Çünkü başarılı * olması durumunda artık "mample" programı çalışacaktır ve onun kodları * koşacaktır. Başarısızlık durumunda "exit_sys" fonksiyonu çağrılacaktır. */ return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* mample.c */ #include #include void exit_sys(const char* msg); int main(int argc, char** argv) { /* # OUTPUT # ./a.out ali veli selamli */ /* * "sample.c" programındaki "execl" fonksiyonuna geçilen, * Birinci parametre çalıştırılabilir dosya olan "mample" dosyasının ismi. * İkinci parametre, "shell" programından direkt olarak "mample" programını * çalıştırdığımız zaman geçtiğimiz argümanlardan ilki, yani "argv[0]". * Üçüncü parametre, "shell" programından direkt olarak "mample" programını * çalıştırdığımız zaman geçtiğimiz argümanlardan ikincisi, yani "argv[1]". * Dördüncü parametre, "shell" programından direkt olarak "mample" programını * çalıştırdığımız zaman geçti ğimiz argümanlardan ikincisi, yani "argv[2]". * Beşinci parametre, "shell" programından direkt olarak "mample" programını * çalıştırdığımız zaman geçtiğimiz argümanlardan ikincisi, yani "argv[3]". */ for(int i = 0; i < argc; ++i) puts(argv[i]); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # total 20 -rwxr-xr-x 1 runner23 runner23 16080 Feb 9 19:37 a.out -rwxrwxrwx 1 root root 655 Feb 9 19:37 main.c */ if(execl("/bin/ls", "/bin/ls", "-l", (char*)NULL) == -1) exit_sys("execl"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 3, #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # total 20 -rwxr-xr-x 1 14085 14085 16152 Feb 9 20:09 a.out -rwxrwxrwx 1 root root 399 Feb 9 20:09 main.c */ /* * Nasılsa programın akışı sadece ve sadece "execl" başarısız * olduğunda aşağı satıra geçecekse, aşağı satıra ilgili fonksiyonun * yazılması makül bir davranıştır. */ execl("/bin/ls", "/bin/ls", "-l", (char*)NULL); exit_sys("execl"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 4, #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # execl: No such file or directory */ execl("/bin/lsS", "/bin/lsS", "-l", (char*)NULL); exit_sys("execl"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Çıktılardan da görüldüğü üzere bizim esas programımız hayatına artık başka kodlar ile devam etmektedir. Pekiyi bizler "shell" programının yaptığı gibi yapmak istiyorsak, yani hem esas programımız hayatına kendi kodları ile devam etsin hem de başka program da çalışsın, o zaman önce "fork", sonrasında da alt proses için "exec" fonksiyonlarından birisini çağırmamız gerekmektedir. Bu yüzdendir ki "exec" fonksiyonları "fork" fonksiyonları ile birlikte kullanılmaktadır. Fakat "login" programı aslında "exec" programını tek başına kullanmaktadır. * Örnek 1, #include #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # main is running. total 20 -rwxr-xr-x 1 14077 14077 16240 Feb 9 21:01 a.out -rwxrwxrwx 1 root root 1381 Feb 9 21:01 main.c parent process is running. */ printf("main is running.\n"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); if(pid == 0) { /* * Alt proses bu bloktaki kodları koşturacaktır. "exec" * fonksiyonunu çağırdığımız için artık hayatına başka * kodlar ile devam edecektir. */ execl("/bin/ls", "/bin/ls", "-l", (char*)0); /* * Fonksiyon sadece başarısız olduğunda geri dönecektir. * Bu durumda da bu fonksiyon çağrılacağı için, alt prosesin * akışı bu bloktan dışarı çıkmayacaktır. * Fakat bu çağrı sonucunda standart C fonksiyonu olan * "exit" çağrılacağı için giriş-çıkış tamponları boşaltılacaktır. * Bunun önüne geçmek için "_exit(EXIT_FAILURE)" çağrısı yapmalıyız. */ exit_sys("execl"); } /* * Bu noktaya sadece üst prosesin akışı gelecektir. */ printf("parent process is running."); /* * Üst prosesimiz, alt prosesi beklemesi gerekmektedir. Çünkü "execl" ile * çalıştırılan prosesin üst prosesi, aslında "fork" fonksiyonunu çağıran * prosesin kendisidir. * Sadece ona dair çıkış kodunu almak istemedik. */ if(waitpid(pid, NULL, 0) == -1) exit_sys("waitpid); // wait(NULL); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, #include #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # main is running. total 20 -rwxr-xr-x 1 14056 14056 16240 Feb 9 21:07 a.out -rwxrwxrwx 1 root root 569 Feb 9 21:07 main.c parent process is running. */ printf("main is running.\n"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); char* myvec[] = { "/bin/ls", "-l", NULL }; /* * "execv" fonksiyonunun ikinci parametresinin adresi bir dizinin başlangıç adresi. * Bu dizinin elemanları birer "const-pointer" ve gösterdikleri yazılar ise "char[]" türden. * Öte yandan argüman olarak geçilen dizinin elemanları ise "pointer", gösterdikleri yazılar * ise yine "char[]" türden. Bu da demektir ki ilgili fonksiyonun gövdesinde, bizim dizimizin * elemanları BAŞKA YAZILARI GÖSTEREMEYECEKTİR. */ if(!pid && execv("/bin/ls", myvec) == -1) exit_sys("execl"); printf("parent process is running."); wait(NULL); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Bu örneklerde "fork" işleminden hemen sonra "exec" işlemi yapılmaktadır fakat bazı zamanlarda "fork" işlemlerinden sonra alt proses için bir takım işlemler yapmamız, bu işlemlerden sonra "exec" çağırmamız gerekebilir. Artık "fork" ve "exec" fonksiyonlarının birlikte kullanım biçimi o anki duruma göre değişiklik gösterecektir. Pekiyi bu iki fonksiyonu birlikte çağırmamız ne kadar verimli? Yukarıda da açıklandığı üzere "fork" işleminden sonra prosesin bellek alanı da kopyalanmaktadır. Bu bellek alanı boşaltılıp, üzerinde "exec" ile yeni programın bellek alanı kopyalanmaktadır. Eski sistemlerde bu durum verimsizdi ve bunun için "vfork" isimli POSIX fonksiyonu kullanılmaktadyı. Bu fonksiyon "fork" işlemi sırasında prosesin bellek alanını düşük seviyede kopyalamaktadır. Fakat bu fonksiyondan hemen sonra "exec" yapmazsak, "Tanımsız Davranış" meydana gelecektir. Lakin işlemcilerin gelişmesi ve yeni teknolojilerin kullanılmasından dolayı artık "vfork" fonksiyonuna gerek kalmamıştır. Çünkü bu kopyalama maliyeti bir takım işlemler ile örtbas edilmetedir. Örneğin, işlemcilerin "sayfalama mekanizmaları (paging mechanizm)" ve ya "Copy On Write" yaklaşımı. Bu iki tekniğe de ilerleyen konularda değinilecektir. "vfork" fonksiyonu 2008 POSIX standartları ile kaldırılmıştır. >> "execv" fonksiyonu, "execl" ye nazaran çalıştırılacak programın komut satırı argümanları için bir gösterici dizisinin adresini istemektedir. Bunun haricinde işlevsel olarak "execl" ile aynıdır. Fonksiyonun imzası şu şekildedir; #include int execv(const char *path, char *const argv[]); Fonksiyonun birinci parametresi çalıştırılacak programın yol ifadesini almaktadır. İkinci argüman ise yine gösterici dizisinin başlangıç adresi. Bu dizinin her bir elemanı "const pointer-to-char" biçimindedir. Yani bu dizinin elemanları başka bir "char[]" türden yazıyı gösteremezler. Fakat bu dizinin gösterdiği yazıda değişiklik yapabiliriz. Bu fonksiyon bir yazılardan oluşan dizinin adresini argüman olarak aldığı için, son argümandan sonra NULL sabitini "cast" etmemize lüzum yoktur. Sadece diziyi oluştururken son elemanı NULL yapacağız. * Örnek 1, #include #include #include #include void exit_sys(const char* msg); int main(int argc, char** argv) { /* # Command Line Arguments # /bin/ls -l */ /* # OUTPUT # main is running. total 20 -rwxr-xr-x 1 14067 14067 16240 Feb 10 00:00 a.out -rwxrwxrwx 1 root root 1083 Feb 10 00:00 main.c parent process is running. */ printf("main is running.\n"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); /* * "execv" isimli fonksiyonun birinci parametresi çalıştırılacak olan * dosyanın ismi. Öte yandan "argv" dizisinin sıfır indisinde ise "main" * programı yer alırken, birinci indisten itibaren bizlerin komut satırına * yazacakları yer alacak. Birinci indis ise çalıştırılacak programın ismi. */ if(!pid && execv(argv[1], argv + 1) == -1) exit_sys("execv"); printf("parent process is running."); wait(NULL); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, #include #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # main is running. total 20 -rwxr-xr-x 1 14056 14056 16240 Feb 9 21:07 a.out -rwxrwxrwx 1 root root 569 Feb 9 21:07 main.c parent process is running. */ printf("main is running.\n"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); char* myvec[] = { "/bin/ls", "-l", NULL }; if(!pid && execv(myvec[0], &myvec[0]) == -1) exit_sys("execv"); printf("parent process is running."); wait(NULL); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 3, /* main.c */ #include #include #include #include void exit_sys(const char* msg); int main(int argc, char** argv) { /* # Command Line Arguments # /bin/cp main.c my_copy_main.c */ /* # OUTPUT # */ printf("main is running.\n"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); if(!pid && execv(argv[1], argv + 1) == -1) exit_sys("execv"); printf("parent process is running."); wait(NULL); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* my_copy_main.c */ #include #include #include #include void exit_sys(const char* msg); int main(int argc, char** argv) { /* # Command Line Arguments # /bin/cp main.c my_copy_main.c */ /* # OUTPUT # */ printf("main is running.\n"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); if(!pid && execv(argv[1], argv + 1) == -1) exit_sys("execv"); printf("parent process is running."); wait(NULL); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >> "execlp" fonksiyonundaki "p" harfi "PATH" lafını işaret etmektedir. Bu fonksiyonun imzası, "p" siz versiyonu ile aynıdır fakat birinci parametre bazında iki fonksiyon arasında bir semantik fark vardır. Fonksiyonun imzası şu şekildedir; #include int execlp(const char *file, const char *arg0, ... /*, (char *)0 */); Fonksiyonun birinci parametresinde belirtilen argüman çalıştırılacak programın ismidir. Fakat bu isim "PATH" isimli çevre değişkeni içerisinde aranmakta, ilk bulduğuna da "exec" işlemi uygulamaktadır. "PATH" çevre değişkeni, ':' karakteri ile ayrılmış yol ifadelerinden oluşan bir yazıdır. Burada prosesin "Current Working Directory" konumuna BAKILMAMAKTADIR. Windows sistemlerinde ise "PATH" çevre değişkeni '%' karakteri ile birbirinden ayrılmıştır. Öte yandan dikkat etmemiz gereken bir diğer nokta ise şudur; birinci parametresine geçilen argümanın içerisinde '/' karakteri var ise bu fonksiyon "execl" GİBİ DAVRANACAKTIR. Yani "PATH" değişkenine bakılmaz. Buradan hareketle diyebiliriz ki "p" li versiyon "p" siz veriyonu kapsamaktadır. Fonksiyon, ilgili programın ismini "PATH" değişkeninde bulamaz ise başarısız olacaktır. "PATH" çevre değişkeni aşağıdaki formatta bulunması gerekmektedir; /opt/swift/swift-5.7.3-RELEASE-ubuntu22.04/usr/bin/:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin Tekrar hatırlatmak gerekirse iş bu "p" li versiyon, prosesin "Current Working Directory" konumuna hiç bir şekilde bakmamaktadır.Tabii geçilen argümanda hiç bir '/' karakterinin olmadığı varsayılmıştır. Ek olarak, "PATH" çevre değişkeninde prosesin o anki çalışma dizini '.' karakteri ile de belirtilebilir. Böylesi bir senaryoda artık prosesin o anki çalışma dizinine de bakacaktır. Örneğin, aşağıdaki gibi bir "PATH" çevre değişkeni; /opt/swift/swift-5.7.3-RELEASE-ubuntu22.04/usr/bin/:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/. Artık böylesi bir "PATH" çevre değişkeni olduğu için, birinci parametreye geçilen isim fonksiyonun çalışma dizininde de aranacaktır. Tabii son olarak söylemekte fayda vardır ki "PATH" çevre değişkeni dizin girişlerinden oluşmuştur. Çalıştırılmak istenen dosyanın ismi iş bu dizin girişlerinde sırasıyla aranmakta ve ilk bulduğu dosyayı çalıştırmaya çalışacaktır. * Örnek 1, #include #include #include #include void exit_sys(const char* msg); int main(int argc, char** argv) { /* # OUTPUT # main is running. total 20 -rwxr-xr-x 1 14068 14068 16248 Feb 10 00:56 a.out -rwxrwxrwx 1 root root 516 Feb 10 00:56 main.c parent process is running. */ printf("main is running.\n"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); if(!pid && execlp("ls", "ls", "-l", (char*)0) == -1) exit_sys("execv"); printf("parent process is running."); wait(NULL); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, #include #include #include #include void exit_sys(const char* msg); int main(int argc, char** argv) { /* # OUTPUT # main is running. total 20 -rwxr-xr-x 1 14068 14068 16248 Feb 10 00:56 a.out -rwxrwxrwx 1 root root 516 Feb 10 00:56 main.c parent process is running. */ printf("main is running.\n"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); /* * Artık fonksiyonumuz "execl" gibi davranacaktır. */ if(!pid && execlp("/bin/ls", "/bin/ls", "-l", (char*)0) == -1) exit_sys("execv"); printf("parent process is running."); wait(NULL); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 3, /* sample.c */ #include #include #include #include void exit_sys(const char* msg); int main(int argc, char** argv) { /* # OUTPUT # main is running. execv: No such file or directory parent process is running. */ printf("main is running.\n"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); /* * Fonksiyonun birinci parametresinde '/' karakteri kullanıldığı için "PATH" değişkenine * bakılmayacak. Fakat '.' karakteri prosesin o anki çalışma dizinini belirttiği için * "mample" isimli program bulunmaktadır. Buradan hareketle diyebiliriz ki "shell" * programının kendisi de "execlp" çağırmaktadır. */ if(!pid && execlp("./mample", "./mample", "ali", "veli", "selami", (char*)0) == -1) exit_sys("execv"); printf("parent process is running."); wait(NULL); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* mample.c */ #include #include void exit_sys(const char* msg); int main(int argc, char** argv) { /* # Command Line Arguments # ali veli selami */ /* # OUTPUT # ./a.out ali veli selamli */ for(int i = 0; i < argc; ++i) puts(argv[i]); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >> "execvp" fonksiyonu, "execlp" fonksiyonu ile aynı karakteristik özelliklere sahiptir. Sadece argüman olarak bir "vector" almaktadır. "execl" ve "execv" arasındaki fark neyse, "execlp" ile "execvp" arasındaki fark da odur diyebiliriz. * Örnek 1, #include #include #include #include void exit_sys(const char* msg); int main(int argc, char** argv) { /* # Command Line Arguments # ls -l */ /* # OUTPUT # main is running. total 20 -rwxr-xr-x 1 14097 14097 16280 Feb 10 01:19 a.out -rwxrwxrwx 1 root root 605 Feb 10 01:19 main.c parent process is running. */ printf("main is running.\n"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); if(!pid && execvp(argv[1], &argv[1]) == -1) exit_sys("execv"); printf("parent process is running."); wait(NULL); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Our Bash Program (version 5) #include #include #include #include #include #include #include #define MAX_CMD_LINE 4096 #define MAX_CMD_PARAMS 128 #define BUFFER_SIZE 4096 typedef struct tagCMD{ char* name; void (*proc)(void); } CMD; void parse_cmd_line(void); void cd_proc(void); void exit_sys(const char* msg); CMD g_cmds[] = { {"cd", cd_proc}, {NULL, NULL} }; char g_cmdline[MAX_CMD_LINE]; char* g_params[MAX_CMD_PARAMS]; int g_nparams; char g_cwd[PATH_MAX]; int main(void) { char* str; int i; pid_t pid; if(!getcwd(g_cwd, PATH_MAX)) exit_sys("getcwd"); for (;;) { fprintf(stdout, "CSD: %s> ", g_cwd); if (fgets(g_cmdline, MAX_CMD_LINE, stdin) == NULL) continue; if ((str = strchr(g_cmdline, '\n')) != NULL) *str = '\0'; parse_cmd_line(); if(g_nparams == 0) continue; if(!strcmp(g_params[0], "exit")) exit(EXIT_SUCCESS); for(i = 0; g_cmds[i].name != NULL; ++i) if(!strcmp(g_params[0], g_cmds[i].name)) { g_cmds[i].proc(); break; } if(g_cmds[i].name == NULL) { if((pid = fork()) == -1) { perror("fork"); continue; } if(!pid && execvp(g_params[0], &g_params[0]) == -1) { fprintf(stderr, "Command not found, or cannot execute!\n"); continue; } if(wait(NULL) == -1) exit_sys("wait"); } } return 0; } void parse_cmd_line(void) { char* str; g_nparams = 0; for(str = strtok(g_cmdline, " \t"); str != NULL; str = strtok(NULL, " \t")) { g_params[g_nparams++] = str; } g_params[g_nparams] = NULL; } void cd_proc(void) { char *dir; if (g_nparams > 2) { printf("too many arguments!\n"); return; } if (g_nparams == 1) { if ((dir = getenv("HOME")) == NULL) exit_sys("fatal error (getenv)"); } else dir = g_params[1]; if (chdir(dir) == -1) { printf("%s\n", strerror(errno)); return; } if (getcwd(g_cwd, PATH_MAX) == NULL) exit_sys("fatal error (getcwd)"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >> "execle" fonksiyonundaki "e" harfi o prosesin çevre değişkenlerini kastetmektedir. Anımsanacağı üzere bir prosesin çevre değişkenleri, o prosesin bellek alanında tutulmaktadır ve "fork" işlemi sırasında üst prosesten kopyalanmaktadır. Pekiyi "exec" fonksiyonları prosesin bellek alanını tamamiyle ortadan kaldırdığına göre, bir prosesin çevre değişkeninin akıbeti ne olacaktır? İşte "exec" fonksiyonlarının "e" siz versiyonları, "exec" sırasında üst prosesin çevre değişkenlerini bir yerde saklayıp, işlem bittikten sonra ilgili değerleri yeni proses için tekrardan kullanmaktadır. Bu durumda üst prosesten aktarılmış oluyor. Pekiyi "e" li versiyonlar nasıl bir davranış sergiliyorlar? Bu tip versiyonlar da çevre değişkenlerini argüman olarak almaktadırlar. Fonksiyonun prototipi aşağıdaki gibidir; #include int execle(const char *path, const char *arg0, ... /*, (char *)0, char *const envp[]*/); Fonksiyonun prototipi "execl" ile benzerdir. Sadece iş bu fonksiyon, son parametre olarak çevre değişkenlerini argüman olarak almaktadır. Son parametreye geçeceğimiz dizinin elemanları "anahtar=değer\0" biçiminde olmalıdır. Yine en sondan bir önceki argüman için "(char*)NULL" geçmeyi unutmamalıyız, eğer değişken sayıda argüman geçersek. Yine çevre değişkenlerini tuttuğumuz dizinin son elemanı NULL olmak zorundadır. Şimdi görüleceği üzere "exec" fonksiyonlarının "p" li versiyonları, çalıştırılmak istenen programı, "PATH" çevre değişkeninde belirtilen dizinlerden birisinde bulmuş olsun. Eğer bu programı çalıştıramaz ve "errno" değişkeninin değerleri de "EINVAL" YA DA "ENOEXEC" değerlerinden birisini alırsa, iş bu "exec" fonksiyonları bahsi geçen programı "shell" programına ait bir dosya olduğu kanısına varır ve bu programı "/bin" konumundaki "sh" isimli varsayılan "shell" programının kendisi ile çalıştırmaya çalışır. Ancak "exec" fonksiyonlarının diğer versiyonlar, için böyle bir aksiyon almamaktadır. Bahsi geçen bu senaryo ilerleyen paragraflarda tekrar açıklanacaktır. Son olarak belirtmekte fayda vardır ki bu tip versiyonlar eğer "PATH" değişkenini bulamadıkları zaman takınacakları tavır sistemden sisteme değişiklik gösterecektir. Öte yandan pek çok sistem, böylesi bir durumda, "PATH" çevre değişkeni "/bin:/usr/bin" biçimindeymiş gibi davranmaktadır. Pekiyi arka plandaki "execve" fonksiyonu da neyin nesidir? Şöyleki; >> "execve" fonksiyonu aslında bir sistem fonksiyonudur. Dışarıdan argüman olarak çevre değişkeni almaktadır fakat iş bu çevre değişkenlerini tuttuğumuz dizinin son elemanı NULL olmak zorundadır. Fonksiyonun prototipi şu şekildedir; #include int execve(const char *path, char *const argv[], char *const envp[]); Yine bu fonksiyona geçilecek en sonki argüman, çevre değişkenlerini içeren dizinin başlangıç adresidir. İş bu fonksiyon aslında taban fonksiyon olarak işlev görmekte olup "execl", "execlp", "execv", "execvp" ve "execle" fonksiyonları tarafından arka planda çağrılmaktadır. * Örnek 1, Aşağıdaki örnekte "e" siz versiyon kullanılmıştır: /* main.c */ #include #include #include #include void exit_sys(const char* msg); int main(int argc, char** argv) { /* # Command Line Arguments # second ali veli selamli */ /* # OUTPUT # Main program started running!... Second program started running!... Command Line Arguments: ./a.out ali veli selamli Environment Variables: HOSTNAME=Check LANGUAGE=en_US:en PWD=/home HOME=/home/runner11 LANG=en_US.UTF-8 GOROOT=/usr/local/go TERM=xterm DISPLAY=:1 SHLVL=1 PS1=#ogdb"shell"# LC_ALL=en_US.UTF-8 PATH=/opt/swift/swift-5.7.3-RELEASE-ubuntu22.04/usr/bin/:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin DEBIAN_FRONTEND=noninteractive _=/script/tinit */ printf("Main program started running!...\n"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); if(!pid && execv(argv[1], &argv[1]) == -1) exit_sys("execve"); if(waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* second.c */ #include #include #include extern char** environ; void exit_sys(const char* msg); int main(int argc, char** argv) { printf("Second program started running!...\n"); printf("\nCommand Line Arguments:\n"); for(int i = 0; i < argc; ++i) puts(argv[i]); printf("\nEnvironment Variables:\n"); for(int i = 0; environ[i] != NULL; ++i) puts(environ[i]); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte "e" li versiyon kullanılmıştır. Fakat aşağıdaki "main" isimli programı ilgili komut satırı argümanları ile çalıştırmanız gerekiyor: /* main.c */ #include #include #include #include void exit_sys(const char* msg); int main(int argc, char** argv) { /* # Command Line Arguments # second ali veli selamli */ printf("Main program started running!...\n"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); char* env[] = { "name=ahmet", "surname=pehlivanli", "address=istanbul", "PATH=/bin:/usr/bin", NULL }; if(!pid && execve(argv[1], &argv[1], env) == -1) exit_sys("execve"); if(waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* second.c */ #include #include #include extern char** environ; void exit_sys(const char* msg); int main(int argc, char** argv) { printf("Second program started running!...\n"); printf("\nCommand Line Arguments:\n"); for(int i = 0; i < argc; ++i) puts(argv[i]); printf("\nEnvironment Variables:\n"); for(int i = 0; environ[i] != NULL; ++i) puts(environ[i]); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 3, Aşağıdaki program, https://www.onlinegdb.com/ sitesindeki "/bin" dizininde bulunan "sh" isimli kabuk programını çalıştırmaktadır. #include #include #include #include void exit_sys(const char* msg); int main(int argc, char** argv) { /* # Command Line Arguments # /bin/sh */ printf("Main program started running!...\n"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); char* env[] = { "name=ahmet", "surname=pehlivanli", "address=istanbul", "PATH=/bin:/usr/bin", NULL }; if(!pid && execve(argv[1], &argv[1], env) == -1) exit_sys("execve"); if(waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 4, Aşağıdaki program, https://www.onlinegdb.com/ sitesindeki "/bin" dizininde bulunan "sh" isimli kabuk programını çalıştırmaktadır. #include #include #include #include void exit_sys(const char* msg); int main(int argc, char** argv) { /* # Command Line Arguments # /bin/sh */ printf("Main program started running!...\n"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); char* env[] = { "name=ahmet", "surname=pehlivanli", "address=istanbul", "PATH=/bin:/usr/bin", NULL }; if(!pid && execle(argv[1], "/bin/sh", (char*)0, env) == -1) exit_sys("execve"); if(waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 5, Aşağıda "execv" fonksiyonunun temsili bir implementasyonu gösterilmiştir. #include #include #include #include extern char** environ; int my_execv(const char* path, char* const* argv); void exit_sys(const char* msg); int main(int argc, char** argv) { /* # Command Line Arguments # /bin/sh */ printf("Main program started running!...\n"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); if(!pid && my_execv(argv[1], &argv[1]) == -1) exit_sys("my_execv"); if(waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); return 0; } int my_execv(const char* path, char* const* argv) { return execve(path, argv, environ); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 6, Aşağıda "execl" fonksiyonunun temsili bir implementasyonu gösterilmiştir. #include #include #include #include #include extern char** environ; #define MAX_ARG 4096 int my_execl(const char* path, const char* arg0, ...); void exit_sys(const char* msg); int main(int argc, char** argv) { /* # Command Line Arguments # /bin/sh */ printf("Main program started running!...\n"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); if(!pid && my_execl("/bin/sh", "/bin/sh", (char*)0) == -1) exit_sys("my_execl"); if(waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); return 0; } int my_execl(const char* path, const char* arg0, ...) { va_list vl; va_start(vl, arg0); char* args[MAX_ARG + 1]; char* arg; args[0] = (char*)arg0; int i; for(i = 0; (arg = va_arg(vl, char*)) != NULL && i < MAX_ARG; ++i) args[i] = arg; args[i] = NULL; va_end(vl); return execve(path, args, environ); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Yine bu fonksiyonun da "fd" parametre alan ve ismi "fexecve" olan bir versiyonu da vardır. Yani açılmış bir dosyaya ait "fd" betimleyicisini argüman olarak alır. Eğer biz çalıştırmak istediğimiz dosyayı "open" fonksiyonu ile zaten açmışsak, bu fonksiyonu kullanabiliriz. Fonksiyonun prototipi şu şekildedir; #include int fexecve(int fd, char *const argv[], char *const envp[]); Fonksiyonun birinci parametresi, çalıştırılacak dosyaya ait olan "fd" betimleyicisidir. Diğer parametreler ise "execve" ile aynıdır. Eğer bu fonksiyonu çağıracak proses, çalıştırılacak olan program üzerinde "r" ve "w" haklarına sahip değilse, çalıştırılacak olan programın "O_EXEC" bayrağı ile açılmış olması gerekmektedir. Fakat unutmamalıyız ki bu bayrak Linux sistemlerinde mevcut değildir. Öte yandan hem POSIX hem de Linux sistemlerinde çalıştırılacak dosyayı "O_RDONLY" modda açabiliriz. Son olarak Linux sistemlerinde var olan fakat standart olmayan ama "O_EXEC" manasına gelen "O_PATH" bayrağını da kullanabiliriz. Buradan da şu sonucu çıkartabiliriz ki taşınabilirlik açısından "O_RDONLY" modda açmamız gerekiyor. * Örnek 1, Aşağıdaki programı gerçek bir komut satırından çalıştırmamız gerekiyor: #include #include #include #include #include extern char** environ; void exit_sys(const char* msg); int main(int argc, char** argv) { /* # Command Line Arguments # /bin/sh */ printf("Main program started running!...\n"); int fd; if((fd = open(argv[1], O_RDONLY)) == -1) exit_sys("open"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); if(!pid && fexecve(fd, &argv[1], environ) == -1) exit_sys("my_execl"); if(waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); close(fd); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, #include #include #include #include #include extern char** environ; void exit_sys(const char* msg); int main(int argc, char** argv) { /* # Command Line Arguments # /bin/ls */ printf("Main program started running!...\n"); printf("\nCommand Line Arguments:\n"); for(int i = 0; i < argc; ++i) puts(argv[i]); printf("\nEnvironment Variables:\n"); for(int i = 0; environ[i] != NULL; ++i) puts(environ[i]); printf("Main program is still running!...\n"); int fd; if((fd = open(argv[1], O_RDONLY)) == -1) exit_sys("open"); if(fexecve(fd, &argv[1], environ) == -1) exit_sys("my_execl"); printf("Main program is about to stop!...\n"); close(fd); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 3, #include #include #include #include #include extern char** environ; void exit_sys(const char* msg); int main(int argc, char** argv) { /* # Command Line Arguments # /bin/ls */ printf("Main program started running!...\n"); int fd; if((fd = open(argv[1], O_RDONLY)) == -1) exit_sys("open"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); if(!pid && fexecve(fd, &argv[1], environ) == -1) exit_sys("my_execl"); if(waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); close(fd); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 4, #include #include #include #include #include extern char** environ; void exit_sys(const char* msg); int main(int argc, char** argv) { /* # Command Line Arguments # /bin/ls */ printf("Main program started running!...\n"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); if(!pid) { int fd; /* * Dosya açma işlemi "fork" işleminden sonra ama "exec" işleminden * önce yapıldığı için, dosyanın kapatılması "exec" fonksiyonu * sırasında otomatik olarak gerçekleşecektir. */ if((fd = open(argv[1], O_RDONLY)) == -1) exit_sys("open"); if(fexecve(fd, &argv[1], environ) == -1) exit_sys("my_execl"); // close(fd); } if(waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } > Hatırlatıcı Notlar: >> Anımsayacağımız üzere "multi-processing" sistemlerde prosesler zaman paylaşımlı olarak ve birbirinden bağımsız bir şekilde çalışmaktadır. Her proses de kendisine özgü bir adet "Process Control Block" a sahip olduğuna ve bu bloğa da ilgili prosesin "ID" değeri ile erişilmektedir. Ek olarak bu proseslerin kontrol blokları birbirine bağlı listeler ile bağlıdır. O an çalışan prosesin proses kontrol bloğu ise global bir gösterici olan "current" isimli gösterici tarafından gösterilmektedir. Fakat proseslerin "ID" değerinden o prosesin kontrol bloğuna erişmek için de bir "hash-table" kullanılmaktadır. >> O an çalışmakta olan proseslerin listesini çıkartmak için ya "/proc/" dizinine geçip "ls" komutunu çalıştırmalıyız ki bu durumda ekrana çıkanlar rakamlar o an çalışan prosesleri belirtmektedir, ya da "ps -e" kabuk komutunu çalıştırmaktır. >> Başarılı sonlanmalar "0" ile başarısızlar ise "non-zero" bir değer ile sağlanmalıdır (POSIX/Linux sistemlerinde). >> C'de programlarımızı "exit" ile sonlandırmalıyız çünkü yukarıda açıklanan bazı ön işlemleri yerine getirmektedir. Fakat bazı durumlarda direkt olarak "_exit" ile sonlandırmak da gerekebilir. * Örnek 1, #include #include #include void foo(void) { printf("foo\n"); } void bar(void) { printf("bar\n"); } int main() { /* # OUTPUT # */ atexit(foo); atexit(bar); printf("Hello World!"); /* * İş bu C fonksiyonu "abnormal" biçimde sonlandığı * için yine ekrana bir şey basılmamıştır. Ek olarak * kayıt altına aldığımız diğer fonksiyonlar da * çağrılmamıştır. */ _exit(0); return 0; } >> C standartlarına göre, "main" fonksiyonu içerisinde "return" deyimini yazmazsak, "return 0;" yazmış kabul edilmekteyiz. Bu kural sadece "main" fonksiyonuna özgüdür. >> Komut satırından "echo $?" kabuk komutunu çalıştırdığımız zaman, "shell" programının en son çalıştırdığı prosesin "exit code" bilgisini elde etmiş oluruz. >>> Eğer sadece "$?" yazıp "Enter" tuşuna basarsak, son çalıştırılan prosesin "exit code" bilgisini elle yazıp "Enter" tuşuna basmış gibi oluyor. Fakat o "exit code" değerinde bir program ismi olmadığından, "shell" programı hata verecektir. >>> "echo euhuheueueu" komutu ise ise ekrana "euhuheueueu" basacaktır. >> Paralel programlamada işi gereksiz yere "thread" lere bölersek verim açısından zarar edebiliriz. >> Bir sistem fonksiyonunun çağrılması, "syscall" isimli sistem fonksiyonu üzerinden yapılır. * Örnek 1, #include #include #include #include extern char** environ; void exit_sys(const char* msg); int main(void) { /* # OUTPUT # total 20 -rwxr-xr-x 1 14053 14053 16200 Feb 11 02:50 a.out -rwxrwxrwx 1 root root 409 Feb 11 02:50 main.c */ char* args[] = { "/bin/ls", "-l", NULL }; if(syscall(SYS_execve, "/bin/ls", args, environ) == -1) exit_sys("execve"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, #include #include #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # #include #include #include #include #include > "shell" programından "sample" programını aşağıdaki haliyle çalıştıralım; ./sample & Artık "shell" programı "sample" programını "wait/waitpid" fonksiyonları ile beklemeyecektir. Bu biçimdeki bir çalıştırma sonucunda bizlere bir numara verilmektedir. Bu numarayı "fd" komutuna geçersek, artık o proses için "wait/waitpid" yapılacaktır.