> UNIX ve türevi sistemlerde "Daemon(Servis)" Programlarının Yazımı: Genellikle "User-mode" da çalışmak üzere geliştirilen programlardır. Arka planda sessiz sedasız çalışırlar. İsimleri son ek olarak "-d" harfini alır. Eğer başında da "-k" harfi varsa "kernel" a aittirler. "ps -e" komutu ile bunları görebiliriz. Bu programlar kullanıcılar ile terminal programı üzerinden etkileşime girmezler, bir "User Interface" kavramına sahip değildir. Bu programlar da pekala istenildiği zaman başlatılabilir, istenildiği zaman sonlandırılabilir. Bu tip programların çalıştırılması "sudo" ile yapılması da uygundur. Aslında bu tip programlar UNIX ve türevi sistemlerde "init package" içerisindeki bir takım özel "utility" tarafından başlatılmakta, sürdürülmekte ve sonlandırılmaktadır. Yani ilgili dağıtımlar, bu tür programları idare edebilmek için, özel komutlar bulundurabilmektedir. >> "init packages" : Yaygın olarak kullanılan "init package" türleri şunlardır; "sysvinit", "Upstart" ve "systemd". Bu paketler çeşitli proje grupları tarafından oluşturulmuşlardır. Bu paketlerden, >>> "sysvinit": Klasik System5'teki işlevleri yapan init paketidir. Linux uzun bir süre bu paketi kullanmıştır. Bu paketler ismine "run level" denilen bir "boot" seçeneği barındırırlar. Böylece sistem "boot" edilirken sadece ihtiyaç duyulan "Daemon" programları çalıştırmaktadır. >>> "Upstart" : 2006 yılında oluşturulmuştur ve 2010'ların ortalarına kadar (bazı dağıtımlarda hala) kullanılmaya yaygın biçimde kullanılmıştır. Artık geliştirilmesi durdurulmuştur. Fakat hala kullanılıyor olabilmektedir. Yine bu paket de "run level" özelliğini kullanır. >>> "systemd" : 2010 yılında oluşturulmuştur ve son yıllarda pek çok Linux dağıtımında kullanılmaya başlanmıştır. Bugün en yaygın kullanılan init paketi durumundadır. İçeriğinde "boot" sonrasında devreye girecek programlardan ve bazı komutlar barındırmaktadır. Komutlar genellikle son ek olarak "-ctl" ekini alır. Paketin en önemli komutu "systemctl" isimli komuttur. Çünkü bu komut adeta "systemd" paketinin ön yüzü konumundadır ve servis yönetim işleri temel olarak bu komut yoluyla yapılmaktadır. Bu paketteki diğer komutlar ise "/lib/systemd" dizini içerisinde yer alır. Öte yandan "systemd" paketi çalışmaya başladığında çeşitli konfigürasyon dosyalarına başvurmaktadır. Bu konfigürasyon dosyaları "/etc/systemd" dizininde bulunmaktadır. Bu dizinde yer alan ".conf" uzantılı dosyaların içeriği genel olarak "değişken=değer" çifti şeklindedir fakat yorum satırı halindedir. Sistem yöneticisi ilgili satırı yorum satırı olmaktan çıkartarak, o özelliği kullanabilir. Diğer yandan kendi "Daemon" programımızın "systemd" kontrolüne girmesini istiyorsak, ismine "unit file" denilen, bir dosya daha oluşturmamız gerekmektedir. Aslında sistem genelinde çeşitli amaçlarla oluşturulan başka "unit file" lar da mevcuttur. Bu dosyalar ve uzantıları ise aşağıdaki gibidir. Dosya İsmi Uzantısı "Service Unit File" ".service" "Socket Unit File" ".socket " "Slice Unit File" ".slice" "Mount Unit File" ".mount" "Automount Unit File" ".automount" "Target Unit File" ".target " "Timer Unit File" ".timer " "Path Unit File" ".path" "Swap Unit File" ".swap" Tipik olarak biz programcılar, kendi "Daemon" programlarımız için, ".service" uzantılı "Service Unit File" oluştururuz. Bu "unit file" lar ise tipik olarak "/lib/systemd/system" dizini içerisinde oluşturulur. Fakat varsayılan senaryoda sırasıyla kontrol edilecek dizinler ise aşağıdaki gibidir. "/lib/systemd/system" "/etc/systemd/system" "/run/systemd/system" "/usr/lib/systemd/system" "/usr/local/lib/systemd/system" Bu pakette de artık "run level" seçeneği yerine "Target Unit File" kullanılır. Dolayısıyla "run level" ismi yerine "target unit" ismi kullanılır. Konunun detaylarına "Linux Service Management Made Easy with systemd by Donald A. Tevault" kitabından bakabiliriz. Pekiyi bizler kendi "Daemon" programlarımız için "Service Unit File" nasıl oluştururuz? Minimalist bir biçimde ilgili dosya aşağıdaki içeriklere sahip olur. # mydaemon.service [Unit] Description=Mydaemon Unit [Service] Type=forking ExecStart=/usr/bin/mydaemond [Install] WantedBy=multi-user.target Buradaki, -> "#" işareti, o satırı yorum satırı haline getirir. -> "Description", bizim "unit file" ı temsil eden bir yazıdır. -> "Type", bizim "Daemon" programımızın nasıl çalıştırılacağını belirtir. Buradaki "forking", normal fork mekanizmasıyla çalıştırma, anlamına gelmektedir. -> "ExecStart", bizim "Daemon" programımızın nerede olduğunu belirtmektedir. "Daemon" programlarımızı tipik olarak "/usr/bin" dizini ya da "usr/local/bin" içerisinde bulundurmalıyız. -> "WantedBy", hangi "Target Unit File" ile sistem "boot" edildiğinde bizim "Daemon" programının yükleneceğini belirtir. Buradaki "multi-user.target" isimli "Target Unit File", klasik ve tipik olan, boot işlemini belirtmektedir. Biz böylece minimalist bir ".service" dosyası oluşturduk. Dökümantasyon için "man systemd.service" sayfasına başvurmalıyız. Pekiyi servis yönetimini nasıl gerçekleştireceğiz? Bunun için "systemctl" komutu bulundurulmuştur. Bu komutu ise, "systemctl enable my_daemon" biçiminde kullanabiliriz. Buradaki, -> "my_daemon" ise bizim "Daemon" programımızın ismini belirtmektedir. Spesifik olarak uzantıyı da yazmamıza gerek yoktur. -> "enable" ise "Daemon" programımızın bir sonraki "boot" işleminden sonra başlatılacağını belirtmektedir. Eğer hemen şimdi başlatılmasını istiyorsak, "--now" seçeneğini de kullanmalıyız. Örneğin, "systemctl enable my_daemon --now". Bu komuta ilişkin diğer seçenekler ise şunlardır; -> "disable" : "Daemon" programımızın "boot "sırasında devreye sokulmasını istemiyorsak bu komutu kullanmalıyız. Bu seçeneğin kullanım biçimi, "systemctl disable my_daemon" biçimindedir. -> "start" : "Daemon" programı çalıştırmak için kullanılır. Örneğin, "stop" durumdakini tekrar çalıştırmak için, "systemctl start my_daemon" komutunu kullanabiliriz. -> "stop" : "Daemon" programı durdurmak için kullanılır. Bu seçeneğin kullanım biçimi, "systemctl stop my_daemon" şeklindedir. Durdurma işlemi sırasında varsayılan ayarlarda "SIGTERM" sinyali gönderilir, "systemd" paketi tarafından. Eğer "Daemon" program hala sonlanmamışsa, bu sefer ona "SIGKILL" sinyali gönderilir. -> "status" : "Daemon" programımızın durumunu öğrenmek için kullanılır. Bu seçeneğin kullanım biçimi, "systemctl status [my_daemon]" biçimindedir. Eğer herhangi bir "Daemon" program ismi vermezsek, bütün "Daemon" programlar gösterilecektir. Bu komut için "sudo" dememize gerek yoktur. -> "is-enabled" : "Daemon" programımızın "boot" zamanında devreye girip girmeyeceğini, "status" komutunun yanı sıra, bu komut ile de anlayabiliriz. Komutun kullanım biçimi, "systemctl is-enabled my_daemon" biçimindedir. -> "restart" : "Daemon" programımızı önce durdurur, sonra tekrar çalıştırır. Bu seçeneğin kullanım biçimi, "systemctl restart my_daemon" şeklindedir. -> "reload" : Bizler bir "unit" dosyası üzerinde değişiklik yaptığımızda, "systemd" paketi bütün "unit file" ları inceleyerek bir ağaç yapısı oluşturduğundan ve bu yapının da güncellemeden dolayı yeniden oluşturulması gerektiğinden, bu komutu çağırmalıyız. Bu seçeneğin kullanım biçimi, "systemctl daemon-reload" şeklindedir. Bu komutun da parametresiz olduğuna dikkat ediniz. Diğer yandan "systemd" paketlerinde; bir alt prosesin üst prosesi sonlandığında, onun üst prosesi artık "init" prosesi OLMAZ. Bu pakete göre üst proses artık "systemd" OLUR. Sistemimizde hangi "init" paketinin kullanıldığını öğrenmek için, sudo ls -l /proc/1/exe komutunu çalıştırabiliriz. Dolayısıyla ekrana çıkan, lrwxrwxrwx 1 root root 0 Kas 11 11:45 /proc/1/exe -> /usr/lib/systemd/systemd çıktı incelendiğinde görüleceği üzere sistemimizde "systemd" paketi kullanılmaktadır. Yine benzer şekilde, cat /proc/1/status komutunu ya da ps -p 1 (ya da "ps -p 1 -o comm=") komutunu çalıştırarak da öğrenebiliriz. Bir "Daemon" program yazabilmek için öncelikle onun terminal programı ile kesilmesi gerekmektedir. Komut satırından "&" ile bir programı çalıştırmak, onun terminal ile BAĞLANTISINI KESMEYECEKTİR. Bir "Daemon" programının yazılması, şu aşamalardan geçilerek mümkündür: -> İlk adım olarak; bu tür servis programları, bir dosya açmak istediklerinde, tam olarak belirlenen haklarla bunu gerçekleştirebilmelidir. Dolayısıyla ilk olarak bu tip programların "umask" değerini sıfıra çekiyoruz. Şöyleki; umask(0); -> İkinci adım olarak; terminalle olan ilişkimizin sonlandırılması gerekmektedir. Bunu "setsid" fonksiyon çağrısı ile gerçekleştirebiliriz. Anımsanacağı üzere bu fonksiyon çağrıldığında yeni bir Oturum ve yeni bir proses grubu oluşturmakta, prosesimizi de bu proses grubunun ve Oturumun lideri yapmaktaydı. Ancak biz kendi prosesimizi "shell" programı üzerinden çağırdığımız için, aslında prosesimiz için halihazırda bir proses grubu oluşturulacak ve bizler de grubun lideri yapılacağız. Dolayısıyla bu noktada "setsid" çağrısı başarısız olacaktır. Çünkü çağrının başarılı olabilmesi için o prosesin herhangi bir proses grup lideri olmaması gerekir. Bu problemi çözmek için, "setsid" çağrısından evvel, bir kez "fork" yapıp üst prosesimizi sonlandırmamız gerekecektir. Şöyleki; pid_t pid; if ((pid = fork()) == -1) exit_sys("fork"); if (pid != 0) _exit(EXIT_SUCCESS); Bu aşamada kalırsak, prosesimizin bulunduğu proses grubu Öksüz Proses Grubu olacaktır. Artık "setsid" çağrısını yapabiliriz. Şöyleki; if (setsid() == -1) _exit(EXIT_FAILURE); -> Üçüncü adım olarak; "Daemon" programların çalışma dizinlerinin ("Current Working Directory") sağlam bir dizin olması tavsiye edilir. Aksi takdirde o dizin silinirse programımızın çalışması bozulabilir. Bu nedenle "Daemon" programların çoğu Kök Dizin'i (silinemeyeceği için) çalışma dizini yapmaktadır. Tabii bu zorunlu değildir. Kök Dizin yerine, varlığı garanti edilmiş, herhangi bir dizin de çalışma dizini yapılabilir. Şöyleki: if (chdir("/") == -1) _exit(EXIT_FAILURE); -> Dördüncü adım olarak; Bir sonraki aşamada o ana kadar açılmış olan bütün betimleyicileri kapatması gerekmektedir. Buna "0", "1" ve "2" numaralı betimleyiciler de dahildir. Her ne kadar terminal ile olan ilişkimizi sonlandırsak bile prosesimiz "0", "1" ve "2" numaralı betimleyicileri hala kullanabilir. Dolayısıyla terminale hala bir şeyler yazabiliriz. O ana kadar açık olan betimleyicileri kapatmanın kolay bir yolu olmadığından, ilk olarak dosya betimleyici tablosunun uzunluğunu elde etmeli ve bu tablodakileri sırasıyla kapatmalıyız. Halihazırda kapalı olanlara tekrar kapatma işleminin uygulanması programımızın çökmesine neden OLMAYACAKTIR. İlgili tablonun toplam uzunluğunu da "sysconf" çağrısında "_SC_OPEN_MAX" sembolik sabitini kullanarak ya da "getrlimit" çağrısında "RLIMIT_NOFILE" sembolik sabiti kullanarak elde edebiliriz. Şöyleki; long maxfd; if ((maxfd = sysconf(_SC_OPEN_MAX)) == -1) _exit(EXIT_FAILURE); for (long i = 0; i < maxfd; ++i) close(i); -> Beşinci adım olarak; Her ne kadar açık olan betimleyicilerin hepsinin kapatılması önerilse de, "0", "1" ve "2" numaralı betimleyicilerin "/dev/null" aygıtına yönlendirilmesi daha uygun olur. Çünkü her ne kadar bizim "Daemon" programımız betimleyicileri kullanmasa bile çağıracağı fonksiyonlar bu betimleyicileri kullanabilir. Dolayısıyla o fonksiyonlar probleme yol açabilir. Bunu da şöyle gerçekleştirebiliriz; int fd; if ((fd = open("/dev/null", O_RDONLY)) == -1) // fd is guaranteed to be "0" _exit(EXIT_FAILURE); if (dup(fd) == -1 || dup(fd) == -1) // now descriptor "1" and "2" redirected to "/dev/null" _exit(EXIT_FAILURE); -> Altıncı adım olarak(opsiyonel); "Daemon" programlar çoğu zaman tek kopya halinde çalıştırılır. Yani aynı "Daemon" programının birden fazla kez çalıştırılması sorunlara yol açabilmektedir. Bunun gerçekleştirimini dosya kilitleme bölümünde Bütünsel Kilitleme başlığında işlemiştik. (kilit vurulacak dosya genel olarak "/run" dizini içerisinde, ".pid" uzantısı ile oluşturulur. Bu dizinde işlem yapabilmek için prosesimizin uygun önceliğe sahip olması gerekir.) -> Yedinci adım olarak; "Daemon" haline getirilmiş programı sonlandırmak için tipik olarak "SIGTERM" sinyali kullanılır. Çünkü bu sinyal, "SIGKILL" sinyaline nazaran, "handler" edilebilir bir sinyaldir. İşte "Daemon" programımız bir takım sonlandırma işlemleri de yapacaksa, bunu "SIGTERM" sinyalinin işlendiği sırada gerçekleştirebilir. Zaten işletim sistemleri kapanırken yine "SIGTERM" sinyali göndereceğinden, en kötü olasılıkla, sonlandırma işlemleri yine o noktada yapılacaktır eğer ilgili sinyal "set" edilmişse. Burada sinyalin "set" işleminin "Daemon" oluşturma işleminden önce de yapabiliriz, sonra da. Diğer yandan "Daemon" program oluştururken şu husulara da dikkat etmeliyiz: -> Bazı işletim sistemleri "SIGTERM" sinyalinin gönderiminden beş saniye sonra "SIGKILL" sinyali gönderdiğinden, sonlandırma işlemlerinin yapılması yavaş olmayacak biçimde, "SIGTERM" sinyalinin işlenmesi sırasında beklenir. -> "Daemon" programlar başlatılırken bir takım "configuration" dosyalarındaki yönergelere bakılarak başlatılmaktadır. Bu konfigürasyon dosyaları ise genelde "/etc" dizini içerisinde, ".conf" uzantısı ile bulunurlar. İş bu konfigürasyon dosyalarının okunma sırası "Daemon" oluşturmadan evvel olabileceği gibi "Daemon" oluşumundan sonra da olabilir. -> Terminalle ilişkimiz koptuğu için hata mesajlarının yazımına dikkat etmeliyiz. Bunun için genellikle "log" dosyaları kullanılır ki bu konuya ileride değineceğiz. -> "Daemon" programların "reinit." edilmesi de sık karşılaşılan bir durumdur. Yani konfigürasyon dosyasını okuyup çalışan "Daemon" programının, ilgili konfigürasyon dosyasını güncelledikten sonra, konfigürasyon dosyasını yeniden okuyup çalışmasını sağlatabiliriz. Genel olarak bu işlem için "SIGHUP" sinyalini kullanırız. Her ne kadar bu sinyal genel olarak terminal aygıt sürücüsü, "shell" programları tarafından kullanılıyor olsa bile, bizim programımızın terminal ile ilişkisi kesildiğinden "SIGHUP" sinyali "Daemon" programlar için ıskarta edilmiş bir sinyal durumundadır. Şimdi de bu yazılanları anlatan "Daemon" program(lar) oluşturalım: * Örnek 1.0, Bu örneği bir şablon program olarak da kullanabiliriz. #include #include #include #include #include #include #include #include #define DEF_FDT_SIZE 4096 #define LOCK_FILE_PATH "/run/mydaemond.pid" void sigterm_handler(int signo); void exit_sys(const char *msg); int main(void) { // Yedinci Adım: struct sigaction sa; sa.sa_handler = sigterm_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGTERM, &sa, NULL) == -1) exit_sys("sigaction"); // Altıncı Adım(opsiyonel): int fd; if ((fd = open(LOCK_FILE_PATH, O_RDWR|O_CREAT, S_IRUSR|S_IWUSR)) == -1) { /* log mekanizması ile mesaj oluşturulabilir */ _exit(EXIT_FAILURE); } if (flock(fd, LOCK_EX|LOCK_NB) == -1) { if (errno == EWOULDBLOCK) { /* log mekanizması ile mesaj oluşturulabilir */ } else { /* log mekanizması ile mesaj oluşturulabilir */ } _exit(EXIT_FAILURE); } // Birinci Adım: Prosesin "umask" değerini sıfıra çekiyoruz. umask(0); // İkinci Adım: Yeni bir Oturum ve Proses Grubu oluşturmadan evvel uygun şartları sağlıyoruz. pid_t pid; if ((pid = fork()) == -1) exit_sys("fork"); if (pid != 0) _exit(EXIT_SUCCESS); // İkinci Adım: Artık yeni bir Oturum ve Proses Grubu oluşturabiliriz. if (setsid() == -1) _exit(EXIT_FAILURE); // Üçüncü Adım: "Current Working Directory" konumunu silinmeyeceğinden emin olduğumuz bir dizin olarak belirliyoruz. if (chdir("/") == -1) _exit(EXIT_FAILURE); // Dördüncü Adım: Açık olan dosya betimleyicilerinin hepsini kapatıyoruz. errno = 0; long maxfd; if ((maxfd = sysconf(_SC_OPEN_MAX)) == -1) // Eğer tablonun uzunluk bilgisi tanımlı değilse, fonksiyon "-1" ile geri döner... if (errno == 0) //...fakat "errno" değerini değiştirmez... maxfd = DEF_FDT_SIZE; //...Bu senaryo için biz bir uzunluk tayin ediyoruz. else _exit(EXIT_FAILURE); for (long i = 0; i < maxfd; ++i) close(i); // Beşinci Adım: İlk üç betimleyiciyi "/dev/null" aygıtına yönlendiriyoruz. int fd0, fd1, fd2; if ((fd0 = open("/dev/null", O_RDONLY)) == -1) _exit(EXIT_FAILURE); if ((fd1 = dup(fd0)) == -1) _exit(EXIT_FAILURE); if ((fd2 = dup(fd0)) == -1) _exit(EXIT_FAILURE); if (fd0 != 0 || fd1 != 1 || fd2 != 2) _exit(EXIT_FAILURE); // Artık "Daemon" programımız kendi işini görecektir. pause(); return 0; } void sigterm_handler(int signo) { /* cleanup processing */ } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 1.1, Pekala yukarıdaki şablon programı fonksiyonlar oluşturarak da kullanabiliriz. #include #include #include #include #include #include #include #include #define DEF_FDT_SIZE 4096 #define LOCK_FILE_PATH "/run/mydaemond.pid" void sigterm_handler(int signo); void end_daemon(void); void check_instance(void); void make_daemon(void); void exit_sys(const char *msg); int main(void) { check_instance(); // Altıncı Adım(opsiyonel) make_daemon(); // Birinci,..., Beşinci Adım end_daemon(); // Yedinci Adım pause(); return 0; } void sigterm_handler(int signo) { /* cleanup processing */ } void end_daemon(void) { struct sigaction sa; sa.sa_handler = sigterm_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGTERM, &sa, NULL) == -1) exit_sys("sigaction"); } void check_instance(void) { int fd; if ((fd = open(LOCK_FILE_PATH, O_RDWR|O_CREAT, S_IRUSR|S_IWUSR)) == -1) { /* log mekanizması ile mesaj oluşturulabilir */ _exit(EXIT_FAILURE); } if (flock(fd, LOCK_EX|LOCK_NB) == -1) { if (errno == EWOULDBLOCK) { /* log mekanizması ile mesaj oluşturulabilir */ } else { /* log mekanizması ile mesaj oluşturulabilir */ } _exit(EXIT_FAILURE); } } void make_daemon(void) { umask(0); pid_t pid; if ((pid = fork()) == -1) exit_sys("fork"); if (pid != 0) _exit(EXIT_SUCCESS); if (setsid() == -1) _exit(EXIT_FAILURE); if (chdir("/") == -1) _exit(EXIT_FAILURE); errno = 0; long maxfd; if ((maxfd = sysconf(_SC_OPEN_MAX)) == -1) if (errno == 0) maxfd = DEF_FDT_SIZE; else _exit(EXIT_FAILURE); for (long i = 0; i < maxfd; ++i) close(i); int fd0, fd1, fd2; if ((fd0 = open("/dev/null", O_RDONLY)) == -1) _exit(EXIT_FAILURE); if ((fd1 = dup(fd0)) == -1) _exit(EXIT_FAILURE); if ((fd2 = dup(fd0)) == -1) _exit(EXIT_FAILURE); if (fd0 != 0 || fd1 != 1 || fd2 != 2) _exit(EXIT_FAILURE); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, #include #include #include #include #include #include #include #include void read_config(void); void check_instance(void); void sigterm_handler(int signo); void sighup_handler(int signo); void exit_sys(const char *msg); #define DEF_FDT_SIZE 4096 #define LOCK_FILE_PATH "/run/mydaemond.pid" #define CONFIG_FILE "/etc/mydaemond.conf" void make_daemon(void) { pid_t pid; long maxfd; int fd0, fd1, fd2; umask(0); if ((pid = fork()) == -1) exit_sys("fork"); if (pid != 0) _exit(EXIT_SUCCESS); if (setsid() == -1) _exit(EXIT_FAILURE); if (chdir("/") == -1) _exit(EXIT_FAILURE); errno = 0; if ((maxfd = sysconf(_SC_OPEN_MAX)) == -1) if (errno == 0) maxfd = DEF_FDT_SIZE; else _exit(EXIT_FAILURE); for (long i = 0; i < maxfd; ++i) close(i); if ((fd0 = open("/dev/null", O_RDONLY)) == -1) _exit(EXIT_FAILURE); if ((fd1 = dup(fd0)) == -1) _exit(EXIT_FAILURE); if ((fd2 = dup(fd0)) == -1) _exit(EXIT_FAILURE); if (fd0 != 0 || fd1 != 1 || fd2 != 2) _exit(EXIT_FAILURE); } int main(void) { struct sigaction sa; int fd; make_daemon(); check_instance(); read_config(); sa.sa_handler = sigterm_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGTERM, &sa, NULL) == -1) { /* log mekanizması yoluyla hata mesajı oluşturulabilir */ _exit(EXIT_FAILURE); } sa.sa_handler = sighup_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGHUP, &sa, NULL) == -1) { /* log mekanizması yoluyla hata mesajı oluşturulabilir */ _exit(EXIT_FAILURE); } /* ... */ pause(); return 0; } void read_config(void) { /* ... */ } void check_instance(void) { int fd; if ((fd = open(LOCK_FILE_PATH, O_RDWR|O_CREAT, S_IRUSR|S_IWUSR)) == -1) { /* log mekanizması ile mesaj oluşturulabilir */ _exit(EXIT_FAILURE); } if (flock(fd, LOCK_EX|LOCK_NB) == -1) { if (errno == EWOULDBLOCK) { /* log mekanizması ile mesaj oluşturulabilir */ } else { /* log mekanizması ile mesaj oluşturulabilir */ } _exit(EXIT_FAILURE); } } void sigterm_handler(int signo) { /* cleanup processing */ } void sighup_handler(int signo) { read_config(); /* read_config asenkron sinyal güvenli bir fonksiyon olmalı */ } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Pekiyi yukarıda anlatılan "log" dosyası, bir diğer deyişle "log" mekanizması nedir? Terminal bağlantısı bulunmayan "Daemon" programlarının ya da "kernel" modüllerinde, hata veya diğer mesajların terminal ekranına yazdırılması, pek mümkün olmayabilir(ya da uygun olmayabilir). İşte böylesi programlar için bir takım mesajların kullanıcıya ulaştırılabilmesi için "log" mekanizması geliştirilmiştir. UNIX ve türevi sistemlerde "kernel" tarafından desteklenen kapsamlı bir "log" mekanizması mevcuttur. Böylece farklı kaynaklardan gelen mesajların biriktirilip saklanması mümkündür. Bu mekanizmaya genel olarak "syslog" ismi verilir ve gösterimine ise "The Linux Programming Environment by Michale Kerrisk" kitabından bakılabilir. Linux sistemlerindeki "syslog" mekanizmasını açıklayacak olursak; Bu mekanizmanın ana aktörü, yani üst seviye önemli bir bileşeni, "syslogd" isimli "Daemon" bir programdır. Bu program "user mode" da çalışır, birden fazla kaynaktan gelen mesajları alır, organize eder ve hedef dizine/dosyaya bunları yazar. Hedef dizin/dosya pekala değiştirilebilir. Bu programın dinlediği kaynaklar ise şunlardır: Yerel kullanımlar için "/dev" dizini içerisindeki "log" dosyası, uzak kullanımlar için "UDP 514" portu. Aslında buradaki "/dev/log" dosyasına iki farklı aktör yazma yapmaktadır. Bunlardan biri normal kullanıcılar, diğeri ise aygıt sürücüler ve "kernel" modüllerdir. Normal kullanıcılar "syslog" isimli POSIX fonksiyonunu kullanırken, aygıt sürücü ve "kernel" moduülleri ise "klogd" isimli "kernel mode" da çalışan bir "Daemon" programı kullanırlar. "user mod" da "log" işlemleri için üç farklı fonksiyon kullanılır. Bunlar "openlog", "syslog" ve "closelog" isimli POSIX fonksiyonlarıdır. Bu fonksiyonlardan, >> "openlog" : "log" mekanizmasını başlatır. Fonksiyonun prototipi aşağıdaki gibidir. #include void openlog(const char *ident, int logopt, int facility); Fonksiyonun birinci parametresi "log" mesajlarında görüntülenecek program ismini belirtir. Genellikle programın ismi bu parametreye argüman olarak geçilir. Linux sistemlerinde bu parametreye "NULL" değerinin geçilmesi, program isminin geçilmesi anlamına gelir. Ancak POSIX standartlarına göre "NULL" geçme senaryosu tanımlanmamıştır. Fonksiyonun ikinci parametresi ise, "LOG_PID", "LOG_CONS", "LOG_NDELAY", "LOG_ODELAY" ve "LOG_NOWAIT" sembolik sabitlerinin "bit-wise OR" işlemine sokulması ile elde edilen değeri alır. Bu sembolik sabitlerden, -> "LOG_PID" : "log" mesajında prosesin ID değerinin de ekleneceği anlamına gelir. -> "LOG_CONS" : "log" mesajlarının aynı zamanda "Default Console" ekranına da yazdırılacağı anlamına gelir("/dev/console"). -> "LOG_NDELAY" : "log" sisteminin hemen açılacağı anlamına gelir. -> "LOG_ODELAY" : "log" sisteminin ilk "log" işlemi yapıldığında açılacağı anlamına gelir. Varsayılan davranış budur. -> "LOG_NOWAIT" : "child process" söz konusu olduğunda, "log" işlemi için bu proseslerin hayata getirilmelerinin beklenmeyeceği anlamındadır. Pekala bu parametreye "0" da geçebiliriz. Ancak tipik olarak "LOG_PID" sabiti geçilir. Fonksiyonun üçüncü parametresi ise "log" mesajını yollayan prosesin kim olduğuna dair bilgi vermek için kullanılır. Bu parametreye, "LOG_USER", "LOG_KERN", "LOG_DAEMON", "LOG_LOCAL0...7" sembolik sabitlerinden birisini alır. Bu sembolik sabitlerden, -> "LOG_USER" : "log" mesajının normal prosesler tarafından gönderildiği anlamındadır. -> "LOG_KERN" : "log" mesajının "kernel" tarafından gönderildiği anlamındadır. Bu sembolik sabit POSIX'te mevcut DEĞİLDİR. -> "LOG_DAEMON" : "log" mesajının bir sistem "Daemon" programı tarafından gönderildiği anlamındadır. Bu sembolik sabit POSIX'te mevcut DEĞİLDİR. -> "LOG_LOCAL0...7" : "log" mesajının özel "log" kaynakları tarafından gönderildiği anlamındadır. Pekala bu parametreye "0" da geçebiliriz. Bu durumda "LOG_USER" geçilmiş gibi olur. Bu fonksiyonun başarısı kontrol edilememektedir. Başarısızlık durumu diğer "log" fonksiyonları ile anlaşılmaktadır. Bu fonksiyonun çağrılması mekanizmanın kullanılabilmesi için zorunlu DEĞİLDİR. Bu durumda barsayılan değerler kullanılacaktır. >> "syslog" : Bu fonksiyon "log" işlemlerini gerçekleştiren fonksiyondur. Asıl fonksiyon bu fonksiyondur. Fonksiyonun prototipi aşağıdaki gibidir. #include void syslog(int priority, const char *format, ...); Fonksiyonun birinci parametresi mesajın önem derecesini belirtir ve şu sembolik sabitlerden birisini alır: "LOG_EMERG", "LOG_ALERT", "LOG_CRIT", "LOG_ERR", "LOG_WARNING", "LOG_NOTICE", "LOG_INFO" ve "LOG_DEBUG" Bu sembolik sabitler de yine hata mesajlarında görünür olup, en çok kullanılanları şunlardır; -> "LOG_ERR" : Hata mesajları için kullanılır. -> "LOG_WARNING" : Uyarı mesajları için kullanılır. -> "LOG_INFO" : Genel bilgilendirme mahiyetinde kullanılır. Fonksiyonun diğer parametreleri "printf" fonksiyonunun parametreleri gibidir. Bu fonksiyonun da başarısı kontrol edilememektedir. Öte yandan "openlog" fonksiyonunun çağrılması bir zorunluluk olmadığından, "openlog" fonksiyonunun üçüncü parametresi ile bu fonksiyonun birinci parametresini kombine edebiliriz. >> "closelog" : "log" mekanizmasını sonlandırır, kapatır. Bu fonksiyon, eğer "log" mekanizması henüz başlatılmadıysa, bir şey yapmamaktadır. Fonksiyonun prototipi aşağıdaki gibidir. #include void closelog(void); Öte yandan "log" mekanizmasında kullanılan varsayılan hedef dizin, Linux sistemlerinde, "/var/log" dizininde bulunan "syslog" dosyasıdır. Bu dosya disk tabanlı olmadığından, içeriğindeki bilgiler "reboot" işlemi sonrası silinmektedir. Bu dosyanın büyüklüğü artabileceği için, arka planda kuyruk sistemine benzer bir mekanizma kullanılır. Dolayısıyla bir müddet sonra evvelce yazılmış mesajlar kaybolabilmektedir. Pekala bizler yazma işleminin yapılacağı hedef dizini de değiştirebiliriz. Yukarıda da belirtildiği üzere Linux sistemlerinde bu yazma işlemi için "syslogd" isimli "Daemon" program kullanılır. Fakat yeni Linux sürümlerinde "rsyslogd" isimli "Daemon" kullanılmaktadır. Bu iki "Daemon" program ise hedef dizinin ne olduğuna karar verirken sırasıyla "/etc/syslog.conf" ya da "/etc/rsyslog.conf" dosyalarına bakar. İşte bu dosyalarda bilgi bulamadığında, varsayılan dizin olan "/var/log/syslog" dosyasını seçer. Dolayısıyla hedef dizinin hangisi olmasını istiyorsak, "/etc/syslog.conf" ya da "/etc/rsyslog.conf" dosyalarında değişiklik yapmalıyız. Pekiyi bizler "log" dosyalarını nasıl inceleyeceğiz? Esasında, dosyanın sonunu görebilmek için "tail" kabuk komutunu kullanabiliriz. Anımsanacağı üzere bu komut, dosyanın sonundaki son on satırı gösterir. "-n" seçeneği ile daha fazla satırı da görüntüleyebiliriz. Bu komuta ek olarak bir takım "utility" araçlar da bulundurulmuştur. Örneğin lnav, glogg, ksystemlog gibi. Yine "systemd" isimli "init" paketindeki "systemctl" ve "journalctl" komutlarıyla da görüntüleme yapabiliriz. Şimdi de "log" mekanizmasına ilişkin örnekleri inceleyelim. * Örnek 1, "log" mekanizmasına ilişkin temel bir örnek. #include #include int main(void) { openlog("sample", LOG_PID, LOG_USER); syslog(LOG_INFO, "This is a test..."); closelog(); return 0; } * Örnek 2, Bir "Daemon" program içerisinde "log" mekanizmasının kullanımına ilişkin örnek. #include #include #include #include #include #include #include #include #include #include void read_config(void); void check_instance(void); void sigterm_handler(int signo); void sighup_handler(int signo); void exit_daemon(const char *msg); #define DEF_FDT_SIZE 4096 #define LOCK_FILE_PATH "/run/mydaemond.pid" #define CONFIG_FILE "/etc/mydaemond.conf" void make_daemon(void) { pid_t pid; long maxfd; int fd0, fd1, fd2; umask(0); if ((pid = fork()) == -1) exit_daemon("fork"); if (pid != 0) _exit(EXIT_SUCCESS); if (setsid() == -1) exit_daemon("setsid"); if (chdir("/") == -1) exit_daemon("chdir"); errno = 0; if ((maxfd = sysconf(_SC_OPEN_MAX)) == -1) if (errno == 0) maxfd = DEF_FDT_SIZE; else exit_daemon("sysconf"); for (long i = 0; i < maxfd; ++i) close(i); if ((fd0 = open("/dev/null", O_RDONLY)) == -1) exit_daemon("open"); if ((fd1 = dup(fd0)) == -1) exit_daemon("dup"); if ((fd2 = dup(fd0)) == -1) exit_daemon("dup"); if (fd0 != 0 || fd1 != 1 || fd2 != 2) { syslog(LOG_ERR, "invalid file descriptors"); _exit(EXIT_FAILURE); } } int main(void) { struct sigaction sa; int fd; openlog("mydaemond", LOG_PID, LOG_USER); make_daemon(); check_instance(); read_config(); sa.sa_handler = sigterm_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGTERM, &sa, NULL) == -1) exit_daemon("sigaction"); sa.sa_handler = sighup_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGHUP, &sa, NULL) == -1) exit_daemon("sigaction"); syslog(LOG_INFO, "ok, daemon is running"); for (;;) pause(); closelog(); return 0; } void read_config(void) { syslog(LOG_INFO, "mydaemon is reading configuration file..."); } void check_instance(void) { int fd; if ((fd = open(LOCK_FILE_PATH, O_RDWR|O_CREAT, S_IRUSR|S_IWUSR)) == -1) exit_daemon("open"); if (flock(fd, LOCK_EX|LOCK_NB) == -1) { if (errno == EWOULDBLOCK) { syslog(LOG_ERR, "Only one instance of this daemon can be run..."); _exit(EXIT_FAILURE); } exit_daemon("flock"); } } void sigterm_handler(int signo) { syslog(LOG_INFO, "mydaemond is terminating..."); _exit(EXIT_SUCCESS); } void sighup_handler(int signo) { syslog(LOG_INFO, "mydaemond got SIGHUP and read config file..."); read_config(); /* read_config asenkron sinyal güvenli bir fonksiyon olmalı */ } void exit_daemon(const char *msg) { syslog(LOG_ERR, "%s: %s", msg, strerror(errno)); _exit(EXIT_FAILURE); }