TEKNİK YAZI / Backend Sistemleri

Daemon Nedir? Linux Arka Plan Süreçleri ve systemd

Daemon, Linux'ta terminalden bağımsız çalışan arka plan sürecidir. Klasik çift fork adımları, nedenleri ve systemd ile servis yazmanın güncel yolu anlatılır.

Daemon, kullanıcı arayüzü ve denetleyen terminali olmadan arka planda çalışan; ağ isteği, zamanlanmış görev ya da sistem olayı gibi işleri sürekli karşılayan süreçtir. Linux ve Unix benzeri sistemlerde web sunucusundan günlük servisine kadar pek çok temel bileşen bu modelle çalışır. Yazı, daemon’ların ortak özelliklerini, klasik daemonize adımlarını ve bu adımların nedenlerini, systemd altında aynı işin nasıl yapıldığını ele alır.

Daemon nedir?

Daemon, arka planda çalışan sistem programlarına verilen addır. Bu programlar genellikle sistem açılışında başlatılır ve durdurulana kadar çalışır. Teknik açıdan belirleyici olan ad değildir: 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.

Bir Linux sisteminde sık karşılaşılan daemon’lar ve süreç adları:

GörevSüreç adı
Web sunucusunginx, apache2 (Debian), httpd
Veritabanımysqld, postgres
Uzak bağlantısshd
Zamanlanmış görevlercron (Debian/Ubuntu), crond (cronie)
Günlük toplamasystemd-journald, rsyslogd
Ağ yönetimiNetworkManager, systemd-networkd
Yazdırmacupsd
Servis yöneticisisystemd (PID 1)

cron ile zamanlanan işlerin nasıl tanımlandığı cron job yazısında ayrıca anlatılıyor.

Daemon özellikleri

Bir programı daemon yapan özellikler şunlardır:

  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. Üst süreci genellikle PID 1’dir: Her sürecin bir üst süreci (parent) vardır. Kendisini başlatan süreç sonlandığında çekirdek onu yeniden atar (reparenting): en yakın alt süreç toplayıcısı (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. systemd’nin başlattığı sistem servislerinin üst süreci doğrudan PID 1’dir; bir kullanıcı servisinin üst süreci ise o kullanıcının systemd --user yöneticisidir.
  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 syslog’a veya /var/log/ altına yazar; systemd altında standart çıktı doğrudan journald tarafından toplanır.

Daemon adlandırması

Daemon adlarının çoğu d harfiyle biter:

Bu d harfi “daemon” kelimesinden gelir ve programın bir arka plan servisi olduğunu gösterir. Kural zorunlu değildir; nginx ve postgres gibi adlar bu geleneği izlemez.

Daemon’ları yönetmek

systemd ile

Günümüzdeki Linux dağıtımlarının çoğunda servisler systemd ile yönetilir:

# Servis durumunu kontrol et
sudo systemctl status nginx

# Servisi başlat
sudo systemctl start nginx

# Servisi durdur
sudo systemctl stop nginx

# Servisin açılışta otomatik başlamasını sağla
sudo systemctl enable nginx

# Servisin açılışta otomatik başlamasını engelle
sudo systemctl disable nginx

Bir uygulamanın servis birimi (unit) olarak nasıl tanımlandığı systemd servis yazısında adım adım anlatılıyor.

SysV init ve service sarmalayıcısı

service komutu, /etc/init.d/ altındaki SysV init betiklerini çalıştırmak için yazılmıştır. systemd kullanan Debian ve Ubuntu sistemlerinde de bulunur: aynı adlı bir systemd birimi varsa start, stop, status ve reload komutlarını systemctl karşılıklarına aktarır. Bu yüzden eski belgelerdeki komutlar çoğu zaman hâlâ çalışır:

sudo service nginx status
sudo service nginx start
sudo service nginx stop
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ı. daemon(7) kılavuzu bu adımları “SysV daemon” başlığı altında on beş madde olarak sıralar ve yalnız SysV uyumluluğu gerçekten gerekiyorsa uygulanmasını önerir. 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. Sistem çağrılarının genel çerçevesi sistem programlama yazısında anlatılıyor.

Aşağıdaki örnek basitleştirilmiştir ve daemon(7)‘deki adımların yalnız bir bölümünü içerir:

#!/usr/bin/env python3
# Basitleştirilmiş örnek: daemon(7)'deki SysV adımlarının bir bölümü
import os
import sys
import time


def izinli_ac(yol, bayraklar):
    # umask(0) sonrasında dosya izni yalnız burada verilen moddan gelir
    return os.open(yol, bayraklar, 0o644)


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

    def daemonize(self):
        # İlk fork: ebeveyn süreç çıkar, kabuk komut istemini geri verir
        try:
            if os.fork() > 0:
                sys.exit(0)
        except OSError as e:
            sys.stderr.write(f"1. fork başarısız: {e}\n")
            sys.exit(1)

        # Yeni oturum aç, denetleyen terminalden ayrıl
        os.setsid()

        # İkinci fork: oturum lideri çıkar, terminal edinme yetkisi kalkar
        try:
            if os.fork() > 0:
                sys.exit(0)
        except OSError as e:
            sys.stderr.write(f"2. fork başarısız: {e}\n")
            sys.exit(1)

        # Çalışma dizinini köke al
        os.chdir("/")

        # Kalıtılan maskeyi sıfırla; izinler her dosyada açıkça verilir
        os.umask(0)

        # Standart girdi, çıktı ve hatayı /dev/null'a yönlendir
        sys.stdout.flush()
        sys.stderr.flush()
        devnull = os.open(os.devnull, os.O_RDWR)
        for fd in (0, 1, 2):
            os.dup2(devnull, fd)
        os.close(devnull)

        # PID dosyasını 0644 izniyle yaz
        with open(self.pidfile, "w", opener=izinli_ac) as f:
            f.write(str(os.getpid()))

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


if __name__ == "__main__":
    daemon = SimpleDaemon("/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. Ebeveyn çıktığı için kabuk komut istemini geri verir ve süreç arka plana düşer. Ayrıca ortaya çıkan çocuk süreç bir süreç grubu lideri (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 olasılık 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 ayrılması (unmount) gereken bir dosya sistemini meşgul tutar. os.umask(0), oluşturulan dosyaların izinlerinin kalıtılan bir maskeye bağlı kalmasını engeller; daemon(7) bu adımı, open() ve mkdir() çağrılarına verilen modların izinleri doğrudan belirlemesi için önerir. Bunun bir sonucu vardır: maske sıfırken mod verilmeden oluşturulan dosya, dilin varsayılan moduyla oluşur. Python’un open() fonksiyonu bu modu 0o666 olarak kullanır; yani PID dosyası ve günlük dosyası herkes tarafından yazılabilir olurdu. Root olarak çalışan bir daemon’da başka bir kullanıcının PID dosyasını değiştirebilmesi, aşağıda anlatılan yanlış süreci sonlandırma riskini kasıtlı bir saldırıya çevirir. Örnekte bu yüzden iki dosya da opener ile 0o644 moduyla oluşturulur. Standart akışların /dev/null üzerine yönlendirilmesi ise kapanmış bir terminale yazmaya çalışmaktan doğacak hataları önler.

daemon(7) listesindeki diğer adımlar örnekte yoktur: miras alınan dosya tanıtıcılarını kapatmak, sinyal işleyicilerini ve sinyal maskesini sıfırlamak, ortam değişkenlerini temizlemek, yetkileri düşürmek ve başlatan sürece hazır olunduğunu bildirmek.

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 /run/mydaemon.pid yolu da yalnız root için çalışır. /run dizini (eski /var/run bugün ona bir bağlantıdır) root’a aittir ve tmpfs olduğu için her açılışta boşalır. Root olmayan bir kullanıcıyla çalışan daemon için doğru çözüm, dizini unit dosyasında RuntimeDirectory= ile tanımlamaktır; systemd dizini /run/<ad>/ altında servis kullanıcısına ait olarak 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. ExecStart= verilmiş ve Type= belirtilmemiş bir serviste varsayılan Type=simple’dır: süreç ön planda, sıradan bir program gibi çalışır; fork etmez, oturum açmaz, 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:

# /etc/systemd/system/mydaemon.service
[Unit]
Description=Basit örnek servis

[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 genellikle PID 1 olarak çalışır. Orada daemonize etmek ana sürecin çıkmasına ve konteynerin hemen sonlanmasına yol açar.

Daemon ve servis arasındaki fark

Bu iki terim sık karıştırılır:

Örneğin httpd daemon’ı HTTP hizmetini sağlar ve systemd’de httpd.service birimiyle yönetilir.

Daemon’ları izlemek

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 (çekirdek iş parçacıkları da dâhil)
ps -eo pid,ppid,tty,comm | awk '$3 == "?"'

# nginx ana sürecinin üst süreci, oturumu ve terminali
ps -o pid,ppid,sid,tty,comm -p "$(pgrep -o -x nginx)"

# Adı tam olarak nginx olan süreçler
pgrep -a -x nginx

pgrep -o en eski eşleşmeyi, yani nginx’in ana (master) sürecini seçer; -x adın tam eşleşmesini ister. -f ile tam komut satırında arama yapmak daha geniş eşleşir ve ilgisiz süreçleri de yakalayabilir.

ps aux gibi tam komut satırını gösteren çıktılarda köşeli parantez içinde görünen adlar ([kworker/0:1], [ksoftirqd/0]) daemon değil, çekirdeğin kendi iş parçacıklarıdır. Onları ayrıca listelemek için ps -o pid,comm --ppid 2 kullanılır; PID 2, çekirdek iş parçacıklarını başlatan kthreadd’dir. Bu komuta -e eklenmemelidir: ps seçim seçeneklerini birleştirdiği için -e ile birlikte bütün süreçler listelenir.

Sonuç

Uzun yaşayan bir arka plan işi geliştirirken yalnızca döngüyü çalıştırmak yeterli değildir. Yetki sınırı, sinyal yönetimi, günlükleme, yeniden başlatma politikası ve temiz kapanış da servis sözleşmesinin parçasıdır. Güncel Linux sistemlerinde bu sorumlulukları elle daemonize ederek değil systemd gibi bir servis yöneticisine bırakarak karşılamak daha güvenli ve daha kolay gözlemlenebilir bir çözümdür; klasik çift fork kalıbı ise yalnız eski sistemlerle uyumluluk gerektiğinde anlamlıdır.

Birincil kaynaklar