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örev | Süreç adı |
|---|---|
| Web sunucusu | nginx, apache2 (Debian), httpd |
| Veritabanı | mysqld, postgres |
| Uzak bağlantı | sshd |
| Zamanlanmış görevler | cron (Debian/Ubuntu), crond (cronie) |
| Günlük toplama | systemd-journald, rsyslogd |
| Ağ yönetimi | NetworkManager, systemd-networkd |
| Yazdırma | cupsd |
| Servis yöneticisi | systemd (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:
- Terminale bağlı değildir: Denetleyen terminali (controlling terminal) yoktur; bu yüzden terminal kapandığında gönderilen
SIGHUPsinyalinden etkilenmez.psçıktısındaTTYsütununun?görünmesi bunun işaretidir. - Ü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ınsystemd --useryöneticisidir. - 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. - Uzun ömürlüdür: Tek bir işi yapıp çıkmaz; durdurulana veya sistem kapanana kadar çalışır.
- 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:
httpd: HTTP daemonsshd: SSH daemonmysqld: MySQL daemonftpd: FTP daemon
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:
- Süreç takibi: systemd süreci PID dosyasıyla değil cgroup üyeliğiyle izler. Uygulama alt süreçler başlatsa bile hepsi aynı cgroup içinde kalır; kaçan süreç ve PID yeniden kullanımından doğan yarış ortadan kalkar.
- Günlükleme: Standart çıktıya yazmak yeterlidir. journald çıktıyı zaman damgası, unit adı ve öncelik bilgisiyle toplar;
journalctl -u mydaemon -file izlenir. Dosya döndürme ve izin yönetimi uygulamanın işi olmaktan çıkar. - Yeniden başlatma:
Restart=on-failureveRestartSec=çökme sonrası davranışı tanımlar. - Sıralama ve bağımlılık:
After=veBefore=yalnız başlatma sırasını belirler; başka bir birimi işleme dâhil etmek içinWants=veyaRequires=gerekir. Ağa ihtiyaç duyan bir servisWants=network-online.targetveAfter=network-online.targetsatırlarını birlikte yazar; örnekteki döngü ağ kullanmadığı için bu satırlara gerek yoktur.Type=notifyseçilir ve uygulama hazır olduğundasd_notify("READY=1")çağırırsa systemd servisi “başlatıldı” değil “hazır” kabul eder; onaAfter=ile bağlanan birimler ancak o noktada başlar. - Yetki sınırı:
User=,DynamicUser=,ProtectSystem=strict,PrivateTmp=,NoNewPrivileges=veMemoryMax=gibi direktifler izolasyonu unit dosyasında tanımlar. - Kapanış: systemd durdurma isteğinde önce
SIGTERMgönderir veTimeoutStopSec=süresince bekler; süre dolarsaSIGKILLuygular. Bu, kill ve PID yazısında anlatılan güvenli sonlandırma sırasıyla aynıdır. UygulamanınSIGTERMiçin bir işleyici tanımlaması, bağlantıları kapatıp veriyi diske yazarak temiz çıkması için yeterlidir.
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:
- Daemon: Arka planda çalışan programın kendisi.
- Servis: Daemon’ın sağladığı hizmet; systemd bağlamında ise bu hizmeti tanımlayan
.servicebirimi.
Ö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.