TEKNİK YAZI / Backend Sistemleri

Daemon Nedir? Arka Planda Çalışan Güçlü Yardımcılar

Daemon'lar arka planda sessizce çalışan sistem programlarıdır. Bu yazıda daemon'ların ne olduğunu ve nasıl çalıştığını öğreneceksiniz.

Daemon’lar, kullanıcı arayüzü olmadan arka planda çalışan ve ağ isteği, zamanlanmış görev ya da sistem olayı gibi işleri sürekli yöneten süreçlerdir. Linux ve Unix benzeri sistemlerde web sunucusundan günlük servisine kadar pek çok temel bileşen bu çalışma modelini kullanır.

Daemon Nedir?

Daemon (telaffuz: “day-mon”), arka planda çalışan sistem programlarına verilen isimdir. Bu programlar genellikle sistem başladığında otomatik olarak çalışmaya başlar ve sürekli aktif kalırlar.

Adlandırma tarihsel olarak Maxwell’in daimon kavramına bağlanır. Teknik açıdan önemli olan isim değil; sürecin terminal oturumundan bağımsız çalışması, yaşam döngüsünün bir servis yöneticisi tarafından izlenmesi ve kontrollü biçimde sonlandırılabilmesidir.

Daemon’lar Ne İş Yapar?

Daemon’lar sisteminizde bir sürü önemli görevi yerine getirir:

Web Sunucuları

Ağ Servisleri

Sistem Servisleri

Daemon Özellikleri

Bir programın daemon olabilmesi için bazı özellikler taşıması gerekir:

  1. Terminale bağlı değildir: Denetleyen terminali (controlling terminal) yoktur; bu yüzden terminal kapandığında gönderilen SIGHUP sinyalinden etkilenmez. ps çıktısında TTY sütununun ? görünmesi bunun işaretidir.
  2. Parent’ı PID 1’dir: Süreçlerin parent’sız olması mümkün değildir. Kendisini başlatan süreç çıktığında çekirdek onu yeniden atar (reparenting): en yakın child subreaper varsa ona, yoksa PID 1’e. Bu devrin amacı, süreç sona erdiğinde onu wait() ile toplayacak ve zombi olarak kalmasını önleyecek bir sorumlu bırakmaktır.
  3. Kendi oturumunda çalışır: Elle daemonize edilen bir süreç setsid() ile yeni bir oturum açar. Systemd altında bu adım uygulamanın değil servis yöneticisinin sorumluluğundadır; süreç kendi cgroup’una yerleştirilir.
  4. Uzun ömürlüdür: Tek bir işi yapıp çıkmaz; durdurulana veya sistem kapanana kadar çalışır.
  5. Günlük yazar: Elle yönetilen daemon’lar genellikle /var/log/ altına yazar; systemd altında standart çıktı doğrudan journald tarafından toplanır.

Daemon İsimlendirmesi

Daemon’ların isimlendirilmesinde bir gelenek vardır - çoğunun sonunda ‘d’ harfi bulunur:

Bu ‘d’ harfi “daemon” kelimesinden gelir ve bu programların arka plan servisi olduğunu gösterir.

Daemon’ları Nasıl Yönetiyoruz?

Systemd ile (Günümüz Linux’larda)

# Servis durumunu kontrol et
sudo systemctl status nginx

# Servisi başlat
sudo systemctl start nginx

# Servisi durdur
sudo systemctl stop nginx

# Servisin otomatik başlamasını sağla
sudo systemctl enable nginx

# Servisin otomatik başlamasını engelle
sudo systemctl disable nginx

Init.d ile (Eski sistemlerde)

# Servis durumunu kontrol et
sudo service nginx status

# Servisi başlat
sudo service nginx start

# Servisi durdur
sudo service nginx stop

# Servisi yeniden başlat
sudo service nginx restart

Elle Daemonize Etme: Klasik Yöntem

Servis yöneticileri yaygınlaşmadan önce bir programın daemon hâline gelmesi programın kendi sorumluluğundaydı. Aşağıdaki örnek bu klasik adımları içerir. Bugün yeni kod yazarken bu adımlara gerek yoktur; yine de mevcut sistemlerde sık karşılaşıldığı ve her adımın somut bir sebebi olduğu için bilinmesi gerekir.

#!/usr/bin/env python3
import os
import sys
import time
import signal

class SimpleDaemon:
    def __init__(self, pidfile):
        self.pidfile = pidfile

    def daemonize(self):
        # İlk fork
        try:
            pid = os.fork()
            if pid > 0:
                sys.exit(0)  # Parent process çık
        except OSError as e:
            sys.stderr.write(f"Fork #1 failed: {e}")
            sys.exit(1)

        # Yeni session oluştur
        os.setsid()

        # İkinci fork
        try:
            pid = os.fork()
            if pid > 0:
                sys.exit(0)  # İkinci parent process çık
        except OSError as e:
            sys.stderr.write(f"Fork #2 failed: {e}")
            sys.exit(1)

        # Working directory'yi değiştir
        os.chdir("/")

        # File descriptors'ları kapat
        os.umask(0)

        # Standard input/output'ları yönlendir
        sys.stdout.flush()
        sys.stderr.flush()

        with open('/dev/null', 'r') as dev_null:
            os.dup2(dev_null.fileno(), sys.stdin.fileno())

        with open('/dev/null', 'w') as dev_null:
            os.dup2(dev_null.fileno(), sys.stdout.fileno())
            os.dup2(dev_null.fileno(), sys.stderr.fileno())

        # PID dosyasını yaz
        with open(self.pidfile, 'w') as f:
            f.write(str(os.getpid()))

    def run(self):
        # Daemon'ın asıl işi burada yapılır
        while True:
            # Log dosyasına mesaj yaz
            with open('/var/log/mydaemon.log', 'a') as f:
                f.write(f"Daemon çalışıyor: {time.ctime()}\n")
            time.sleep(60)  # 1 dakika bekle

if __name__ == "__main__":
    daemon = SimpleDaemon('/var/run/mydaemon.pid')
    daemon.daemonize()
    daemon.run()

Adımlar neden bu sırada?

Koddaki sıra keyfi değildir; her adım bir öncekinin açık bıraktığı kapıyı kapatır.

Birinci fork() iki iş görür. Parent çıktığı için kabuk komut istemini geri verir ve süreç arka plana düşer. Ayrıca ortaya çıkan çocuk süreç bir process group leader değildir; bu şarttır, çünkü setsid() çağrısı zaten grup lideri olan bir süreçte EPERM ile başarısız olur.

setsid() yeni bir oturum açar ve süreci denetleyen terminalinden ayırır. Terminal kapandığında gönderilen SIGHUP artık sürece ulaşmaz. Yan etkisi olarak süreç bu yeni oturumun lideri hâline gelir.

İkinci fork() tam olarak bu yan etkiyi ortadan kaldırmak içindir. SysV terminal semantiğinde yalnızca oturum lideri bir denetleyen terminal edinebilir. Süreç oturum lideri olarak kalsaydı, ileride O_NOCTTY bayrağı olmadan herhangi bir terminal aygıtını açtığında (/dev/tty1, bir seri port veya yanlış yapılandırılmış bir günlük hedefi) o aygıt sessizce sürecin denetleyen terminali olurdu; ilgili oturum kapandığında gelen SIGHUP daemon’ı düşürebilirdi. İkinci fork’un çocuğu oturum lideri olmadığından bu yetki kalıcı olarak ortadan kalkar.

Bu adım atlandığında sorun çoğu zaman hemen görünmez; hatanın nadir ve teşhisinin zor olması kalıbın yerleşmesinin sebebidir. BSD semantiğinde denetleyen terminal edinmek açık bir ioctl(TIOCSCTTY) çağrısı gerektirdiğinden ikinci fork orada zorunlu değildir.

Kalan adımların da birer karşılığı vardır. os.chdir("/") çalışma dizinini sabitler; aksi hâlde daemon, sonradan silinmesi veya çıkarılması gereken bir dizini meşgul tutar. os.umask(0) oluşturulacak dosyaların izinlerinin kalıtılan bir maskeye bağlı kalmasını engeller. Standart akışların /dev/null üzerine yönlendirilmesi ise kapalı bir dosya tanıtıcısına yazmaktan doğacak hataları önler.

Bu yaklaşımın sınırları

PID dosyası yaklaşımı kırılgandır. Süreç beklenmedik biçimde sonlanırsa dosya diskte kalır; sistem o PID numarasını bir süre sonra başka bir sürece atayabilir ve sonraki durdurma isteği ilgisiz bir süreci sonlandırır. “PID yazıldı” ile “süreç yaşıyor” arasında atomik bir ilişki yoktur.

Örnekteki /var/run/mydaemon.pid yolu ayrıca güvenilir değildir: /var/run bugün /run dizinine bağlıdır ve /run bir tmpfs’tir, her açılışta boşalır. Root olmayan bir kullanıcı bu dizine yazamaz. Doğru çözüm dizini unit dosyasında RuntimeDirectory= ile tanımlamaktır; systemd dizini uygun izinlerle oluşturur ve servis durduğunda temizler.

Systemd Döneminde Daemon Yazmak

Systemd kullanan bir sistemde yukarıdaki adımların hiçbiri uygulamanın sorumluluğu değildir. Varsayılan Type=simple ile süreç ön planda, sıradan bir program gibi çalışır: fork etmez, oturum açmaz, dosya tanıtıcılarını kapatmaz, PID dosyası yazmaz.

#!/usr/bin/env python3
import time

while True:
    print(f"Daemon çalışıyor: {time.ctime()}", flush=True)
    time.sleep(60)

Daemon davranışı kodda değil unit dosyasında tanımlanır:

[Unit]
Description=Basit örnek servis
After=network-online.target

[Service]
ExecStart=/usr/bin/python3 /opt/mydaemon/main.py
Restart=on-failure
RestartSec=5
User=mydaemon
RuntimeDirectory=mydaemon

[Install]
WantedBy=multi-user.target

Tanım sudo systemctl enable --now mydaemon ile devreye alınır. Aradaki fark yalnızca kod miktarı değildir:

Type=forking seçeneği yalnızca elle daemonize eden eski uygulamaları desteklemek için vardır ve PIDFile= ile birlikte kullanılır; yeni yazılan servisler için tercih edilmez.

Konteyner ortamlarında aynı kural daha da katı biçimde geçerlidir: konteyner içinde uygulama zaten PID 1 olarak çalışır. Orada daemonize etmek parent’ın çıkmasına ve konteynerin anında sonlanmasına yol açar.

Daemon ve Service Arasındaki Fark

Bu terimleri sık karıştırırız:

Örneğin httpd daemon’u, HTTP service’ini sağlar.

Popüler Daemon’lar

İşte sisteminizdeki bazı önemli daemon’lar:

Web ve Database

Ağ ve Güvenlik

Sistem

Daemon’ları İzlemek

Hangi daemon’ların çalıştığını görmek için:

# Çalışan servisleri listele
systemctl list-units --type=service --state=running

# Denetleyen terminali olmayan süreçler: daemon'ların ayırt edici özelliği
ps -eo pid,ppid,tty,comm | awk '$3 == "?"'

# Tek bir sürecin parent'ı, oturumu ve terminali
ps -o pid,ppid,sid,tty,comm -p "$(pgrep -f nginx | head -1)"

# Belirli bir daemon'u ara
pgrep -f nginx

Köşeli parantez içinde görünen isimler ([kworker/0:1], [ksoftirqd/0]) daemon değil, çekirdeğin kendi iş parçacıklarıdır. Onları ayrıca listelemek için ps -eo pid,comm --ppid 2 kullanılır; PID 2 kthreadd’dir.

Sonuç

Uzun yaşayan bir arka plan işlemi geliştirirken yalnızca döngüyü çalıştırmak yeterli değildir. Yetki sınırı, sinyal yönetimi, loglama, yeniden başlatma politikası ve sağlıklı kapanış davranışı da servis sözleşmesinin parçasıdır. Modern Linux ortamlarında bu sorumlulukların çoğunu el ile daemonize etmek yerine systemd gibi bir servis yöneticisine bırakmak daha gözlemlenebilir bir çözüm sağlar.