> İşletim Sistemlerinde Prosesler: Daha önceden de belirttiğimiz gibi çalışmakta olan programalara "proses (process)" denilmektedir. İşletim sistemleri bir proses yaaratıldığında prosesi izlemek için ismine "Proses Kontrol Block (Process Control Block)" bir veri yapısı oluşturmaktadır. Windows, Linux, macOS gibi koruma mekanizmasına sahip işlemlerin kullanıldığı işletim sistemlerinde prosesler genel olarak birbirinden izole edilmiştir. Yani biz bir programı birden fazla kez çalıştırdığımızda oluşturulan prosesler birbirinden bağımsızdır. Bundan dolayıdır ki işletim sistemlerinde pek çok özellik, prosese özgdür. Dolaysıyla her prosesin ayrı bir "çalışma dizini (current working directory)"si vardır. Her prosesin açtığı dosyalar diğerinden farklıdır. Yetkiler yine prose özgüdür. Her prosesin bellek alanı biribirinden ayrılmıştır. Örneğin sistemlerin çoğunda "heap" alanı da prosese özgüdür. Eskiden proseslerin tek bir akışı oluyordu. Ancak 90'lı yıllarla birlikte "thread" kavramı işletim sistemlerine sokulmuştur. Bugünkü modern işlem sistemleri "multithread" çalışmaya izin vermektedir. Thread'ler proseslerin bağımsız çizelgelenen akışlarıdır. Örneğin programın bir akışı foo fonksiyonunda ilerlerken diğer bir akışı bar fonksiyonunda ilerleyebilir. İşte böyle prosesin bağımsız akışlarına "thread" denilmektedir. Bir proses tek bir akışka (yani thread ile) çalışmaya başlar. Prosesin diğer thread'lerini programcı kendisi yaratmaktadır. Bugün kullandığımız kapasiteli modern işletim sistemleri ise thread temelinde "zaman paylaşımlı (time sharing)" bir çalışma mekanizması oluşturmaktadır. Bu mekanizmada proseslerin thread'leri bir kuyruk sisteminde toplanır. Buna "çalışma kuyruğu (run queue)" denilmektedir. Çalışma kuyruğundan sıradaki thread alınır, CPU'ya atanır. Belli bir süre onun CPU'da çalışmasına izin verilir. O süre dolduğunda thread CPU'dan alınarak sıradaki diğer thread CPU'ya atanır. O da belli bir süre çalıştırılır. CPU'ya atanan bir thread'in quanta süresini bitirdiğinde CPU'dan kopartılması donanım kesmeleri yoluyla zorla yapılmaktadır. Bu tür sistemlere "preemtive" sistemler denilmektedir. Bazı sistemlerde thread CPU atandığında zorla koparma işlemi yapılmaz. Thread CPU'yu kendisi kendi isteğiyle bırakır. Aslında bu tür sistemler eskiden genellikle thread'siz sistemler olarak karşımıza çıkıyordu. Thread'siz sistem tek thread'li sistem gibi düşünülebilir. Bu tür işletim sistem sistemlerine "non-preemptive" sistemler ya da "cooperative multitask" sistemler denilmektedir. Örneğin Windows 3.X işletim sistemleri, PalmOS işletim sistemleri böyle sistemlerdi. Ancak günümüzde sistemler hep preemtive biçimdedir. Öte yandan bir thread'in parçalı çalışma süresine "quanta süresi (time quantum)" denilmektedir. Quanta süreleri işlemcinin hızı da göz önünde bulundurularak işletim sistemleri tarafından belirlenmektedir. Quanta süresi uzun tutulursa interaktivite azalır, kısa turulursa birim zamanda yapılan iş miktarı (througput) düşer. Kullanıcı programının sürekli çalıştığı gibi bir illüzyon yaşamaktadır. Aslında kullanıcının programı kesikli kesikli çalıştırılmaktadır. Bir thread'in CPU'ya atanıp belli bir süre çalıştırılması ve çalışma bittikten sonra diğer bir thread'in CPU'ya atanması sürecine "bağlamsal geçiş (context switch)" denilmektedir. Tabii context switch işlemi de belli bir zman almaktadır. İşletim sistemelrinin bu işlemlerle uğraşan alt sistemlerine "çizelgeleyici (scheduler)" denilmektedir. Artık günümüzde bilgisayarlarımızda birden fazla işlemci ya da çekşrdek vardır. Ancak zaman paylaşımlı çalışma modelinde bir farklılık oluşmamaktadır. Nasıl bir işletmeden tek bir yemel servis noktası yerine birden fazla servis noktası olduğunda temel kuyruk mekanizmasında bir farklılık oluşmuyorsa aynı durum çok işlemcili ya da çekirdekli sistemlerde de benzer biçimdedir. Tipik olarak işletim sistemleri her CPU ya da çekirdek için ayrı çalışma kuyrukları oluşturmaktadır. Tabii işletim sistemi bu CPU ya da çekirdeklerdeki iş yükünü de dengelemeye çalışır. (Yani örneğin bir CPU ya da çekirdeğin açlışma kuyruğunda az sayıda thread kalmışsa başka kuyruktaki thread'leri buraya taşıyabilmektedir.) İşin özünde prosesler konusunda en temel işlem bir prosesin yaratılmasıdır. Bir prosesin yaratılması demekle aslında dolaylı olarak bir programın çalıştırılması kastedilmektedir. Biz programları işletim sisteminin sunduğu kabuk ortamından faydalanarak çalıştırmaktayız (örneğin Windows'ta bir dosyaya çift tıklayarak, Linux'ta kabukta programın ismini yazarak.) Aslında proseslerin yaratılması işletim sisteminin sistem fonksiyonlarıyla yapılmaktadır. Dolayısıyla kabuk programları da aslında bu sistem fonksiyonlarını kullanmaktadır. Proses yaratmak için Windows'ta sistem fonksiyonlarını çağıran API fonksiyonları UNIX/Linux macOS sistemlerinde de POSIX fonksiyonları bulunmaktadır. Biz bu bölümde önce Windows sistemlerinde sonra UNIX/Linux ve macOS sistemlerinde proses yaratımı üzerinde duracağız. >> Windows Sistemleri: Windows sistemlerinde proses yaratmak için (yani bir programı çalıştırabilmek için) CreateProcess isimli API fonksiyonu kullanılmaktadır. >>> "CreateProcess" : Fonksiyonun prototipi şöyledir: BOOL CreateProcess( LPCTSTR lpApplicationName, LPSTR lpCommandLine, LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, LPCSTR lpCurrentDirectory, LPSTARTUPINFO lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation ); Fonskiyonun, >>>> Birinci parametresi çalıştırılacak olan programın yol ifadesini almaktadır. Bu yol ifadesi mutlak ya da göreli olabilir. Fonksiyonun ikinci parametresi programın komt satırı argümanlarını belirtmektedir. Bu komut satırı argümanları tek bir yazı biçiminde oluşturulur. Ancak bu parametrenin const olamayan bir gösterici olduğuna dikkat ediniz. Fonksiyon verilen adreste değişiklik yapıp sonra onu eski haline getirmektedir. Bu nedenle ikinci parametrenin bir string olarak girilmemesi gerekir. (C'de stringlerin karakterlerinin değiştirilmesi tanımsız davranışa yol açmaktadır.) Komut satırı argümanları fonksiyon tarafından boşluk karakterlerinden ayrıştırılıp gösterici dizisine yerleştirilmekte ve main fonksiyonun argv parametresi bu biçimde oluşturulmaktadır. Tabii ilk komut satırı argümanının programın ismi olması zorunlu olmasa da genel bir kabuldür. Örneğin: char szCmdLine[] = "C:\\windows\\notepad.exe test.txt"; ... CreateProcess("C:\\windows\\notepad.exe", szCmdLine, ...); Burada "C:\windows\notepad.exe" programı çalıştırılmak istenmiştir. Komut satırı argümanları boşluk parse edildiğinde iki tane olacaktır: C:\\windows\\notepad.exe test.txt Program için uzantı verilmezse programın default ".exe" uzantılı olduğu kabul edilmektedir. Fonksiyonun birinci parametresi NULL adres geçilebilmektedir. Bu durumda çalıştırılacak program ikicni parametredeki boşluksuz ilk string olarak belirlenmektedir. Örneğin: char szCmdLine[] = "C:\\windows\\notepad.exe test.txt"; ... CreateProcess(NULL, szCmdLine, ...); Burada birinci parametre NULL adres geçilmiştir. Bu durumda fonksiyon ikinci parametrededki ilk string'i çalıştırılacak program olarak ele alır. İkinci parametrenin tamamı yine aynı zamanda komut satırı argümanı olmaktadır. Genellikle programcılar bu birinciyi parametreyi NULL geçip çalıştırılacak programı ve komut satırı argümanlarını ikinci parametreye girerler. Ancak bu durumda çalıştırılacak program boşluklardan parse edildiği için dikkat etmek gerekir. Windows eğer iki tırnaklanmamışsa boşluklardan sırasıyla keserek aramayı yapar. Örneğin ikinci parametre şöyle girilmiş olsun: "c:\\program files\\sub dir\\program name" Burada sistem sırasıyla şu aramaları yapar ve ilk bulduğu dosyayı çalıştırmaya çalışır: c:\program.exe c:\program files\sub.exe c:\program files\sub dir\program.exe c:\program files\sub dir\program name.exe Bu tür durumlarda boşluk içeren kısım iki tırnak içerisine alımabilir. Örneğin: "\"c:\\my folder\\prog.exe\" ali veli selami" >>>> İkinci parametresinde önemli bir özellik vardır. (Ancak bu özellik birinci parametresinde yoktur.) Eğer CreateProcess fonksiyonun birinci NULL geçilip ikinci parametresindeki ilk boşluksuz yazı ile belirtilen program isminde hiçbir ters bölü karakteri kullanılmamışsa bu durum özel bir anlama gelmektedir. Bu durumda söz konusu "çalıtırılabilir (executable)" dosyanın aranması sırasıyla şu biçimde yapılmaktadır: -> CreateProcess uygulayan prosesin ".exe" dosyasının bulunduğu dizin -> CreateProcess uygulayan prosesin çalışma dizini (current working directory) -> 32 Bit Windows Sistem Dizini ("Windows" dizininin içerisindeki "System32" dizini) -> 16 bit Windows Sistem Dizini ("Windows" dizininin içerisindeki "System" dizini) -> Windows dizininin kendisi -> CreateProcess uygulayan prosesin PATH çevre değişkininde belirtlen dizinler sırasıyla Tabii bu sırada arama yapılırken eğer söz konusu dosya bulunursa aramaya deavm edilmemektedir. Burada dikkat edilmesi gereken önemli nokta şudur. Yukarıdaki arama davranışı yalnızca "CreateProcess fonksiyonun birinci paraöetresi NULL geçilip, ikinci paramtresindeki ilk boşluksuz yazının hiçbir ters bölü karakteri içermemesi durumunda" gösterilmektedir. Yukarıdaki arama listesinin sonundaki PATH çevre değişkenine dikkat ediniz. Eğer CreateProcess söz konusu dosyayı diğer dizinlerde bulamazsa PATH çevre değişkeninin değerinde belirtilen dizinlerde de aramaktadır. PATH çevre değişkeninin değeri Windows sistemlerinde ";" karakteriyle ayrılan alanlardan oluşnmaktadır. Her ";" arası ayrı bir dizini belirtmektedir. Örneğin: C:\Program Files (x86)\VMware\VMware Player\bin\;C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.4\bin; C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.4\libnvvp;C:\Program Files\Java\jdk-11.0.2\bin; C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem;C:\WINDOWS\System32\WindowsPowerShell\v1.0\; ... O halde CreateProcess son maddedeki aramaya yapmak için PATH çevre değişkeninin değerini elde etmekte ve buradaki yazıyı ";" karakterlerinden ayrıştırmaktadır. Tabii eğer söz konusu çalıştırılabilir dosya bu PATH çevre değişkeni ile belirtilen birden fazla dizinde varsa ilk bulunan dizindeki program çalıştırılmaktadır. Windows'un komut satırı uygulaması olan "cmd.exe" programında biz komut satırında bir şeyler yazıp ENTER tuşuna bastığımızda CreateProcess bu "cmd.exe" tarafından uygulanmaktadır. Dolayısıyla eğer komutun ilk boşluksuz kısmında hiçbir "\" karakteri yoksa dosya nihayetinde PATH çevre değişkeni ile belirtilen dizinlerde de aranacaktır. Tersten gidersek "eğer biz komut satırında bir programın ismini yazdığımızda onun çalışmasını istiyorsak onun bulunduğu dizini PATH çevre değişkenin değerinde belirtmelitiz." >>>> Üçüncü ve dördüncü parametreleri (lpProcessAttributes ve lpThreadAttributes) kernel tarafından yaratılacak olan proses ve thread nesnelerinin güvenlik bilgilerini (yetki derecelerini) belirtmektedir. Ancak Windows sistemlerinde proseslerin yetki dereceleri UNIX/Linux sistemlerindeki kadar basit değildir. Biz bu kursta bu konu üzerinde durmayacağız. Bu konu "Windows Sistem Programlama" kurslarında ele alınmaktadır. Buradaki SECURITY_ATTRIBUTES bir yapı belirtmektedir. Bu iki parametreye default olarak NULL geçilebilmektedir. >>>> Beşinci parametresi (bInheritHandles) kernel nesnelerinin yaratılmakta olan alt prosese aktarılıp aktarılmayacağnı belirtmektedir. Bu bir ana şalter görevindedir. Bu parametreye sıfır dışı bir değer (örneğin TRUE değeri) geçirilirse aktarım yapılmaktadır. sıfır değeri (FALSE) geçirilirse aktarım yapılmamaktadır. Aslında her kernel nesnesi yaratılırken LPSECURITY_ATTRIBUTES parametresiyle o kernel nesnenin bireysel olarak alt proseslere aktarılabilirliği belirtilmektedir. Ancak buradaki parametre ana şalter görevindedir. Yani ilgili kernel nesnesi yaratılırken "alt prosese aktarılsın" demiş olsak bile bu parametre FALSE geçildikten sonra aktarım yapılmamaktadır. Programcı "özel bir gerekçesi yoksa" bu parametreye TRUE geçebilir. >>>> Altıncı parametresi (dwCreationFlags) yaratılacak prosesin bazı özelliklerini belirlemek için kullanılmaktadır. Bu parametreye bazı sembolik sabitler bit düzeyinde OR işlemine sokularak girilebilir. Kursumuzda buradaki bayraklar üzerinde durmayacağız. Ancak thread'ler konusunda buraya atıfta bulunacağız. Bu nedenle bu parametreyi 0 olarak geçebiliriz. >>>> Yedinci parametresi (lpEnvironment) yaratılacak alt prosin çevre değişken listesini belirtmektedir. Buraya aşağıdaki gibi bir bloğun adresi geçirilmelidir: değişken=değer\0değişken=değer\0....değişken=değer\0\0 Eğer bu parametreye NULL adres geçilirse alt prosesin çevre değişken listesi üzet prosesten alınmaktadır. Genellikle bu parametreye NULL adres geçilmektedir. >>>> Sekizinci parametresi (lpCurrentDirectory) oluşturulacak olan alt prosesin çalışma dizinini belirtmektedir. Yani üst proses isterse alt proses yaratıldığında onun çalışma dizininin ne olması gerektiğini belirleyebilmektedir. Bu parametre de NULL adres geçilebilir. Bu durumda alt prosesin çalışma dizini üst prosesle aynı olur. Genellikle bu parametre NULL adres biçiminde geçilmektedir. >>>> Dokuzuncu parametresi STARTUPINFO isimli bir yapı nesnesinin adresini almaktadır. Programcı bu yapı nesnesinin içini doldurmalı ve adresini fonksiyona geçirmelidir. Bu oldukça fazla elemana sahiptir. Yaratılacak prosesin bazı ikincil özelliklerinin belirlenmesi amacıyla kullanılmaktadır. Bu yapıdaki 0 elemanları default değer anlamına gelir. Dolayısıyla programcı yapı elemanlarını sıfırlayarak default değerler geçebilir. Bu parametreye NULL adres girilememektedir. Burada küçük ayrıntı vardır. STARTUPINFO yapısının ilk elemanı olan cb elemanına yapının sizeof değeri geçirilmelidir. Bunun nedeni ileriye doğru uyumun korunmak istenmesidir. Bu tür yapılara ileride elemanlar eklenebilmektedir. Bu durumda yapının hangi versiyonunun kullanıldığının anlaşılabilmesi için bu elemandan faydalanılmaktadır. Bu yapı nesnesinin elemanlarının sıfırlanması şöyle yapılabilir: STARTUPINFO si = {sizeof(STARTUPINFO)}; >>>> Onuncu parametresi (lpProcessInformation) PROCESS_INFORMATION isimli bir yapı nesnesinin adresini almaktadır. Bu yapı nesnesini programcı doldurmaz. Bu yapı nesnesi fonksiyon tarafından doldurulmalıdır. Bu yapı aşağıdaki gibi bildirilmiştir. typedef struct _PROCESS_INFORMATION { HANDLE hProcess; HANDLE hThread; DWORD dwProcessId; DWORD dwThreadId; } PROCESS_INFORMATION, *PPROCESS_INFORMATION, *LPPROCESS_INFORMATION; Bir proses yaratıldığında iki kernel nesnesi de yaratılmaktadır. >>>> Windows işletim sisteminde "kernel nesnesi (kernel object)" denildiğinde kendi HANDLE değeri olan, kernel tarafından izlenen, bir grup nesne anlaşılmaktadır. Bu terim UNIX/Linux sistemlerinde kullanılmamaktadır. Örneğin CreateFile fonksiyonu ile açmış olduğumuz dosyalar da "kernel nesnesi" grubundadır. Tüm kernel nesneleri Windows sistemlerinde ortak bazı özelliklere sahiptir. Örneğin hepsi CloseHandle API fonksiyonuyla boşaltılmaktadır. Örneğin tüm kernel nesnelerinin bir güvenlik (yetki) bilgisi vardır. Bu güvenlik bilgisi o kernel nesnesinin yaratılması sırasında CreateXXX fonksiyonlarının LPSECURITY_ATTRIBUTES parametresiyle temsil edilmektedir. Bu iki kerner nesnesinden biri proses için diğeri ise ana thread içindir. Bu kernel nesnelerinin handle değerleri ve id değerleri vardır. İşte bu fonksiyon bu değerleri yerleştirmektedir. Bu parametreye de NULL adres geçilememektedir. Fonksiyon başarı durumunda sıfır dışı bir değere başarısızlık durumunda sıfır değerine geri dönmektedir. Fonksiyonun örnek bir kullanımı şöyle olabilir: char szPath[] = "notepad.exe"; STARTUPINFO si = {sizeof(STARTUPINFO)}; PROCESS_INFORMATION pi; ... if (!CreateProcess(NULL, szPath, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi)) ExitSys("CreateProcess"); Aşağıdaki örnekte "prog1" ve "prog2" biçiminde iki program bulunmaktadır. prog1 programı CreateProcess uygulayarak prog2 programını çalıştırmaktadır. Bu çalıştırmada birinci NULL geçildiği için ve ikinci parametrede programın ismi "\" karakteri olmadan belirtildiği için yukarıda açıklamış olduğumuz arama işlemleri belirttiğimiz dizinlerde en sonunda da PATH çevre değişkeniin belirtildiği dizinde uygulanacaktır. Bu örnekte "prog2" programının aranan dizinlerin birinde olması gerekmektedir. * Örnek 1, /* Prog1.c */ #include #include #include void ExitSys(LPCSTR lpszMsg); int main(void) { char szPath[] = "prog2.exe prog1.c"; char env[] = "ALI=10\0Veli=20\0"; STARTUPINFO si = {sizeof(STARTUPINFO)}; PROCESS_INFORMATION pi; if (!CreateProcess(NULL, szPath, NULL, NULL, TRUE, 0, env, "c:\\windows", &si, &pi)) ExitSys("CreateProcess"); Sleep(1000); printf("Ok\n"); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastError = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } /* Prog2.c */ #include #include #include void ExitSys(LPCSTR lpszMsg); int main(int argc, char *argv[]) { LPCH envStr; char cwd[MAX_PATH]; printf("Prog2 command line arguments:\n"); printf("--------------------------\n"); for (int i = 0; i < argc; ++i) puts(argv[i]); printf("--------------------------\n"); printf("Prog2 environment variables:\n"); if ((envStr = GetEnvironmentStrings()) == NULL) { fprintf(stderr, "Cannot get environment strings!..\n"); exit(EXIT_FAILURE); } while (*envStr != '\0') { puts(envStr); envStr += strlen(envStr) + 1; } printf("--------------------------\n"); printf("Prog2 current workşng directory:\n"); if (!GetCurrentDirectory(MAX_PATH, cwd)) ExitSys("GetCurrentDirectory"); printf("%s\n", cwd); printf("--------------------------\n"); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastError = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte chrome programı komut satırı argümanları verilerek çalıştırılmıştır. Ancak "chrome" programı sizin bilgisayarınızda farklı bir dizine yüklenmiş olabilir. Sizin chrome programının kendi bilgisayarınızdaki yol ifadesini vermeniz gerekir. #include #include #include void ExitSys(LPCSTR lpszMsg); int main(void) { char szPath[] = "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe", csystem.org cumhuriyet.com\""; STARTUPINFO si = { sizeof(STARTUPINFO) }; PROCESS_INFORMATION pa; if (!CreateProcess(NULL, szPath, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pa)) ExitSys("CreateProcess"); printf("Ok\n"); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastError = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } * Örnek 3, Yukarıda da belirttiğimiz gibi Windows sistemlerinde HANDLE türüyle belirtilen tüm nesnelere (dosyalara, thread'lere, proseslere vs.) "kernel nesneleri" denilmektedir. Tüm kernel nesneleri prosese özgü "proses handle tablosu" denilen bir tabloda giriş belirtmektedir. Tüm kernel nesnelerinin yok edilmesi (ya da kapatılması) CloseHandle isimli API fonksiyonu ile yapılmaktadır. Bir prosesi yarattıktan sonra PROCESS_INFORMATION yapısı ile elde ettiğiniz proses ve thread handle alanlarını CloseHandle fonksiyonuyla kapatabilirsiniz. Bunları kapatıyor olmanız bu prosesin sonlandırılacağı anlamına geşmemektedir. Zaten program sonlandığında bütün kernel nesneleri kapatılmaktadır. #include #include #include void ExitSys(LPCSTR lpszMsg); int main(void) { char szPath[] = "notepad.exe"; STARTUPINFO si = {sizeof(STARTUPINFO)}; PROCESS_INFORMATION pi; if (!CreateProcess(NULL, szPath, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi)) ExitSys("CreateProcess"); printf("Ok\n"); CloseHandle(pi.hThread); CloseHandle(pi.hProcess); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastError = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } Aslında CreateProcess API fonksiyonu bir kernel fonksiyonudur. Yani en alt seviyedki proses yaratan fonksiyondur. Windows'ta kernel API fonksiyonlarının yanı sıra ismine "Shell API fonksiyonları" denilen bir grup API fonksiyonu da bulunmaktadır. Shell API fonksiyonarı daha yüksek seviyeli fonksiyonlardır. Bunlar işlemlerini yaparken kernel API fonksiyonlarını çağırmaktadır. Örneğin bir programı çalıştırmak için en aşağı seviyeli API fonksiyonu CreateProcess fonksiyonudur. Ancak ShellExecute isimli bir shell API fonksiyonu da bulunmaktadır. ShallExecute fonksiyonu yüksek seviyeli bir fonksiyondur ve nihai olarak CreateProcess fonksiyonunu çağırmaktadır. Burada biz ShellExecute fonksiyonu üzerinde de duracağız. >>>> "ShellExecute" : ShellExecute kabuk fonksiyonunun en önemli özelliği "dosya ilişkilendirmesini" dikkate almasıdır. Yani biz bu fonksiyon ile örneğin bir ".docx" dosyasını çalıştırmak istersek ShellExecute bu uzantının ilişkin olduğu programı tespit eder ve CreateProcess fonksiyonu ile o programı ("word.exe" programını) çalıştırır. Bu ".docx" dosyasını da bu programa ("word.exe" programına) komut satıı argümanı yapar. Hangi uzantılı dosyaların hangi programla ilişkilendirildiği "registry" denilen dosyalarda tutulmaktadır. Bu registry dosyalarında manuel işlem yapabilmek için "regedit" isimli bir program da bulundurulmaktadır. Biz masaüstünde bir dosyaya çift tıkladığımız zaman ya da komut satırında bir dosyanın ismini yazıp ENTER tuşuna bastığımız zaman aslında ShellExecute fonksiyonu ile çalıştırma işlemi yapılmaktadır. Dosya ilişkilendirmelerine bakan ShellExecute fonksiyondur. Ancak proses aslında CreateProcess fonksiyonu tarafından yaratılmaktadır. ShellExecute ---> Dosya ilişkilendirmesine bak ----> CreateProcess ShellExecute API fonksiyonunun parametrik yapısı şöyledir: HINSTANCE ShellExecuteA( HWND hwnd, LPCSTR lpOperation, LPCSTR lpFile, LPCSTR lpParameters, LPCSTR lpDirectory, INT nShowCmd ); Fonksiyonun, >>>>> Birinci parametresi (hwnd) UI mesajlarının yazdırılacağı pencerenin üst penceresini belirtmektedir. Biz bu konu üzerinde durmayacağız. Ancak bu parametreye NULL adres geçilebilir. >>>>> İkinci parametresi yapılacak işleme ilişkin bir komut yazısını almaktadır. omut belirten bu yazılar şunlar olabilir: "edit" "explore" "find" "open" "print" "runas" Program çalıştırmak için bu komut yazsının "open" biçiminde girilmesi gerekmektedir. >>>>> Üçüncü parametresi (lpFile) işlem uygulanacak dosyanın yol ifadesini belirtmektedir. Bu yol ifadesi mutlak ya da göreli olabilir. Burada belirtilen dosya çalıştırılabilir bir dosya olabileceği gibi bir doküman dosyası da (doküman dosyası demekle çalıştırılabilir olmayan dosyalar kastedişmektedir) olabilir. Yukarıda belirttiğimiz gibi bu durumda ShellExecute aslında bu doküman dosyasının ilişkin olduğu program dosyasını çalıştırıp bu doküman dosyasını komut satırı argümanı yapmaktadır. Buradaki dosya ismi çalıştırılabilir bir dosya ise ve bu dosya ismi hiç "\" içermiyorsa yine CreateProcess fonksiyonunda açıkladığımız dizinlere ve PATH çevre dğeişkenine bakılmaktadır. >>>>> Dördüncü parametresi (lpParameters) üçüncü parametre çalıştırılabilir bir dosya ise ona aktarılacak komut satırı argümanlarını belirtmektedir. Doküman dosyalarında bu parametre NULL geçilebilir. >>>>> Beşinci parametre (lpDirectory) çalıştırılacak programın çalışma dizinini belirtmektedir. Bu parametre NULL geçilebilir. Bu durumda ShellExecute fonksiyonunu uygulayan prosesin çalışma dizini kullanılır. >>>>> Fonksiyonun son parametresi (nCmdShow) GUI penceresinin hangi boyutta açılacağını belirtmektedir. Bu parametre SW_NORMAL olarak geçilebilir. Bu durumda programa ilişkin GUI penceresi normal boyutla (restore boyutunda) açılmaktadır. ShellExecute fonksiyonu başarı durumunda >= 32 olan bir değere geri dönmektedir. Ancak fonksiyonun geri dönüş değeri 16 bit Windows uyumunu korumak için HINSTANCE olarak alınmıştır. HINSTACE void bir adres biiminde typedef edilmiştir. Dolayısıyla karşılaştırma öncesinde bu değerin adres ile aynı uzunlukta bulunan bir tamsayı türüne dönüştürülmesi gerekir. Bu tür içerisinde INT_PTR olarak typedef edilmiştir. Aşağıda ShellExecute fonksiyonunun kullanımına bir örnek verilmiştir. * Örnek 1, #include #include #include #include void ExitSys(LPCSTR lpszMsg); int main(void) { HINSTANCE hResult; hResult = ShellExecute(NULL, "open", "test.txt", NULL, NULL, SW_NORMAL); if ((INT_PTR)hResult < 32) ExitSys("ShellExecute"); printf("Ok\n"); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastError = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } * Örnek 2, ShellExecute fonksiyonunun ikinci parametresinde "explore" komutu kullanılırsa "windows explorer" açılmaktadır. Bu durumda üçüncü parametrenin bir dizin belirtmesi gerekmektedir. Aşağıdaki örnekte "windows dizini" windows explorer ile programlama yoluyla açılmıştır. #include #include #include #include void ExitSys(LPCSTR lpszMsg); int main(void) { HINSTANCE hResult; hResult = ShellExecute(NULL, "explore", "c:\\windows", NULL, NULL, SW_NORMAL); if ((INT_PTR)hResult < 32) ExitSys("ShellExecute"); printf("Ok\n"); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastError = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } Pekiyi Windows'ta kendi uzantılı dosyalarımızı kendi programlarımızla nasıl ilişkilendirebiliriz? Bunun için öncelikle bizim programımızı komut satırı argümanı alacak biçimde organize etmemiz gerekir. Yani pogramımızda en azsından "argc > 1" koşulu sağlanmaıdır. Çünkü ShellExecute ilgili dokğman dosyasını bizim programımıza komut satırı argümanı olarak "(argv[1])" geçirecektir. Tabii bizim dosya ilişkilendirmesini de yapmamız gerekir. Dosya ilişkilendirmesi yukarıda da belirttiğimiz Windows'un "registry" kayıt dosyalarına yazılmalıdır. Ancak bu işlem manuel olarak da yapılabilmektedir. Manuel ilişkilendirme ve silme Windows versiyonları arasında farklılıklar gösterebilmektedir. Programlama yoluyla dosya ilişkilendirmesi için hazır shell API fonksiyonu yoktur. Windows registry dosyalarına erişim için ismine "Registry API'leri" denilen API fonksiyonları bulundurmaktadır. Registry API fonksiyonlarının hepsi RegXXX biçiminde isimlendirilmiştir. Registry işlemlerini yapabilmek için hangi ayarın registry içerisinde nerede tutulduğunun bilinmesi gerekmektedir. Bu bilgiye Microsoft dokümanlarından erişilebilir. Öte yandan Windows sistemlerinde her prosesin sistem genelinde tek (unique) olan bir "proses id" değeri vardır. Proses id değerleri DWORD türü ile temsil edilen tamsayı değerlerdir. Ancak bir prosesle ilgili işlem yapabilmek için onun handle değerinin elde edilmesi gerekmektedir. Anımsanacağı gibi CreateProcess API fonksiyonunda biz bir proses yarattığımız zaman onun id ve handle değerlerini fonksiyonun PROCESS_INFORMATION parametresi yoluyla elde edebiliyorduk. Windows sistemlerinde proses id değerleri sistem genelinde tek olduğu halde handle değerleri tek değildir. Biz bir proses nesnesini her açtığımızda değişik bir handle değeri elde edebiliriz. Proseslerle ilgili sık gereksinim duyulan bir işlem de proses listesinin elde edilmesidir. Bu işlem Windows sistemlerinde bir grup API fonksiyonuyla yapılmaktadır. Bu fonksiyonlar, >>> "EnumProcesses" : EnumProcesses isimli API fonksiyonu sistemdeki tüm proseslerin (bazı ayrtıntılar vardır) ID değerlerini elde etmek için kullanılmaktadır. EnumProcesses fonksiyonunun prototipi şöyledir: #include BOOL EnumProcesses( DWORD *lpidProcess, DWORD cb, LPDWORD lpcbNeeded ); Fonksiyonun, >>>> Birinci parametresi prosslerin id değerlerinin yerleştirileceği DWORD dizinin adresini almaktadır. >>>> İkinci parametresi, birinci parametresindeki dizinin byte uzunluğunu (eleman uzunluğunu değil) belirtmektedir. >>>> Üçüncü parametre ise diziye yerleştirilen byte sayısının (eleman sayısının değil) yerleştirileceği DOWORD nesnenin adresini almaktadır. Fonksiyon başarı durumunda 0 dışı değerine, başarısızlık durumunda 0 değerine geri dönmektedir. * Örnek 1, #include #include #include #include #define NPROC_IDS 4096 void ExitSys(LPCSTR lpszMsg); int main(void) { DWORD dwProcessIds[NPROC_IDS]; DWORD dwNeeded; DWORD i; if (!EnumProcesses(dwProcessIds, sizeof(dwProcessIds), &dwNeeded)) ExitSys("EnumProcesses"); for (i = 0; i < dwNeeded / sizeof(DWORD); ++i) printf("%lu\n", (unsigned long)dwProcessIds[i]); printf("%lu process Ids listed...\n", dwNeeded / sizeof(DWORD)); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastError = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } >>> "OpenProcess" : Sistemdeki prosesler hakkında bilgi elde edebilmek için id değeri yetmemektedir. Bunun için proseslere ilişkin handle değerlerine gereksinim vardır. İşte prosesin id değerinden handle değerinin elde edilmesi için OpenProcess isimli API fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: HANDLE OpenProcess( DWORD dwDesiredAccess, BOOL bInheritHandle, DWORD dwProcessId ); Fonksiyonun, >>>> Birinci parametresi açış işleminin ne amaçla yapılacağnı belirten bayrak değerlerinin bit OR işlemine sokulmasıyla oluşturulmaktadır. Buradaki bayrak değerleri PROCESS_XXXX biçiminde isimlendirilmiş sembolik sabitlerden oluşmaktadır. Proses bilgisini almak için bu parametreye en azından "PROCESS_QUERY_INFORMATION|PROCESS_VM_READ" bayraklarının girilmesi gerekmektedir. >>>> İkinci parametresi bu handle değerinin alt prosese aktarılabilirliğini belirtmektedir. Bu parametre FALSE olarak geçilebilir. >>>> Üçünü parametre ilgili prosesin id değerini belirtmektedir. Fonksiyon başarı durumunda proses nesnesinin handle değerine, başarısızlık durumunda NULL adrese geri dönmektedir. Windows sistemlerinde bir proses nesnenin açılabilmesi için ilgili prosesin bazı yetkilere sahip olması gerekmektedir. Dolayısıyla biz bu fonksiyonla id değerini bildiğimiz her prosesi açamayız. >>> "EnumProcessModules" : Windows sistemlerinde bağımsız olarak yüklenme bilgilerine sahip olan "exe" ve "dll"" dosyalarına "modül (module)" denilmektedir. Bir uygulama bir "exe dosya" ve birtakım "dll dosyalarından" oluşmaktadır. Uygulamayı oluşturan modüllerin elde edilmesi EnumProcessModules fonksiyonuyla yapılmaktadır. Fonksiyonun prototipi şöyledir: BOOL EnumProcessModules( HANDLE hProcess, HMODULE *lphModule, DWORD cb, LPDWORD lpcbNeeded ); Fonksiyonun, >>>> Birinci parametresi prosesin handle değerini belirtmektedir. >>>> İkinci parametre proses modüllerinin handle değerlerinin yerleştirileceği HMODULE türünden dizinin adresini almaktadır. HMODULE proses modüllerini temsil eden handle değeridir. >>>> Üçüncü parametresi bu dizinin byte uzunluğunu (eleman uzunluğunu değil) belirtmektedir. >>>> Son parametresi diziye yerleştirilen byte sayısınının (eleman sayısının değil) yerleştirileceği DOWORD nesnenin adresini almaktadır. Fonksiyon başarı durumunda sıfır dışı bir değere, başarısızlık durumunda sıfır değerine geri dönmektedir. Her zaman prosesin ilk modülü "exe dosyaya ilişkin" modüldür. >>> "GetModuleBaseName" : Nihayet modülün handle değerindne hareketle GetModuleBaseName API fonksiyonu ile modülün ismi elde edilebilmektedir. Fonksiyonun prototipi şöyledir: DWORD GetModuleBaseName( HANDLE hProcess, HMODULE hModule, LPSTR lpBaseName, DWORD nSize ); Fonksiyonun, >>>> İlk iki parametresi sırasıyla ilgili prosesin ve modülün handle değerlerini belirtmektedir. >>>> Üçüncü parametre modül isminin yerleştirileceği dizinin adresini alır. >>>> Son parametresi ismin yerleştirileceği dizinin uzunluğunu belirtmektedir. Fonksiyon başarı durumunda diziye yerleştirilen karakter sayısına başarısızlık durumunda 0 değerine geri dönmektedir. Aşağıdaki örnekte prosesimizin yetki bakımdan elde edebileceği proses bilgileri yazdırılmıştır. * Örnek 1, #include #include #include #include #define NPROCESSES 4096 #define NMODULES 4096 void ExitSys(LPCSTR lpszMsg); int main(void) { DWORD dwProcessIds[NPROCESSES]; DWORD cbNeeded; HANDLE hProcess; HMODULE hModules[NMODULES]; DWORD dwProcessCount; DWORD dwModuleCount; char szModuleName[MAX_PATH]; if (!EnumProcesses(dwProcessIds, sizeof(dwProcessIds), &cbNeeded)) ExitSys("EnumProcesses"); dwProcessCount = cbNeeded / sizeof(DWORD); for (DWORD i = 0; i < dwProcessCount; ++i) { if ((hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, dwProcessIds[i])) == NULL) continue; if (!EnumProcessModules(hProcess, hModules, sizeof(hModules), &cbNeeded)) { CloseHandle(hProcess); continue; } dwModuleCount = cbNeeded / sizeof(DWORD); for (DWORD k = 0; k < dwModuleCount; ++k) { if (!GetModuleBaseName(hProcess, hModules[k], szModuleName, MAX_PATH)) continue; if (k == 0) { printf("%s: ", szModuleName); continue; } if (k != 1) printf(", "); printf("%s", szModuleName); } printf("\n\n"); CloseHandle(hProcess); } printf("%lu process(es) listed...\n", dwProcessCount); getchar(); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastError = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } >> UNIX/Linux Sistemlerinde: UNIX/Linux ve macOS sistemlerinde her prosesin sistem genelinde tek olan (unique) bir "proses id" değeri vardır. Bu sistemlerde Windows sistemlerindeki gibi ayrıca proseslerin bir handle değerleri yoktur. Bu sistemlerde bir prosesi teşhis etmek için ve onun üzerinde işlem yapmak için proses id değeri yetmektedir. UNIX/Linux ve macOS sistemlerinde proseslerin id değerleri pid_t türü ile temsil edilmektedir. POSIX standartlarına göre pid_t türü işaretli bir tamsayı türü olmak üzere işletim sistemlerini yazanlar tarafından herhangi bir olarak ve dosyalarında typedef edilmek zorundadır. Bu tür genellikle "int" ya da "long" olarak typedef edilmektedir. UNIX/Linux sistemleri boot edildiğinde boot kodu kendini bir proses yapmakta ve 0 numaralı id'ye sahip olmaktadır. Bu prosese "swapper" ya da "pager" da denilmektedir. Bu proses "init" isimli prosesi yaratmaktadır. Bu "init" prosesi her zaman 1 numaralı id'ye sahip olur. Bu sistemler her yaratılan proses için sırasıyla bir proses id değeri üretirler. Üst limite ulaşıldığında yeniden başa dönülmektedir. UNIX/Linux sistemlerinde çekirdek proses id verildiğinde çok hızlı bir biçimde o prorsese ilişkin proses kontrol bloğuna erişebilmektedir. Yani bu sistemlerde proses id'ler proses kontrol bloğuna erişmekte kullanılan bir handle değeri gibidir. Bunun için UNIX/Linux çekirdekleri tipik olarak bir hash tablosu kullanmaktadır. UNIX/Linux sistemlerinde proseslerin yaratılması Windows sistemlerindekinde oldukça farklıdır. UNIX/Linux sistemlerinde yeni bir proses yaratmak için fork isimli POSIX fonksiyonu kullanılmaktadır. fork POSIX fonksiyonu genel olarak doğrudan işletim sisteminin ilgili sistem fonksiyonunu çağırmaktadır. Bu sistemlerde proses yaratmanın başkaca yolu yoktur. (fork benzeri vfork isimli bir fonksiyon olsa da bu fonksiyonun artık bir kullanım gerekliliği kalmamıştır.) fork işlemi UNIX/Linux ve macOS sistemlerindeki en önemli kavramsal süreçlerden biridir. >>> "fork" : fork fonksiyonu bir prosesin özdeş bir kopyasını oluşturmaktadır. fork işlemi sırasında kabaca şunlar yapılmaktadır: -> Proses kontrol bloğunun yeni bir kopyası oluşturulur. Yani yeni bir proses kontrol bloğu yaratılıp üst prosesin proses kontrol bloğu içeriği bazı istisnalarla alt prosesin pross kontrol bloğuna kopyalanır. Dolayısıyla bu işlemden sonra üst ve alt proseslerin gerçek ve etkin kullancı ve grup id'leri, çalışma dizinleri vs. tamamen aynı olmaktadır. -> Üst prosesin bellek alanı da tamamen alt proses için alt prosesin bellek alanına kopyalanmaktadır. Artık üst proses ve alt proses bağımsız iki ayrı proses olmaktadır. Bu iki prosesin geçmişleri aynı gibidir ancak birbirinden bağımsızdırlar. Burada üst proses ile alt prosesin aynı program koduna sahip olacağına da dikkat ediniz. -> Yeni bir çizelgele elemanı oluşturulup alt proses de zaman paylaşımlı biçimde bağımısz çalışmaya devam eder. -> Çatallanma fork fonksiyonun içinde olmaktadır. Böylece yeni proses (yani alt proses) hayatına fork fonksiyonun içinden başlamaktadır. Böylece hem üst proses hem de alt proses fork fonksiyonundan çıkacaktır. fork fonksiyonun prototipi şöyledir: #include pid_t fork(void); Hem üst proses hem de alt proses fork fonksiyonundan çıkamktadır. Üst proseste fork fonksiyonu alt prosesin proses id değeri ile, alt proseste ise 0 değeriyle geri dönmektedir. (Alt prosesin fork fonksiyonundan 0 ile geri dönmesi onun proses id'sinin 0 olduğu anlamına gelmemektedir.) fork başarısız olabilir. Bu durumda fork -1 değerine geri döner. fork fonksiyonunun kullaımına ilişkin tipik kalıp şöyledir: pid_t pid; if ((pid = fork()) == -1) exit_sys("fork"); if (pid != 0) { /* parent process */ ... } else { /* chile process */ ... } Üst prosesin ve alt prosesin her ikisin de fork fonksiyonundan çıktığını söylemiştik. Ancak hangi prosesin fork fonksiyonundan önce çıkacağının hiçbir garantisi yoktur. Aşağıda tipik bir fork uygulama kalıbı görülmektedir. * Örnek 1, #include #include #include void exit_sys(const char *msg); int main(void) { pid_t pid; if ((pid = fork()) == -1) exit_sys("fork"); if (pid != 0) { /* parent process */ printf("parent process...\n"); } else { printf("child process...\n"); /* child process */ } printf("common code...\n"); sleep(1); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } fork işleminden sonra artık üst proses ile alt proses arasında hiçbir bağlantı kalmamıştır. Dolayısıyla birinin yaptığı bir şey diğerini etkilemez. * Örnek 1, Aşağıdaki kodda üst proses g_x global değişkenine 10 değerini atamıştır ancak bu g_x üst prosesin kendi kopyasındaki g_x'tir. Dolayısıyla alt proses bu g_x nesnesinin değerini değişmiş görmez. #include #include #include void exit_sys(const char *msg); int g_x; int main(void) { pid_t pid; if ((pid = fork()) == -1) exit_sys("fork"); if (pid != 0) { /* parent process */ g_x = 10; printf("parent: %d\n", g_x); } else { sleep(1); printf("child: %d\n", g_x); } printf("common code...\n"); sleep(1); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2.0, Aşağıdaki kodda ekrana (stdout dosyasına) kaç tane "common code..." yazısı çıkacaktır (kontroller yapılmamıştır)? fork(); fork(); fork(); printf("common code...\n"); Birinci fork'a bir proses girer ondan 2 proses çıkar. İkinci fork'a 2 proses girer ondan 4 proses çıkar. Üçüncü fork'a 4 proses girer ondan 8 proses çıkar. Yani bu yazı toplamda 8 kere ekrana basılacaktır. * Örnek 2.1, #include #include #include void exit_sys(const char *msg); int main(void) { pid_t pid; if ((pid = fork()) == -1) exit_sys("fork"); if ((pid = fork()) == -1) exit_sys("fork"); if ((pid = fork()) == -1) exit_sys("fork"); printf("common code...\n"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } fork fonksiyonu başka bir programın çalışmasına yol açmamaktadır. Aynı programın özdeş bir kopyasının çalışmasına yol açmaktadır. Oysa Windows'taki CreateProcess başka bir programı çalıştırarak proses oluşturmaktadır. O halde UNIX/Linux sistemlerinde fork ne işe yaramaktadır ve başka bir program nasıl çalıştırılmaktadır? Sonraki paragraflarda açıklayacağımız gibi aslında genellikle fork fonksiyonu tek başına değil exec fonksiyonlarıyla birlikte kullanılmaktadır. Eskiden thread'ler yoktu. Bir işi birden fazla akışa yaptırabilmek için yeni prosesler yaratmak gerekiyordu. Böylece üst proses ile alt proses aynı işin farklı kısımlarını birlikte yapabiliyordu. Örneğin: if ((pid = fork()) == -1) exit_sys("fork"); if (pid != 0) { /* üst proses */ /* üst proses işin bir parçasını yapıyor */ } else { /* üst proses */ /* üst proses işin diğer bir parçasını yapıyor */ } Ancak artık bu tür işlemler için thread'ler kullanılmaktadır. Thread'ler proseslere göre çok daha az sistem kaynağı harcamaktadır. Dolayısıyla yaratılmaları ve yok edilmeleri de proseslere kıyasla daha hızlıdır. Ayrıca bir işi birden fazla akışa yaptırırken onların koordine edilmesi de gerekmektedir. Bu koordinasyon için fork modelinde proseslerarası haberleşme yöntemleri kullanılmaktadır. Bunun da ek maliyetleri vardır. Halbuki thread'ler global değişkenler yoluyla haberleşebilmektedir. >>> "exec" fonksiyonları : Başka program dosyasını çalıştırabilmek için UNIX/Linux sistemlerinde "exec fonksiyonları" diye isimlendirilen POSIX fonksiyonları kullanılmaktadır. exec fonksiyonları bir aile belirtmektedir. Aslında bu fonksiyonların hepsi aynı işlemleri biraz değişik parametrelerle yapmaktadır. exec ailesinde 7 farklı fonksiyon vardır: execl execlp execv execvp execle execvpe execve (Linux'ta yalnızca bu bir sistem fonksiyonu olarak gerçekleştirilmiştir) Aslında yalnızca execve bir sistem fonksiyonu olarak gerçekleştirilmiştir. Diğer exec fonksiyonları kütüphane fonksiyonlarıdır ve aslında bu execve fonksiyonunu çağırarak işlemlerini yaparlar. exec fonksiyonları prosesin bellek alanını boşaltıp başka bir program dosyasını o bellek alanına yükleyip onu çalıştırmaktadır. exec fonksiyonları proses yaratmazlar. Mevcut prosesin başka bir kodla çalışmaya devam etmesini sağlarlar. Yani exec işlemi yapıldığında artık exec yapan kod yok olacak exec işleminde belirtilen program dosyası çalıştırılacaktır. Ancak exec işlemi proses kontrol bloğunu değiştirmemektedir. Dolayısıyla exec işleminden sonra prosesin id'si, etkin kullanıcı id'si, etkin grup id'si, çalışma dizini vs. aynı kalır. Şimdi de sırasıyla bu fonksiyonları inceleyelim: >>>> "execl" : En çok kullanılan exec fonksiyonlarından biri "execl" isimli fonksiyondur. Fonksiyonun prototipi şöyledir: #include int execl(const char *path, const char *arg0, ... /*, (char *)0 */); Fonksiyonun, -> Birinci parametresi çalıştırılacak olan program dosyasının yol ifadesini belirtmektedir. -> Diğer parametreler o programa geçirilecek olan komut satırı argümanlarıdır. -> Fonksiyonun "..." parametresi aldığına dikkat ediniz. C'de bu parametre fonksiyonun istenildiği kadar çok argümanla çağrılabileceğini belirtmektedir. Tabii buradaki "..." için girilen argümanların hepsi komut satırı argüman yazılarının adresleri olmalıdır. Ancak execl fonksiyonunun argüman listesinin bittiğini anlayabilmesi için buradaki argüman listesinin sonu NULL adresle bitirilmelidir. Ancak burada NULL adesi düz sıfır olarak ya da NULL sembolik sabitiyleoluşturmayınız. Çünkü C'de "..." için kullanılan argümanlarda düz sıfır int olarak ele alınacaktır. Benzer biçimde NULL sembolik sabitide düz sıfır olarak define edilmiş olabileceği için benzer soruna yol açabilmektedir. Bu nedenle programcının argüman listesinin sonuna açıkça NULL adresi (char *)0 biçiminde yerleştirmesi uygun olmaktadır. Programın ilk komut satırı argümanının program simi olması zorunlu değildir. Ancak genel beklenti bu yöndedir. execl fonksiyonu başarısızlık durumunda -1 değerine geri dönmektedir. Fonksiyon başarı durumunda geri dönmez. Çünkü zaten başarı durumunda başka bir program kodu çalışmaya başlayacaktır. Örneğin: if (execl("/bin/ls", "/bin/ls", "-l", (char *)0) == -1) exit_sys("execl"); Burada "/bin/ls" programı (yani ls komutu uygulandığında çalıştırılan program) çalıştırılmak isteniştir. Programın ilk komut satırı argümanı kendisi olmalıdır. Komut satırı argüman listesinin (char *)0 ifadesi ile sonlandırıldığına dikkat ediniz. * Örnek 1, #include #include #include void exit_sys(const char *msg); int main(void) { printf("main begins...\n"); if (execl("/bin/ls", "/bin/ls", "-l", (char *)0) == -1) exit_sys("execl"); /* unreachable code */ return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte "sample" programı execl fonksiyonu ile aşağıdaki gibi mample programını çalıştırmaktadır: if (execl("mample", "mample", "ali", "veli", "selami", (char *)0) == -1) exit_sys("execl"); "mample" programı komut satırı argümanlarını yazdıran bir programdır. mample programının çıktısı şöyledir: main begins... argv[0] ==> mample argv[1] ==> ali argv[2] ==> veli argv[3] ==> selami İlgili programın kodları aşağıdaki gibidir: /* sample.c */ #include #include #include void exit_sys(const char *msg); int main(void) { printf("main begins...\n"); if (execl("mample", "mample", "ali", "veli", "selami", (char *)0) == -1) exit_sys("execl"); /* unreachable code */ return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* mample.c */ #include int main(int argc, char *argv[]) { for (int i = 0; i < argc; ++i) printf("argv[%d] ==> %s\n", i, argv[i]); return 0; } >>>> "execv" : execv (v vector'den geliyor) fonksiyonu execl fonksiyonun komut satırı argümanlarını tek tek değil de bir gösterici dizisi biçiminde tek bir parametreyle alan biçimidir. Yani çalıştırılacak programın komut satırı argümanları char türünden bir gösterici dizisine yerleşltirilip bu dizinin adresi execv fonksiyonuna verilmektedir. Fonksiyonun prototipi şöyledir: #include int execv(const char *path, char * const argv[]); Fonksiyonun birinci parametresi çalıştırılacak program dosyasının yol ifadesini almaktadır. İkinci parametre komut satırı argümanlarının bulunduğu dizinin başlangıç adresini alır. Bu gösterici dizisinin son elemanı NULL adres olmalıdır. Yine fonksiyon başarısızlık durumunda -1 değerine geri döner. Başrı durumunda zaten çağrıldığı yere geri dönememektedir. Örneğin: const char *args[] = {"ls", "-l", NULL}; ... if (execv("/bin/ls", args) == -1) exit_sys("execv"); Aşağıda execv fonksiyonunun kullanımına bir örnek verilmiştir. * Örnek 1, #include #include #include void exit_sys(const char *msg); int main(void) { pid_t pid; char *args[] = {"ls", "-l", NULL}; printf("main begins...\n"); if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0 && execv("/bin/ls", args) == -1) exit_sys("execl"); for (int i = 0; i < 10; ++i) { printf("%d\n", i); sleep(1); } return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Bazen execv fonksiyonu execl fonksiyonundan daha uygun olabilmektedir. Örneğin komut satırı argümanlarıyla aldığımız bir propgramı çalıştırmak istesek execl işimizi çok zorlaştıracaktır. Burada execv çok daha uygundur. "sample" programının komut satırı argümanlarıyla aldığı programı çalıştırdığını düşünelim: ./sample /bin/ls -l -i Burada çalıştırılacak program "/bin/ls" programıdır. Diğerleri onun komut satırı argümanlarıdır. İlk komut satırı argümanının program ismiyle aynı olması gerektiğini de anımsayınız. Burada "sample" programının argv[1] parametresi "/bin/ls" yazısını göstermektedir. &argv[1] ise buradan başlayan bir gösterici dizisi gibidir. argv listesinin sonunda zaten NULL adres bulunmaktadır. O halde exec işlemi şöyle basit bir biçimde yapılabilir: if (execv(argv[1], &argv[1]) == -1) exit_sys("execv"); Aşağıda programın nasıl yazıldığı görülmektedir. * Örnek 1, #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { pid_t pid; if (argc < 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0 && execv(argv[1], &argv[1]) == -1) exit_sys("execl"); sleep(1); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } >>>> exec fonksiyonlarının p'li versiyonları (yani sonunda p soneki olan versiyonları) eğer yol ifadesinde hiç '/' karakteri yoksa ilgili dosyayı PATH çevre değişkeni ile belirtilen dizinlerde tek tek aramaktadır. Eğer dosyanın yol ifadesinde en az bir '/' karakteri varsa bu durumda dosya belirtilen yol ifadesinde aranır. Yani bu durumda exec fonksiyonlarının p'li versiyonlarıyla p'siz versiyonları arasında bir fark olmaz. execlp ve execvp fonksiyonlarının prototipleri exel ve execv ile aynıdır. Yalnızca kavramsal farkılığı vurgulamak için birinci parametre "path" yerine "file" olarak isimlendirilmiştir: #include int execlp(const char *file, const char *arg0, ... /*, (char *)0 */); int execvp(const char *file, char *const argv[]); PATH çevre değişkeni UNIX/Linux sistemlerinde ':' karakterinden ayrıştırılmaktadır. (Windows sistemlerinde ';' karakteri ile ayrıştırıldığını anımsayınız.) Linux sistemlerindeki örnek bir PATH değişkeninin değerine bakınız: /home/kaan/anaconda3/bin:/home/kaan/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin "/bin", "/usr/bin", "/usr/local/bin" gibi prgram dosyalarının bulunduğu dizinlerin PATH çevre değişkeninde bulunduğuna dikkat ediniz. Artık UNIX/Linux sistemlerinde bir programı çalıştırırken neden "./sample" biçiminde program isminin önüne "./" getirdiğimizi anlamış olmalısınız. Kabul programları exec fonksiyonlarının p'li versiyonlarını kullanmaktadır. execv fonksiyonlarının p'li versiyonları eğer çalıştırılacak program dosyasına ilişkin yoli fadesinde hiç '/' karakteri yoksa onu PATH çevre değişkeni ile belirtilen dizinlerde sırasıyla aramaktadır. Bu durumda prosesin çalışma dizinin de hiç arama yapmamaktadır. (Halbuki Windows'ta benzer durumda dosya önce çalışma dizininde arandıktan sonra PATH çevre değişkenine bakılmaktadır.) Örneğin biz bulunduğumuz dizindeki "sample" programını şöyle çalıştırmaktayız: ./sample Bu programı biz aşağıdaki gibi çalıştırsaydık prgram dosyası bulunamaz: sample Çünkü burada exec fonksiyonlarının p'li versiyonları dosya yol ifadesinde hiç '/' karakteri geçmediği için onu yalnızca PATH çevre değişkeni ile belirtilen dizinlerde arayacaktır. Halbuki biz programı "./sample" biçiminde çalıştırmak istediğimizde artık işin içine bir '/' karakteri sokulduğu için exec fonksiyonlarının p'li versyionları PATH çevre değişkenine bakmayacaktır. '.' karakterinin "prosesin çalışma dizini" anlamına geldiğine dikkat ediniz. Biz çalıştırmayı örneğin "/sample" biçiminde yapamazdık. Çünkü bu durumda "sample" programı kök dizinde aranırdı. Pekiyi exec fonksiyonlarının p'li biçimleri neden Windows gibi önce prosesin çalışma dizinine bakmamaktadır? Ya da kabuk programları neden prosesin çalışma dizinine de bakmamaktadır? İşte çok eskiden kabuk programları Windows'taki gibi kabuğun çalışma dizinine de bakıyordu. Ancak bu tasarımın bazı kötüye kullanımlarıyla karşılaşıldı. (Örneğin birisi birisinin dizinine bir komutla aynı isimli bir program dosyası yerleştirebilir ve kişi komutu çalıştırdığını sanırken başkasının programını çalıştırabilir. Ya da kişi bir komutun ismini yanlış yazarak yanlışlıkla çalışma dizinindeki başka bir programı da çalıştırabilir.) Bugün artık bu sistemlerde kabuk programlarının exec fonksiyonlarının p'li versiyonlarıyla exec yapması oturmuş bir kural biçimindedir. Aşağıda komut satırı argümanlarıyla aldığı programı çalıtıran programı bu kez execvp fonksiyonunu kullanarak yazıyoruz. * Örnek 1, #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { pid_t pid; if (argc < 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0) { execvp(argv[1], &argv[1]); exit_sys("execvp"); } sleep(1); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } >>>> exec fonksiyonlarının bir de e'li biçimleri vardır. Bunlar exec işlemi sırasında çalıştırılacak program için yeni bir çevre değişken takımı oluşturmakltadır. execle ve execve fonksiyonlarının prototipleri şöyledir: #include int execle(const char *path, const char *arg0, ... /*, (char *)0, char *const envp[]*/); int execve(const char *path, char *const argv[], char *const envp[]); Bu fonksiyonlar p'li olmadığı için PATH çevre deişkenine hiç bakmamaktadır. Her iki fonksiyonun da ilk parametresi çalıştırılacak programın yol ifadesini belirtmektedir. execle fonksiyonu önce komut satırı argümanlarını bir liste olarak alır. Bu argümanların sonunda NULL adres bulunmalıdır. Bu NULL adresten sonra çevre değişkenleri "anahtar=değer" yazıları biçiminde sonu NULL adresle biten bir gösterici dizisi biçiminde girilmelidir. execve fonksiyonu ise hem komut satırı argümanlarını ehm de çevre değişkenlerini gösterici dizisi biçiminde almaktadır. Her iki fonksiyon da yine başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri dönmektedir. UNIX/Linux sistemlerinde çevre değişkenlerinin genel olarak prsoesin bellek alanı içerisinde tutulduğunu anımsayınız. Çevre değişkenleri aslında fork işlemi sırasında üst prosesin tüm bellek alanı alt prosese kopyalandığından dolayı alt prosese geçirilmektedir. Ancak exec fonksiyonları prosesin bellek alanını yok edip onun yerine başka bir programı bu bellek alanına yüklediklerinden dolayı bu çevre değişkenlerinin kaybolması beklenir. İşte exec fonksiyonlarının e'siz biçimleri fork sonrasında bu eçvre değişkenlerini saklayıp exec işlemi ile çalıştırılan program için ayrılan bellek alanına aktarmaktadır. Yani exec fonksiyonlarının e'siz versiyonlarında biz programı çalıştırdığımızda çevre değişkenleri üst prosesle aynı olmaktadır. Ancak exec fonksiyonlarının e'li biçimleri programcının belirlediği çevre değişkenlerini exec işlemi sırasında yeni programın bellek alanına aktarmaktadır. execle fonksiyonu tipik olarak şöyle kullanılmaktadır: pid_t pid; char *env[] = {"city=eskişehir", "plate=26", NULL}; ... if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0 && execle("mample", "mample", "ali", "veli", "selami", (char *)NULL, env) == -1) exit_sys("execl"); Aşağıda bu fonksiyonların kullanımına ilişkin bir program verilmiştir. * Örnek 1, /* sample.c */ #include #include #include #include void exit_sys(const char *msg); int main(void) { pid_t pid; char *envs[] = {"city=eskişehir", "plate=26", NULL}; if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0 && execle("mample", "mample", "ali", "veli", "selami", (char *)NULL, envs) == -1) exit_sys("execl"); if (wait(NULL) == -1) exit_sys("wait"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* mample.c */ #include extern char **environ; int main(int argc, char *argv[]) { printf("Command line arguments:\n"); for (int i = 0; i < argc; ++i) printf("%s\n", argv[i]); printf("\nEnvironment Variables:\n"); for (int i = 0; environ[i] != NULL; ++i) puts(environ[i]); return 0; } * Örnek 2, Aşağıda execve fonksiyonunun kullanımına bir örnek verilmiştir. execve fonksiyonunda hem komut satırı argümanlarının hem de çevre değişkenlerinin gösterici dizisi biçiminde verildiğine dikkat ediniz. /* sample.c */ #include #include #include #include void exit_sys(const char *msg); int main(void) { pid_t pid; char *args[] = {"mample", "ali", "veli", "selami", NULL}; char *envs[] = {"city=eskişehir", "plate=26", NULL}; if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0 && execve("mample", args, envs) == -1) exit_sys("execl"); if (wait(NULL) == -1) exit_sys("wait"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* mample.c */ #include extern char **environ; int main(int argc, char *argv[]) { printf("Command line arguments:\n"); for (int i = 0; i < argc; ++i) printf("%s\n", argv[i]); printf("\nEnvironment Variables:\n"); for (int i = 0; environ[i] != NULL; ++i) puts(environ[i]); return 0; } Aslında pek çok UNIX türevi sistemde yalnızca execve fonksiyonu bir sistem fonksiyonu olarak yazılmıştır. Başka bir deyişle yalnızca execve fonksiyonu çekirdeğin içerisindedir. Diğer exec fonksiyonlarının hepsi bir çeşit "sarma (wrapper) fonksiyon" gibidir ve normal user mod kütüphane içerisinde bulunmaktadır. Örneğin biz execl fonksiyonunu çağırdığımızda bu komut satırı argümanları bir gösterici dizisine yerleştirilip environ değişkenini kullanarak execve fonksiyonunu çağırmaktadır. Örneğin execlp fonksiyonu programı PATH çevre değişkeni ile belirtilen dizinlerde tek tek execve uygulayarak aramaktadır. Yani aslında PATH çevre değişkenine çekirdek kodları bakmamaktadır. Bu işlem kütüphane fonksiyonu tarafından user modda yapılmaktadır. exec fonksiyonlarının execve fonksiyonu çağrılarak nasıl gerçekleştirildiğine yönelik "Advanced Programming in the UNIX Environment" kitabının 254'üncü sayfasındaki şekli inceleyebilirsiniz. Aşağıda bir execve kullanımı görülmektedir. * Örnek 1, /* sample.c */ #include #include #include #include void exit_sys(const char *msg); int main(void) { pid_t pid; char *args[] = {"./mample", "mample", "ali", "veli", "selami", NULL}; char *env[] = {"city=eskişehir", "plaka=26", NULL}; if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0 && execve("./mample", args, env) == -1) exit_sys("execve"); if (wait(NULL) == -1) exit_sys("wait"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* mample.c */ #include extern char **environ; int main(int argc, char *argv[]) { int i; printf("Command line arguments:\n"); for (i = 0; i < argc; ++i) printf("%s\n", argv[i]); printf("Environment Variables:\n"); for (i = 0; environ[i] != NULL; ++i) puts(environ[i]); return 0; } >>>> exec fonksiyonlarının sonuncusu fexecve isimli fonksiyondur. Bu fonksiyonun execve fonksiyonundan tek farkı çalıştırılacak dosyanın yol ifadesini değil onun dosya betimleyicisini almasıdır. Bu fonksiyon çok seyrek kullanılmaktadır. Prototipi şöyledir: #include int fexecve(int fd, char *const argv[], char *const envp[]); exec fonksiyonları ile aslında çalıştırılabilir olmayan dosyalar da (örneğin text dosyalar) çalıştırılmak istenebilir. Bu durumda exec fonksiyonları ilgili dosyayı açıp onun ilk iki karakterine bakmaktadır. Eğer dosyada ilk iki karakter #! biçimindeyse bu karakterlere "shebang" denilmektedir. shebang karakterlerini gerçek çalıştırılabilir bir dosyanın yol ifadesi izlemelidir. (Shebang'ten sonra boşluk karakterleri bulunabilir.) İşte exec fonksiyonları aslında burada belirtilen dosyayı çalıştırılar. Çalıştırdıkları dosyaya da exec yapılan dosyayı komut satır argümanı olarak geçirirler. Yukarıda da belirttiğimiz gibi UNIX/Linux sistemlerinde yalnızca execve fonksiyonu bir sistem fonksiyonu olarak gerçekleştirilmiştir. Bu shebang kontrolü kernel tarafından bu execve içerisinde yapılmaktadır. execve burada belirtilen çalıştılabilir dosyayı bir yol ifadesi olarak kabul eder. Genel olarak buradaki dosyanın mutlak yol ifadesi belirtmesi istenmektedir. Ancak bugünkü sistemler göreli yol ifadelerini de kabul etmektedir. Buradaki dosya execve tarafından PATH çevre değişkenine bakılmadan doğrudan ele alınmaktadır. Burada belirtilen dosyadan sonra yazılan komut satırı argümanları buradaki çalıştırılabilen dosyaya tek bir komut satırı argümanı biçiminde aktarılmaktadır. exec fonksiyonlarının kendisinde belirtilen argümanlar ise son komut satırı argümanları olarak kullılmaktadır. Aşağıdaki örnekte bir shebang mekanizması uygulanmıştır. "sample.c" programı komut satırı argümanlarıyla alınan dosyayı execv ile çalıştırmaktadır. Biz bu programla "test.txt" dosyasını aşağıdaki gibi çalıştıracağız: #! /home/kaan/Study/SysProg/mample Burada execp fonksiyonunda programın komut satırı argümanları "test.txt", "xxx" ve "yyy" durumundadır. "test.txt" dosyasının başındaki shebang kısmı da şöyle olsun: #! /home/kaan/Study/SysProg/mample mample programı da komut satırı argümanlarını ekrana yazan program olsun. Bu durumda ekrana şunlar çıkacaktır: argv[0]: /home/csd/Study/SysProg-2020/mample argv[1]: ali veli selami argv[2]: test.txt argv[3]: xxx argv[4]: yyy Shebang içeren dosyanın ne olursa olsun x özelliklerine sahip olması gerekir. Çünkü exec fonksiyonları bunu kontrol etmektedir. Bir dosyaya x hakları vermenin en pratik yolu şöyledir: chmod +x test.txt Tabii aslında text dosyalar eğer "x" hakkına sahipse doğrudan kabuk üzerinden de çalıştırılabilmektedir. Zaten bu durumda kabuk programları bunları exevp ile çalıştırmaktadır. Örneğin "text.txt" dosyası aşağıdaki gibi olsun: #! /home/kaan/Study/SysProg/mample Şimdi biz bu dosyayı komut satırından çalıştırmak istediğimizde aslında "mample" programı çelıştırılacaktır. "mample" programına da "ali veli selami" ve "test.txt" parametre olarak geçirilecektir. Örneğin: $ ./test.txt argv[0]: /home/kaan/Study/SysProg/mample argv[1]: ./test.txt $ ./test.txt xxx yyy argv[0]: /home/kaan/Study/SysProg/mample argv[1]: ./test.txt argv[2]: xxx argv[3]: yyy Shebang kullanımının en önemli faydası script dosyalarının sanki çalıştırılabilir bir dosya gibi çalıştırılmasını sağlamaktır. Örneğin aşağıdaki gibi "sample.py" isminde bir python programı bulunyor olsun: #! /usr/bin/python3 for i in range(10) : print(' {}'.format(i)) Biz bu "sample.py" dosyasına "x" hakkı vererek onu çalıştırdığımızda aslında "/usr/bin/python3" programı çalıştırılacaktır. Bu programa da "sample.py" komut satırı argümanı olarak verildiği için sanki çalıştırma aşağıdaki gibi yapılıyormuş etkisi oluşacaktır: python3 sample.py Windows sistemlerinde shebang gibi bir kullanım yoktur. Benzer işlemler dosya ilişkilendirmesi yoluyla yapılmaktadır. ShellExecute fonksiyonun dosya ilişkilendirmesine baktığını biliyorsunuz. #!/usr/bin/python3 for i in range(10) : print(' {}'.format(i)) Aslında kabuk komutları kendi içlerinde yorumlayıcı (interpreter) da içermektedir. Yani biz kabuk komutlarını bir dosyada bulundurup onları sanki bir program gibi çalıştırabiliriz. Kabukların ayrı bir script dili vardır. Shell scirpt dilleri basit olsa da ayrıca öğrenilmesi gerekir. Örneğin aşağıdaki gibi "sample.sh" isminde bir bash script dosyası oluşturup ona "x" hakkı vererek komut satırından çalıştırabiliriz: #!/bin/bash for n in {1..10}; do echo $n done Çalıştırma şöyle yapılabilir: ./sample.sh Aslında bu script bash tarafından aşağıdaki gibi çalıştırılmaktadır: /bin/bash sample.sh Bir çalıştırılabilir text dosyanın başında shebang bölümü yoksa bu durumda bu dosya kabuk programı tarafından bir kabuk scipt dosyası gibi çalıştırılmaktadır. Örneğin: for n in {1..10}; do echo $n done Buradaki dosyanın başında "shebang" karakterşeri yoktur. Bu dosya çalıştırılmak istendiğinde doğrudan sanki bir shell script gibi çalıştırılacaktır. Burada aslında kabuk önce dosyayı "execvp" ile çalıştırmak ister. Dosya çalıştırılamazsa (örneğin dosyanın başında shebang) yoksa bu kez onu script dosyası gibi kendisi çalıştırır. Ancak bu tarzda çalıştırma daha maliyetlidir. Bu nedenle shell script dosyalarının başında "gerekmese bile" shebang bulundurulmalıdır. Tek başına fork bir prosesin özdeş kopyasını oluştup çalıştırmaktadır. Tek başına exec ise prosesin başka bir kodla çalışmasını sağlamaktadır. O halde hem üst prosesin kodu çalışsın hem de başka bir programın kodu çalışsın isteniyorsa fork ve exec birlikte kullanılmalıaıdır. Yani önce bir kez fork yapılır. Alt proseste exec uygulanır. fork sırasında üst prosesin kopyasından çıkartılacaktır. Bu sırada üst prosesin bellek alanın da kopyasından çıkarılır. Alt proseste exec yapıldığında o kod bellekten yok edilip exec yapılan programın kodu belleğe yüklenip çalıştırılır. Tipik fork/exec kalıbı şöyledir: if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0) { if (execl(....) == -1) exit_sys("execl"); /* dikkat burada sonlanan alt proses */ /* unreachable code */ } /* buraya yalnızca üst proses akışı gelecektir */ && operatörünün kısa devre özelliği kullanılarak bu işlem daha kompakt biçimde de ifade edilebilirdi: if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0 && execl(....) == -1) { exit_sys("execl"); /* dikkat burada sonlanan alt proses */ /* unreachable code */ } /* buraya yalnızca üst proses akışı gelecektir */ Bazı programcılar ise aşağıdaki kalıbı tercih etmektedir: if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0) { execl(....); exit_sys("execl"); /* exec başarısızsa akış buraya gelecektir */ } /* buraya yalnızca üst proses akışı gelecektir */ Microsoft sistemlerindeki CreateProcess API fonksiyonunun fork/exec ikilisi gibi işlem gördüğüne dikkat ediniz. * Örnek 1, #include #include #include void exit_sys(const char *msg); int main(void) { pid_t pid; printf("main begins...\n"); if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0 && execl("/bin/ls", "/bin/ls", "-l", (char *)0) == -1) exit_sys("execl"); for (int i = 0; i < 10; ++i) { printf("%d\n", i); sleep(1); } return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, fork/exec modelinde etkin olmayan şöyle bir durum kişilerin aklına gelmektedir: fork işleminden sonra alt proseste exec uyguladığımızda üst prosesin bellek alanının gereksiz bir kopyası oluşturulmuş olmuyor mu? İlk bakışta burada gerçekten bir performans sorunu olduğu düşünülmektedir. Eski sistemlerde gerçekten bu bir sorundu. Bu nedenle fork fonksiyonun vfork isminde bir kardeşi bulundurulmuştu. vfork fonksiyonunun fork fonksiyonundan tek farkı üst prosesin bellek alanının kopyasından çıkarmamasıdır. Tabii vfork fonksiyonundan sonra artık kesinlikle exec işlemi yapılmalıdır. Eğer vfork fonksiyonundan sonra exec işlemi yapılmazsa "tanımsız davranış" oluşmaktadır. Bugün vfork fonksiyonu POSIX standratlarında hala muhafaza ediliyor olsa da faydalı bir kullanımı kalmamış gibidir. Çünkü uzun süredir UNIX/Linux sistemlerinin çalıştığı makinelerdeki işlemcilerin "sayfalama (paging)" ve "sanal bellek (virtual memory) mekanizması" vardır. Bu mekanizma sayesinde aslında üst prosesin bellek alanın kopyası zaten gerektiğinde çıkartılmaktadır. Bu mekanizmaya "copy on write" denilmektedir. Dolayısıyla bugün zaten bir fork yapsak bile alt proseste henüz bir işlem yapmadıktan sonra ciddi bir kopyalama yapılmamaktadır. Bu nednele artık "fork/exec" işleminde yukarıdaki gibi bir performans problemi oluşmayacaktır. Aşağıdaki örnekte sample programı mample programını çalıştırmıştır. /* sample.c */ #include #include #include void exit_sys(const char *msg); int main(void) { pid_t pid; printf("main begins...\n"); if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0 && execl("mample", "mample", "ali", "veli", "selami", (char *)0) == -1) exit_sys("execl"); for (int i = 0; i < 10; ++i) { printf("%d\n", i); sleep(1); } return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* mample.c */ #include int main(int argc, char *argv[]) { printf("mample running...\n"); for (int i = 0; i < argc; ++i) printf("argv[%d] ==> %s\n", i, argv[i]); return 0; } Aslında UNIX/Linux sistemlerinde shell programlarındaki komutların çok büyük çoğunluğu çalıştırılabilir dosyalardır. Yani örneğin "ls" komutu aslında "/bin/ls" programıdır, "cat" komutu aslında "/bin/cat" programıdır. Çok az sayıda "internal" komut vardır. Bunlardan biri "cd" komutudur. (Zaten "cd" komutu bir program olarak yazılamazdı. Eğer "cd" bir program olsaydı üst prosesin çalışma dizinini değil (yani shell'in değil) kendi prosesinin çalışma dizinini değiştirirdi.) Hiçbir proses üst prosesinin çalışma dizinini değiştirememektedir. * Örnek 1, Aşağıda daha önce yazmış olduğumuz basit shell programının komutları external biçimde çalıştıran versiyonunu veriyoruz. Bu programda henüz görmediğimiz wait isimli bir fonksiyonu kullandık. Bu fonksiyon alt proses bitene kadar üst prosesi blokede bekletmektedir. /* myshell.c */ #include #include #include #include #include #include #define MAX_CMD_LINE 4096 #define MAX_CMD_PARAMS 128 #define BUFFER_SIZE 8192 #define MAX_PATH_SIZE 4096 void parse_cmdline(void); void cd_proc(void); void exit_sys(const char *msg); typedef struct tagCMD { char *cmd_name; void (*cmd_proc)(void); } CMD; char g_cmdline[MAX_CMD_LINE]; char *g_params[MAX_CMD_PARAMS]; int g_nparams; CMD g_cmds[] = { {"cd", cd_proc}, {NULL, NULL} }; char g_cwd[MAX_PATH_SIZE]; int main(void) { char *str; int i; pid_t pid; if (getcwd(g_cwd, MAX_PATH_SIZE) == NULL) exit_sys("getcwd"); for (;;) { printf("CSD:%s>", g_cwd); if (fgets(g_cmdline, MAX_CMD_LINE, stdin) == NULL) continue; if ((str = strchr(g_cmdline, '\n')) != NULL) *str = '\0'; parse_cmdline(); if (g_nparams == 0) continue; if (!strcmp(g_params[0], "exit")) break; for (i = 0; g_cmds[i].cmd_name != NULL; ++i) if (!strcmp(g_cmds[i].cmd_name, g_params[0])) { g_cmds[i].cmd_proc(); break; } if (g_cmds[i].cmd_name == NULL) { if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0 && execvp(g_params[0], &g_params[0]) == -1) { printf("invalid command: %s\n", g_params[0]); exit(EXIT_FAILURE); } if (wait(NULL) == -1) exit_sys("wait"); } } return 0; } void parse_cmdline(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) { if (g_nparams == 1) { printf("argument missing!..\n"); return; } if (g_nparams > 2) { printf("too many arguments!..\n"); return; } if (chdir(g_params[1]) == -1) { printf("%s: %s!..\n", g_params[1], strerror(errno)); return; } if (getcwd(g_cwd, MAX_PATH_SIZE) == NULL) exit_sys("getcwd"); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); }