> Zaman paylaşımlı çalışmak: Sistemimizde birden fazla proses çalışıyor olabilir. Bir proses de birden fazla "thread" i çalıştırıyor da olabilir. Fakat bir proses ilk çalışmaya başladığında tek bir "thread" ile çalışmaya başlıyor ki bunun adı da "main-thread". Daha sonra biz kullanıcılar o proses için başka başka "thread" ler hayata getirebiliriz. Pekiyi zaman paylaşımlı çalışmak ne demek? Varsayalım ki elimizde tek çekirdekli bir adet CPU olsun. O anda da bir kaç tane prosesin çalıştığını, her bir proses de bünyesinde iki ya da üç tane "thread" çalıştırıyor olsun. İşte işletim sisteminin "Schelurer" isimli alt mekanizmaları her bir "thread" i peyderpey CPU'ya atıyor. CPU, bu "thread" i bir miktar çalıştırıyor. Daha sonra diğer "thread" atanıyor. İşte bu şekilde her bir "thread" in küçük zaman dilimlerinde çalıştırılmasına zaman paylaşımlı çalışma denmektedir. Aslında o anda sadece bir "thread" CPU tarafından çalıştırılıyor fakat dışarıdan bakıldığında sanki bütün prosesler aynı anda çalışıyormuş gibi gözükmektedir. Bir CPU'nun bir "thread" i çalıştırdığı o süreye de "time quanta" süresi denmektedir. Bu süre tamamen işletim sisteminin tasarımına bağlıdır. Linux sistemlerinde tipik süre 60 milisaniyedir. Windows sistemlerinde ise bu süre 20 milisaniye civarındadır. Burada iki "thread" arası geçiş zorla yaptırılmaktadır. Yani 60 milisaniye çalıştırılan bir "thread" zorla CPU'dan ayrılmakta, en son kaldığı yer kaydedilmekte ve aynı "thread" tekrardan CPU'ya atandığında kaldığı yerden devam etmektedir. Bir "thread" in zorla CPU'dan koparılmasına ve diğer "thread" in CPU'ya atanması işlemine de "task-switch" ya da "context-switch" denmektedir. Tabii bu işlem de belli bir zaman almaktadır. Burada çizelgelenen şey "thread" lerdir. Burada yeni gelen "thread" başka bir prosese ait de olabilir. Pekiyi bu "time quanta" süresinin uzun ya da kısa olması nelere sebebiyet vermektedir? Eğer bu süre uzun tutulursa, diğer "thread" ler sanki hiç çalışmıyormuş gibi görünecektir. Bu süre kısa tutulduğunda ise çok sık "task-switch" gerçekleşeceğinden dolayı birim zamanda yapılan iş miktarı azalacaktır. İşletim sistemi dünyasında birim zamanda yapılan iş miktarına ise "throughput" denmektedir. Pekiyi bu "time quanta" süresi nasıl hesaplanıyor? Tabii ki dışsal donanım kesmeleriyle. CPU'ya 1 milisaniyede bir "timer" devresinden kesme gelmekte ve "kernel" da bunları saymaktadır. 60 milisaniye olduğunda ise "task-switch" meydana geliyor. Linux sistemlerde bu "timer" devresi için "jiffy" kavramı da kullanılmaktadır. Bahsi geçilen bu "timer" devreler ise artık günümüzde CPU'nun içerisine yerleştirilmektedir. "thread" akışının "time quanta" sonrasında zorla CPU'dan kopartılmasına işletim sistemleri dünyasında "preemptive OS" denmektedir. Windows, Linux, macOS işletim sistemleri bu tür işletim sistemleridir. Bazı tip işletim sistemleri ise "cooperative multi-task" işletim sistemleri olarak geçmektedirler. Bu tip işletim sistemlerinde "thread" ler zorla CPU'dan koparılmazlar. "thread" ler kendi istekleriyle CPU'dan ayrılırlar. Dolayısıyla bir "thread" kendini bırakmazsa, diğer "thread" ler çalışma fırsatı bulamazlar. Bu senaryoya da "starvation" denmektedir. Fakat bu tip sistemler çok kısıtlı alanlarda kullanılmaktadır. Örneğin, palmOS ve Windows 3.X sistemleri böyledir. Önceki derslerde anlatılan Zaman Paylaşımlı Çalışma methodu, aslında tek "core" a sahip işlemciler için geçerlidir. Pekiyi "multi-core" işlemciler için durum nasıldır? Böylesi işlemcilerde her bir "core" için ayrı bir "queueu" vardır ve her bir "core" yine zaman paylaşımlı çalışmaktadır. Örneğin, 4 "core" işlemciye sahip olduğumuzu düşünelim. Bu durumda her bir "core" için bir adet "queueu" olacağından yine dört adet "run-queueu" olacaktır fakat her bir "core" önceki derste anlatıldığı üzere kendi sırasındaki "thread" leri çalıştıracaktır. Dolayısıyla bilinmeyen bir t anında gerçekten de aynı anda çalışan dört adet "thread" den söz edebiliriz. Fakat "core" özelinde bakarsak, bilinmeyen bir t anında yine sadece bir adet "thread" söz konusudur. Linux işletim sistemi günümüzde bu sistemi kullanmaktadır fakat öncesinde bütün işlemciler için sadece bir adet "run-queueu" mevcuttu. Boşalan işlemcilere "thread" ler bu tekil "run-queueu" dan atanmaktaydı. Bu sistemin etkin olmadığı görüldüğü için kullanımından vazgeçilmiştir. İşletim sistemi bir "thread" i bir "run-queueu" ya atadıktan sonra onu bambaşka bir "run-queueu" ya atayadabilir. Çünkü işin başında müsait olan bir "core", daha sonra namüsait olabilir. Bu durumda müsait olan diğer "core" lar devreye girecektir.Windows ve macOS sistemleri de günümüzde Linux'un yöntemini kullanmaktadır. Çizelgeleme Algoritmasının detaylarına ise "thread" ler konusunda değinilecektir. Şu an için kavramsal olarak bir giriş yapmış olduk. Öte yandan klavyeden bir giriş bekleyen, disk üzerinde okuma/yazma işlemi yapacak olan, "socket" ten okuma yapacak olan "thread" ler için işletim sistemi nasıl bir yol izlemektedir? Bu tip "thread" ler bahsi geçen dışsal olayları yapacağı zaman, "run-queue" dan çıkartılır ve "wait-queueu" dediğimiz bekleme sırasına alınırlar. Böylelikle bu "thread" ler CPU'da çalışma zamanının boşa harcanmasına yol açmazlar. Ancak ilgili dışsal olay gerçekleştikten sonra tekrardan "run-queue" ya alınırlar. İşte bir "thread" in "run-queue" dan çıkartılıp "wait-queue" ya alınmasına ise ilgili "thread" in bloke olması denmektedir. "wait/waitpid" fonksiyonları da çağıran "thread" in bloke olmasını sağlarlar. Bütün bu işlemler o "thread" in "quanta" süresini ne kadar harcadığına bakmazlar. Uzun sürecek dışsal bir olay gerçekleşeceği vakit çalışma kuyruğundan alınır, bekleme kuyruğuna atanırlar. Burada işletim sistemi sürekli olarak çalışmamakta, donanımsal kesme geldikçe kontrolleri sağlamaktadır. Eee pekiyi bir "thread" in bekleme kuyruğundan alınıp, tekrardan çalışma kuyruğuna atanmasını sağlayan sistem nedir? Bu durumda "_exit" sistem fonksiyonu da kullanılmakta, donanımsal kesme de. Örneğin, "socket" ten bir okuma yaparken "network" kartından bir kesme geldiğinde ilgili "thread" uyandırılıyor veya bir proses "_exit" ile sonlandığında "wait/waitpid" çağrısı yapan "thread" tekrardan çalışma kuyruğuna alınıyor. Yine "sleep" fonksiyonunda da "timer" bir kesme uygulanmakta, sayaç ilgili değere ulaştığında "thread" tekrardan uyandırılmaktadır. Özetle; -> Çalışma kuyruğundan bekleme kuyruğuna geçiş için dışsal bir olayın vuku bulması gerekmektedir. -> Bekleme kuyruğundan tekrardan çalışma kuyruğuna geçiş için donanımsal kesme, "timer" kesmesi, "_exit" sistem fonksiyonunun çağrılması gibi olaylar vuku bulması gerekmektedir. Bekleme kuyrukları da her olay için ayrıdır. Örneğin, aygır sürücüler kendi bekleme kuyruklarını oluşturup bloke işlemlerini kendileri yapmaktadır. Paralel programlamada hangi "thread" in hangi "core" da çalıştırılacağını ayarlamamız gerekebilir. Bu duruma da "Processor Affinity" denmektedir. "thread" konusunda detaylarına değinilecektir. > "thread" hakkında genel bilgileri: "thread" ler "IO Yoğun" ve "CPU Yoğun" olmak üzere iki kategoriye ayrılmaktadır. "IO Yoğun" olanlar kendisine verilen "quanta" süresini çok az kullanıp hemen bloke olan "thread" lerdir. "CPU Yoğun" olanlar ise kendisine verilen "quanta" süresini büyük ölçüde kullanan "thread" lerdir. Dolayısıyla "CPU Yoğun" olanlar sistemi yavaşlatma potansiyeli taşırken, "IO Yoğun" olanlar böyle bir potansiyele sahip değildir. Çünkü birisi sürekli olarak işlemciyi çalıştırırken, diğeri hayatının çoğunu "wait-queue" da geçirmektedir. Fakat bu kavramlar insanlar tarafından uydurulmuş kavramlardır. İşletim sistemi açısından böyle kavramlar mevcut değildir. > Bir programın akışı sırasında iki nokta arası geçen zamanın ölçülmesi: * Örnek 1, Aşağıdaki programı "shell" vasıtasıyla "&" atomunu kullanarak "./sample &" biçiminde art arda çalıştırdığımız zaman göreceğiz ki ne kadar çok çalıştırırsak hesaplamanın süresi bi' o kadar da artacaktır. #include "stdio.h" #include "stdlib.h" #include int main(int argc, char** argv) { /* # INPUT # */ /* # OUTPUT # Total Work Hours: 19.100542 Total Duration: 20 */ clock_t clock_start, clock_stop; clock_start = clock(); for(long long i = 0; i < 10000000000; ++i) ; clock_stop = clock(); /* * Linux sistemlerde "thread" lerin bekleme zamanı dikkate alınmamaktadır eğer "clock" * ile ölçüm yapıyorsak. */ printf("Total Work Hours: %f\n", (double)(clock_stop - clock_start) / CLOCKS_PER_SEC); time_t time_start, time_stop; time_start = time(NULL); for(long long i = 0; i < 10000000000; ++i) ; time_stop = time(NULL); printf("Total Duration: %lld\n", (long long)(time_stop - time_start)); } * Örnek 2, Aşağıdaki programı "shell" üzerinden "time sample" biçiminde çalıştırırsak yine geçen zamanı hesaplamış olacağız. Eğer "time sample &" biçiminde peşpeşe çalıştırırsak yine hesap süresinin arttığını da göreceğiz. #include "stdio.h" #include "stdlib.h" #include int main(int argc, char** argv) { /* # INPUT # */ /* # OUTPUT # real 0m17,356s user 0m17,315s sys 0m0,012s */ for(long long i = 0; i < 10000000000; ++i) ; /* * Ekrandaki çıktılardan, * "real" olan gerçek duvar saatine göre geçen zamanı. 17 saniye. * "sys" olan "kernel" zamanı. 17 saniye * "user" olan ise "user" zamanı. 0 saniye. */ printf("Total Duration: %lld\n", (long long)(time_stop - time_start)); } > Hatırlatıcı Notlar: >> "NGROUPS_MAX" gibi bazı sembolik sabitleri için "Runtime Increasable Values" denmektedir. >> O(1) Scheduler: Eski Linux sistemlerinde tek bir "run-queueu" olduğundan bahsetmiştik. Boşa çıkan "core" a bir "thread" atanması için bu "run-queueu" nun kontrol edilmesi gerekmektedir çünkü sıradaki ilk "thread" i atarsak verimsiz bir çalışma yapabiliriz. Bunun da yegane sebebi bazı "thread" lerin evvelki "core" da çalışmaya devam etmesinin ya da önceliği yüksek olan "thread" lerin önce alınmasının getireceği faydadır. İşte hangi "thread" in boş olan ilk "core" a atanacağını bulan algoritmanın karmaşıklığı da O(1) karmaşıklığıdır. Herhangi bir döngü olmaksızın en uygun "thread" seçilmektedir. Detaylı bilgi için: https://en.wikipedia.org/wiki/O(1)_scheduler >> O(n) Scheduler: "n" kadar "thread" içeren "run-queue" dan en uygun "thread" in bulunması için bir döngü yardımıyla sıranın baştan sonra dolaşılmasıdır. Buradaki "n" ise "run-queue" içerisindeki eleman sayısını işaret etmektedir. Bu tip zamanlayıcıylarda eleman sayısı arttıkça geçecek zaman da "linear" olarak değişecektir. Detaylı bilgi için: https://en.wikipedia.org/wiki/O(n)_scheduler >> Bir "thread" in "quanta" süresinden sonra tekrardan aynı "core" de çalışmasının avantajları vardır çünkü o "thread" e ilişkin bir takım bilgiler o "core" a kopyalanmaktadır. >> Bir "thread" hayatı boyunca tipik 3 farklı durumdadır. Bunlardan birisi "Run", diğeri "Ready" ve sonuncusu "Wait". "Run" durumunda olan "thread" ler "run-queue" ya alınmış ve "quanta" süresi işlemekte olan "thread" lerdir. "quanta" süresinden sonra durumu "Ready" olacaktır ve bir sonraki "quanta" süresini bekleyecektir. Eğer "Run" durumundayken dışsal bir olay gerçekleşirse durumu artık "Wait" olacaktır. Dışsal olayın tamamlanmasından sonra yeniden "Ready" konumuna geçip bir sonraki "quanta" süresini bekleyecektir. En sonunda ise "thread" sonlanmaktadır.