https://www.youtube.com/watch?v=Iwfe5d-DlVU
Linux fork(), Copy-on-Write, CPython Heap/Stack Yapısı ve Reference Counting İlişkisi
Giriş
CPython üzerinde çalışan uygulamalarda performans optimizasyonu amacıyla çoklu süreç (multiprocessing) kullanımı oldukça yaygındır. Özellikle Linux sistemlerinde fork() çağrısı sayesinde yeni süreç oluşturma işlemi son derece hızlı gerçekleşir. Ancak birçok geliştiricinin gözden kaçırdığı önemli bir detay vardır:
CPython'un referans sayımı (Reference Counting) mekanizması ile Linux'un Copy-on-Write (CoW) sistemi birbirleriyle etkileşime girer ve beklenenden çok daha fazla bellek tüketimine neden olabilir.
Bu makalede;
- Linux sanal bellek mimarisi
- Process adres alanı
- Heap ve Stack yapıları
- fork() sistem çağrısı
- Copy-on-Write mekanizması
- CPython nesne modeli
- Reference Counting
- fork() sonrası oluşan performans problemleri
konularını detaylı olarak inceleyeceğiz.
1. Bir Linux Sürecinin Bellek Yapısı
Bir proses oluşturulduğunda işletim sistemi ona sanal bir adres alanı tahsis eder.
Tipik görünüm:
+----------------------+
| Kernel Space |
+----------------------+
| Stack |
| ↓↓↓ |
+----------------------+
| |
| Free Area |
| |
+----------------------+
| Heap |
| ↑↑↑ |
+----------------------+
| BSS / Data Segment |
+----------------------+
| Text Segment |
+----------------------+
Text Segment
Programın makine kodlarını içerir.
Örneğin:
print("Hello")
CPython yorumlayıcısının çalıştırdığı makine kodları burada bulunur.
Salt okunurdur.
Data Segment
Global değişkenler burada tutulur.
Örnek:
GLOBAL_COUNT = 100
Heap
Dinamik olarak oluşturulan nesneler burada yer alır.
Python'daki her nesne:
x = [1, 2, 3]
Heap üzerinde oluşturulur.
Bellek büyümesi yukarı doğrudur.
Heap
↑
↑
↑
Stack
Fonksiyon çağrıları burada tutulur.
Örnek:
def foo():
x = 10
Buradaki:
x
değişkeninin referansı stack üzerinde bulunur.
Bellek büyümesi aşağı doğrudur.
↓
↓
↓
Stack
2. CPython Nesne Yapısı
Python'da her nesne aslında bir C yapısıdır.
Basitleştirilmiş hali:
typedef struct {
Py_ssize_t ob_refcnt;
PyTypeObject *ob_type;
} PyObject;
Bellekte:
+----------------+
| ob_refcnt = 3 |
+----------------+
| ob_type |
+----------------+
| object data |
+----------------+
Örnek:
x = [1, 2, 3]
Heap üzerinde:
Stack Heap
x -------------> [PyListObject]
3. Reference Counting Sistemi
CPython'un temel Garbage Collection mekanizmasıdır.
Referans Ekleme
a = [1,2,3]
b = a
Önce:
ob_refcnt = 1
Sonra:
ob_refcnt = 2
olur.
Referans Silme
del b
sonrası:
ob_refcnt = 1
Sıfıra Düşerse
ob_refcnt = 0
olursa:
free(object)
çalışır.
Nesne yok edilir.
4. fork() Sistem Çağrısı
Linux'ta yeni süreç oluşturmanın temel yöntemidir.
pid = fork();
Çağrı sonrası:
Parent Process
|
|
fork()
|
|
+------ Child Process
İlk bakışta sanki tüm RAM kopyalanıyormuş gibi görünür.
Ancak gerçekte durum farklıdır.
5. Copy-on-Write (CoW)
Linux performans için tüm belleği anında kopyalamaz.
Bunun yerine:
Parent
|
+------+
|
|
Physical Page
aynı fiziksel sayfa hem parent hem child tarafından paylaşılır.
Örnek
Parent:
data = [1] * 1000000
100 MB bellek kullansın.
Fork sonrası:
pid = os.fork()
Normalde:
100 MB + 100 MB
olması gerekir.
Ancak Linux:
100 MB
kullanır.
Çünkü aynı sayfalar paylaşılır.
Sayfa Tablosu
Parent Page Table
|
+------> Physical Page
Child Page Table
|
+------> Physical Page
6. CoW Ne Zaman Tetiklenir?
Bir süreç yazma yaparsa.
Örnek:
data[0] = 999
İşletim sistemi şunu görür:
WRITE DETECTED
ve ilgili sayfayı kopyalar.
Parent ----> Page A
Child -----> Page B
Artık paylaşım sona ermiştir.
7. CPython ve Büyük Problem
Teorik olarak:
obj[0]
gibi bir okuma işlemi CoW tetiklememelidir.
Çünkü veri değişmiyordur.
Ancak CPython'da durum farklıdır.
8. Okuma Sırasında Reference Count Güncellenmesi
Örneğin:
item = obj[0]
CPython arka planda:
Py_INCREF(item);
çağırır.
Bu da:
ob_refcnt
alanına yazı yazılması anlamına gelir.
Bellekte:
+----------------+
| ob_refcnt = 7 |
+----------------+
olur:
+----------------+
| ob_refcnt = 8 |
+----------------+
Bu bir WRITE işlemidir.
9. CoW Neden Bozuluyor?
İşletim sistemi açısından:
READ OBJECT
değil,
WRITE MEMORY
gerçekleşmiştir.
Çünkü:
ob_refcnt++
yapılmıştır.
Sonuç:
Copy-on-Write Triggered
10. Sayfa Kopyalanması
Diyelim ki:
huge_data = [...]
10 milyon nesne içeriyor.
Fork sonrası:
pid = os.fork()
Bellek paylaşılır.
Child:
for x in huge_data:
pass
çalıştırıyor.
Görünürde sadece okuma yapıyor.
Ancak gerçekte:
Py_INCREF(x);
Py_DECREF(x);
işlemleri sürekli çalışıyor.
Sonuç:
Page Modified
olur.
Linux:
Copy Page
Copy Page
Copy Page
Copy Page
işlemine başlar.
11. Bellek Patlaması
Başlangıç:
Parent = 4 GB
Child = 0 GB ekstra
Toplam:
4 GB
Bir süre sonra:
Parent = 4 GB
Child = 4 GB
Toplam:
8 GB
Buna literatürde sıklıkla:
Memory Double-Up
denir.
12. Multiprocessing'de Neden Görülür?
Örnek:
from multiprocessing import Process
Linux'ta varsayılan olarak:
fork
kullanılır.
Dolayısıyla:
100 GB dataset
yükleyip ardından:
8 worker
oluşturmak teoride:
100 GB
kullanmalı gibi görünür.
Pratikte:
800 GB+
RAM tüketimine kadar çıkabilir.
13. Modern Yapay Zeka Sistemlerinde Etkisi
Özellikle:
- PyTorch
- NumPy
- Pandas
- Transformers
- TensorFlow
uygulamalarında önemlidir.
Örnek:
model = load_llm()
os.fork()
Çocuk süreç modeli okumaya başlar.
Referans sayaçları güncellenir.
CoW sayfaları kırılır.
RAM kullanımı hızla yükselir.
14. Python Geliştiricileri Ne Yapmalı?
Spawn Kullanmak
Linux'ta:
multiprocessing.set_start_method("spawn")
Forkserver Kullanmak
multiprocessing.set_start_method(
"forkserver"
)
Shared Memory Kullanmak
Python 3.8+
from multiprocessing import shared_memory
NumPy Shared Memory
numpy.memmap
PyTorch
torch.multiprocessing
ve
share_memory_()
kullanmak.
Sonuç
Linux'un fork() mekanizması, Copy-on-Write sayesinde başlangıçta son derece verimli görünür. Ancak CPython'un her nesnenin yaşam döngüsünü takip etmek için kullandığı Reference Counting sistemi, nesnelere yapılan masum görünen erişimlerde bile ob_refcnt alanına yazma işlemi gerçekleştirir.
Bu yazma işlemleri işletim sistemi tarafından gerçek bir bellek değişikliği olarak algılanır ve CoW sayfalarının kopyalanmasına neden olur. Sonuç olarak büyük veri kümeleri, yapay zekâ modelleri ve yüksek hacimli bellek kullanan uygulamalarda fork() sonrasında beklenmedik RAM artışları ortaya çıkabilir.
Bu nedenle ileri seviye Python geliştiricileri için yalnızca Python dilini bilmek yeterli değildir; CPython'un C seviyesindeki nesne modeli ile Linux çekirdeğinin sanal bellek yönetiminin nasıl etkileştiğini anlamak, yüksek performanslı sistemler tasarlamanın temel şartlarından biridir.