Mert Özen Her Satırda Daha İleri
Geliştirici Araçları Terminal ve Command Line — #06

tmux ile Terminal Multiplexing: Pane'ler, Session'lar ve SSH Kopmasına Dayanmak

Mert Özen 14 Kas 2026 12 dk 1 görüntülenme
tmux ile Terminal Multiplexing: Pane'ler, Session'lar ve SSH Kopmasına Dayanmak

tmux'ta shell'ler arka plandaki tmux server'ın içinde çalışır, terminal yalnızca bir client'tır; bu yüzden SSH koptuğunda tmux dışındaki iş ölür, tmux içindeki devam eder (gerçek denemeyle). Session, window ve pane kavramlarını, prefix kısayollarını, dotnet watch ve ng serve'ü tek komutla açan bir geliştirme script'ini, copy mode'u, tmux 3.4 ile 3.8 arasındaki varsayılan farklarını dikkate alan minimal bir .tmux.conf'u ve iç içe tmux ile TERM tuzaklarını anlatıyoruz.

Sürüm notu: Bu yazıdaki komutlar Ubuntu 24.04 üzerinde tmux 3.8 (resmi kaynak koddan derlendi) ile; karşılaştırma için Ubuntu'nun apt paketi tmux 3.4, .NET SDK 10.0.404, Angular CLI 22.2.7 ve bağlantı kopmasını denemek için aynı ortamda çalışan bir OpenSSH sunucusuyla denenmiştir. Windows Terminal ve iTerm2'nin pane kısayolları resmi dokümanlarından aktarıldı.

Önce Tahmin Et

Bir test sunucusuna SSH (uzak makineye şifreli bağlantı) ile bağlandın ve sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) katalog import script'ini başlattın. Script 30 saniye boyunca her saniye bir satır log yazıyor. Dördüncü saniyede Wi-Fi gidiyor ve SSH bağlantısı kopuyor. Aynı denemeyi iki kez yapıyorsun: bir kez script'i doğrudan çalıştırarak, bir kez tmux new -s import ./import.sh ile.

$ ssh dev-server ./import.sh
$ ssh dev-server tmux new -s import ./import.sh

Bağlantı koptuktan birkaç saniye sonra iki durumda da log'un son satırı ne olur ve script hâlâ çalışıyor mu? Cevabı yazının ortasında gerçek bir denemeyle gösteriyorum; tmux'un var olma sebebi bu iki cevap arasındaki fark.

Server, Session, Window ve Pane

tmux bir terminal multiplexer: tek bir terminal penceresinin içinde birden fazla terminal açar ve bunları senin pencerenden bağımsız yaşatır. Bunu iki parçayla yapıyor. tmux server arka planda çalışan bir process; bütün shell'ler ve programlar onun child'ı olarak, onun açtığı PTY'lerde (terminal ile shell arasındaki sanal terminal katmanı) çalışır. Senin terminalinde gördüğün tmux ise yalnızca bir client: ekranı çizer, tuşlarını server'a iletir. Client gidince (pencereyi kapattın, SSH koptu) server ve içindekiler çalışmaya devam eder.

06-tmux-hierarchy-tr.png

Server'ın içinde üç seviye var. Session bir çalışma bağlamıdır; Bookstore için bir tane açıyorum. Window, session içindeki sekme gibidir; durum satırında 1:dev, 2:git diye görünür. Pane, bir window'u bölen parçalardan her biridir ve her pane'de ayrı bir shell çalışır. İlk yazıdaki terminal, PTY ve shell ilişkisi burada da geçerli: her pane'in kendi PTY'si var, terminal emulator rolünü ise tmux üstleniyor.

$ tmux new -s bookstore
$ tmux ls
$ tmux attach -t bookstore
$ tmux new -A -s bookstore
$ tmux kill-session -t bookstore

Sırasıyla: yeni bir session açıp bağlanır, session'ları listeler, var olana tekrar bağlanır, varsa bağlanıp yoksa açar ve session'ı kapatır. Aynı adla ikinci bir session açmaya çalışınca tmux net bir hata verir: duplicate session: bookstore. -A bu durumda hata yerine var olana bağlanır; script'lerde ve alias'larda işe yarar.

Prefix Tuşu ve Temel Kısayollar

tmux'a komut vermek için önce prefix tuşuna basılır, sonra komutun tuşuna. Varsayılan prefix Ctrl-b; aşağıda C-b % "Ctrl-b'ye bas, bırak, sonra % bas" demek. tmux 3.8'in varsayılan bağlamalarından (tmux list-keys -T prefix) en çok kullanacakların:

Tuştmux komutuİş
C-b %split-window -hPane'i sağa böl
C-b "split-windowPane'i aşağı böl
C-b + ok tuşuselect-pane -U/-D/-L/-RPane'ler arasında geç
C-b zresize-pane -ZPane'i büyüt / geri al
C-b c, C-b n, C-b pnew-window, next-window, previous-windowWindow aç, sonrakine / öncekine geç
C-b ddetach-clientSession'dan ayrıl, her şey çalışmaya devam etsin
C-b s, C-b wchoose-treeSession / window listesinden seç
C-b [copy-modeGeçmişte gezin, ara, kopyala
C-b xkill-pane (onay sorar)Pane'i kapat
C-b ?list-keys -NBütün kısayolları göster

Bütün kısayollar arka planda bir tmux komutu çalıştırıyor. Bu yüzden aynı işleri shell'den, script'ten ve C-b : ile açılan komut satırından da yapabilirsin; birazdan Bookstore düzenini bu komutlarla kuracağız.

SSH Kopsa da Süren İşler

Tahminin cevabı. Denemeyi bir SSH sunucusuna bağlanıp bağlantının ortasında ssh client process'ini kill -9 ile öldürerek yaptım; Wi-Fi'nin gitmesiyle aynı şey, sunucu karşı tarafın kaybolduğunu görüyor. import script'i 30 saniye boyunca import batch N satırlarını /tmp/import.log'a yazıyor. Bağlantı 4. saniyede koptu, 4 saniye sonra log'a ve process listesine baktım:

== without tmux
import batch 4
job gone
== inside tmux
import batch 8
job still running
import: 1 windows (created Sat Nov 14 04:12:18 2026)

tmux olmadan script bağlantıyla birlikte öldü. SSH sunucusu oturumu kapatırken PTY'yi kapatır ve o PTY'ye bağlı process'lere SIGHUP (terminalin kapandığını bildiren sinyal) gider; script'in varsayılan davranışı bu sinyalde ölmek. tmux'ta ise SIGHUP'ı alan, ölen client oldu. Script tmux server'ın PTY'sinde çalıştığı için hiçbir şey fark etmedi, log yazmaya devam etti ve birkaç saniye sonra import batch 30 ile bitti. Tekrar bağlandığında tmux attach -t import ekranı kaldığı yerden gösterir.

nohup ./import.sh & de SIGHUP'ı yok sayarak işi yaşatır, ama çıktıya ve etkileşime bir daha ulaşamazsın; tmux'ta aynı ekrana geri dönersin. Sunucuda sürekli çalışması gereken bir şey (API, worker) ise ne tmux'ta ne nohup'la çalıştırılmalı; onun yeri systemd service'i ya da container. Bu ayrımı Linux ve Shell serisinin process ve systemd yazısında anlatıyoruz.

Bookstore Geliştirme Düzeni: Tek Komutla Üç Pane

Yerelde Bookstore üzerinde çalışırken aynı anda üç şeye bakıyorum: dotnet watch ile çalışan API ve onun log'ları, ng serve ile çalışan Angular uygulaması, git ve test komutları için boş bir shell. Bunu her sabah elle bölmek yerine repo'ya bir script koydum:

#!/usr/bin/env bash
# Bookstore geliştirme session'ını açar ya da var olana bağlanır
set -euo pipefail

session=bookstore
root=$(git -C "$(dirname "$0")" rev-parse --show-toplevel)

if ! tmux has-session -t "$session" 2>/dev/null; then
    api=$(tmux new-session -d -s "$session" -n dev -c "$root" -P -F '#{pane_id}')
    web=$(tmux split-window -h -t "$api" -c "$root/web/bookstore-web" -P -F '#{pane_id}')
    shell=$(tmux split-window -v -t "$api" -c "$root" -P -F '#{pane_id}')

    tmux send-keys -t "$api" 'dotnet watch --project src/Bookstore.Api' C-m
    tmux send-keys -t "$web" 'ng serve' C-m
    tmux send-keys -t "$shell" 'git status -sb' C-m

    tmux new-window -d -t "$session" -n git -c "$root"
    tmux select-pane -t "$shell"
fi

if [ -n "${TMUX:-}" ]; then
    tmux switch-client -t "$session"
else
    tmux attach -t "$session"
fi

Birkaç tasarım kararı var. Pane'leri 0.1, 0.2 gibi numaralarla değil, tmux'un oluştururken döndürdüğü kimliklerle (-P -F '#{pane_id}', %0 gibi) hedefliyorum; böylece .tmux.conf'ta numaralamayı 1'den başlatmak script'i bozmuyor. send-keys ... C-m komutu pane'deki shell'e yazıp Enter'a basar, yani komut shell'in history'sine de girer; Ctrl-C ile durdurup yukarı ok ve Enter ile yeniden başlatabilirsin. Session zaten varsa script yalnızca bağlanır, ikinci kez açmaz. tmux'un içinden çalıştırılırsa attach yerine switch-client kullanıyor; iç içe tmux açmamak için.

./scripts/dev-tmux.sh'ın açtığı ekranın bir kısmı (120 sütunluk bir terminalde):

info: Microsoft.Hosting.Lifetime[14]                        │                    | Initial total | 48.16 kB
      Now listening on: http://localhost:5080               │
info: Microsoft.Hosting.Lifetime[0]                         │Application bundle generation complete. [0.582 seconds] - 2
      Application started. Press Ctrl+C to shut down.       │026-11-14T04:17:16.680Z
info: Microsoft.Hosting.Lifetime[0]                         │
      Hosting environment: Development                      │Watch mode enabled. Watching for file changes...
info: Microsoft.Hosting.Lifetime[0]                         │NOTE: Raw file sizes do not reflect development server per-
      Content root path: /home/ada/bookstore/src/Bookstore.A│request transformations.
pi                                                          │  ➜  Local:   http://localhost:4200/
                                                            │  ➜  press h + enter to show help
────────────────────────────────────────────────────────────┤
5                                                           │
ada@dev:~/bookstore$                                        │
[bookstore1:dev* 2:git                                                                             "dev" 04:17 14-Nov-26

Sol üstte API, sağda Angular, sol altta curl -s localhost:5080/api/books | jq length'in cevabı (5). En alttaki durum satırı session'ı, window'ları ve aktif window'u (*) gösteriyor. Pane'lerin dökümü:

$ tmux list-panes -s -t bookstore -F "#{window_index}.#{pane_index} #{window_name} #{pane_current_command} #{pane_current_path}"
1.1 dev dotnet /home/ada/bookstore
1.2 dev bash /home/ada/bookstore
1.3 dev ng /home/ada/bookstore/web/bookstore-web

(Liste 2.1 git bash /home/ada/bookstore satırıyla devam ediyor.) C-b d ile ayrılınca terminalde [detached (from session bookstore)] yazıyor ve API ile Angular çalışmaya devam ediyor; script'i tekrar çalıştırınca hiçbir şeyi yeniden başlatmadan aynı ekrana dönüyorsun.

İlk denemede bir sürprizle karşılaştım: ng serve pane'i hiç başlamadı, çünkü Angular CLI yeni bir home'da ilk çalıştırmada completion kurmak isteyip istemediğini soruyordu ve cevap bekliyordu. İkinci yazıda gördüğümüz o soru, otomatik başlatılan bir komutta işi durduruyor. Bir kez cevap verince (Ok, you won't be prompted again) bir daha sormuyor. Script'le başlatılan her etkileşimli aracın ilk çalıştırmada soru sorabileceğini aklında tut.

Copy Mode ve Scrollback

tmux'un içinde terminalin kendi kaydırma çubuğu her pane'i ayrı göremez; geçmişe bakmak için copy mode var. C-b [ ile girersin, ok tuşları ve Page Up/Down ile gezersin, q ile çıkarsın. .tmux.conf'ta mode-keys vi açıksa ? geriye, / ileriye doğru arar; Space seçimi başlatır, Enter kopyalar, C-b ] yapıştırır. Varsayılan Emacs tuşlarında arama C-r ve C-s ile yapılır.

Her pane'in geçmişi history-limit kadar satırla sınırlı; tmux 3.8'de varsayılan 2000. Uzun build log'larında bu çok az, ben 50000 yapıyorum. Bir pane'in bütün geçmişini dosyaya almak için:

$ tmux capture-pane -p -S - -t bookstore:dev.1 > /tmp/api-pane.log
$ wc -l < /tmp/api-pane.log
33
$ grep -c "Microsoft.Hosting.Lifetime" /tmp/api-pane.log
4

-S - geçmişin en başından başla demek. Bir hatanın tam çıktısını bir issue'ya eklemek ya da bir ekip arkadaşına göndermek için bu, ekrandan seçip kopyalamaktan daha güvenilir.

Minimal .tmux.conf

tmux 3.1'den beri ayar dosyası ~/.config/tmux/tmux.conf da olabilir. Benim kullandığım kısa dosya:

# Varsayılan prefix C-b kalsın: C-a shell'de satır başına gitmek demek

# Yeni tmux'ta değişen varsayılanlar; eski sürümler için de açıkça yaz
set -g mouse on
set -sg escape-time 10

set -g history-limit 50000
set -g base-index 1
setw -g pane-base-index 1
set -g renumber-windows on
setw -g mode-keys vi

# Renkler: dıştaki terminalin 24-bit renk desteklediğini söyle
set -g default-terminal "tmux-256color"
set -as terminal-features ",xterm-256color:RGB"

# Bölme ve yeni window o anki pane'in klasöründe açılsın
bind | split-window -h -c "#{pane_current_path}"
bind - split-window -v -c "#{pane_current_path}"
bind c new-window -c "#{pane_current_path}"

bind r source-file ~/.config/tmux/tmux.conf \; display-message "tmux.conf reloaded"

İlk iki satırın sebebi sürüm farkı. tmux'un değişiklik listesine göre 3.5'te escape-time'ın varsayılanı 10 ms'ye indi, 3.8'de mouse varsayılan olarak açık geliyor. Ubuntu 24.04'ün apt'den kurduğu tmux ise 3.4 ve orada ikisi de eski hâlinde: aynı komutlar 3.4'te mouse için off, escape-time için 500 döndürdü. 500 ms, Escape'e basıp vim'de normal moda geçerken hissedilen bir gecikme. Ayar dosyasında açıkça yazınca WSL'deki (Windows içindeki Linux ortamı) eski tmux ile macOS'taki yeni tmux aynı davranıyor. 3.7 ve 3.8 ayrıca layout'un üstünde duran floating pane'ler getirdi; bu özellik eski sürümlerde yok.

Prefix'i değiştirmeme sebebim ikinci yazıyla ilgili. İnternetteki .tmux.conf örneklerinin çoğu prefix'i C-a yapar; ama Ctrl-A, bash, zsh ve PSReadLine'ın Emacs modunda satır başına gitmek. Prefix olunca her satır başına gidişte iki kez basman gerekir. C-b rahatsız ediyorsa C-Space gibi shell'in kullanmadığı bir tuş seç.

Alternatifler: Windows Terminal ve iTerm2 Pane'leri, zellij

Pane'e ihtiyacın yalnızca yerelde, aynı anda birkaç terminal görmekse terminal emulator'ün kendi pane'leri yeterli olabilir. Windows Terminal dokümanına göre Alt+Shift+Artı sağa, Alt+Shift+Eksi aşağı böler, Alt+Shift+D bulunduğun profile'ı kopyalayarak böler, Alt+ok tuşları pane'ler arasında gezer, Alt+Shift+ok boyutlandırır ve Ctrl+Shift+W pane'i kapatır. Pane'i büyütmek (zoom) varsayılan olarak bir tuşa bağlı değil, Ctrl+Shift+P ile açılan komut paletinden yapılıyor. macOS'ta iTerm2'de Cmd+D sağa, Cmd+Shift+D aşağı böler.

Bu pane'lerin tmux'tan temel farkı detach olmaması: pencereyi kapatınca içindeki her şey kapanır, SSH koptuğunda iş de gider. Ayrıca her makinede farklıdır; tmux ise sunucuda, WSL'de ve macOS'ta aynı tuşlarla çalışır. Windows'ta native bir tmux yok, WSL içinde kullanırsın. iTerm2'nin tmux -CC ile açılan entegrasyon modu, tmux window'larını iTerm2'nin kendi sekme ve pencereleri olarak gösterir; hem tmux'un kalıcılığını hem iTerm2'nin arayüzünü istiyorsan bak.

zellij ise Rust ile yazılmış yeni nesil bir multiplexer (son sürümü 0.46): kısayolları ekranın altında gösterir, layout'ları KDL (zellij'in kullandığı ayar dili) dosyalarıyla tanımlar ve tmux'tan daha kolay öğrenilir. Bir sunucuya her zaman kurulu gelmediği ve ekiplerin büyük kısmı tmux bildiği için ben tmux'ta kalıyorum; ama yalnız kendi makinende kullanacaksan denemeye değer.

Üretimde Dikkat

tmux bir process yöneticisi değil. Sunucu yeniden başlarsa bütün session'lar gider, tmux server bir hata yüzünden kapanırsa içindeki her şey onunla gider. Bir veritabanı migration'ını ya da bir import'u elle başlatıp izlemek için tmux doğru araç; sürekli çalışması gereken bir şeyi tmux'ta bırakmak ise "geçen yıl birisi tmux'ta başlatmıştı" diye başlayan olayların sebebi. Kalıcı işler systemd ya da container orchestrator'ın işidir.

Scrollback bellekte durur ve capture-pane ile dosyaya yazılabilir. Ekrana basılmış bir connection string ya da token, history'den sildikten sonra bile pane geçmişinde kalır; paylaşılan bir sunucuda başka kullanıcılar senin socket'ine erişemez, ama aynı kullanıcıyla bağlanan herkes tmux attach ile ekranını görür. Paylaşılan hesaplarda bunu bil, işin bitince C-b : ve clear-history ile geçmişi temizle.

Sık Düşülen Tuzaklar

Birincisi iç içe tmux. Laptop'unda tmux içindeyken SSH ile tmux kullanan bir sunucuya bağlanırsan iki tmux da C-b bekler ve tuşu her zaman dıştaki alır. Varsayılan bağlamalarda C-b C-b prefix'i içeriye iletir (bind-key -T prefix C-b send-prefix), yani içteki tmux'tan detach olmak için C-b C-b d yazarsın. Bunu sık yapıyorsan dıştaki ile içteki için farklı prefix seç.

İkincisi renkler ve TERM. tmux içinde TERM değeri tmux-256color'dır; kendi denememde bir pane'de echo $TERM bunu döndürdü. Bağlandığın uzak makinede bu terminfo kaydı yoksa less, vim, htop gibi araçlar "unknown terminal" diye şikâyet eder ya da renkler bozulur; tmux'un FAQ'ına (sık sorulan sorular sayfası) göre screen girdisi neredeyse her sistemde bulunduğu için o makinede TERM=screen-256color daha taşınabilir bir seçim; karşılığında italik gibi birkaç özellik kaybolur. 24-bit renkler soluk görünüyorsa yukarıdaki terminal-features satırı eksik demektir.

Üçüncüsü, her pane'in login shell açması. tmux varsayılan olarak her yeni pane'de login shell başlatır; pane'de echo $0 -bash döndürdü, baştaki tire login shell demek. İlk yazıdaki tabloya göre bu, her pane'de .bash_profile ya da .zprofile'ın yeniden okunması demek. PATH'e (komutların arandığı klasör listesi) ekleme yapan satırlar her pane'de tekrar çalışır; zsh'de typeset -U path bu yüzden önemli. Dördüncüsü, aynı adla ikinci session açmaya çalışmak ve duplicate session hatasını görmek: tmux new -A ya da yukarıdaki script'teki has-session kontrolü bunu çözer.

Küçük Bir Deneme

★☆☆ tmux new -s bookstore ile bir session aç, pane'i önce sağa sonra aşağı böl, her pane'de farklı bir komut çalıştır, C-b z ile birini büyüt, C-b d ile ayrıl ve tmux attach ile geri dön.

★★☆ scripts/dev-tmux.sh'ı kendi Bookstore repo'nda çalıştır. Üçüncü bir window ekle: dotnet watch test --project tests/Bookstore.Api.Tests çalıştırsın ve bir testi bozduğunda sonucu orada gör. Copy mode'da hata mesajını arayıp capture-pane ile dosyaya al.

★★★ WSL ya da bir test sunucusunda, bu yazıdaki SSH denemesini kendin tekrarla: uzun süren bir komutu bir kez doğrudan, bir kez tmux içinde başlat ve bağlantıyı kopar. Sonra laptop'unda tmux içindeyken aynı sunucudaki tmux'a bağlan ve iç içe prefix sorununu send-prefix ile ya da iki farklı prefix'le çöz.

Bu yazıyla Terminal ve Command Line serisi bitti: terminal, shell ve prompt'u ayırdık, klavyeden hızlandık, zsh ve PowerShell'i yapılandırdık, modern araçları ve tmux'u ekledik. Bu ayarları sürümlemek, yeni bir makineye tek komutla kurmak ve makineler arasında senkron tutmak Productivity ve Automation serisinin konusu.