Linux fork(), Copy-on-Write, CPython Heap/Stack Yapısı ve Reference Counting İlişkisi

python dev.to

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    |
+----------------------+
Enter fullscreen mode Exit fullscreen mode

Text Segment

Programın makine kodlarını içerir.

Örneğin:

print("Hello")
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Heap

Dinamik olarak oluşturulan nesneler burada yer alır.

Python'daki her nesne:

x = [1, 2, 3]
Enter fullscreen mode Exit fullscreen mode

Heap üzerinde oluşturulur.

Bellek büyümesi yukarı doğrudur.

Heap
 ↑
 ↑
 ↑
Enter fullscreen mode Exit fullscreen mode

Stack

Fonksiyon çağrıları burada tutulur.

Örnek:

def foo():
    x = 10
Enter fullscreen mode Exit fullscreen mode

Buradaki:

x
Enter fullscreen mode Exit fullscreen mode

değişkeninin referansı stack üzerinde bulunur.

Bellek büyümesi aşağı doğrudur.

 ↓
 ↓
 ↓
Stack
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Bellekte:

+----------------+
| ob_refcnt = 3  |
+----------------+
| ob_type        |
+----------------+
| object data    |
+----------------+
Enter fullscreen mode Exit fullscreen mode

Örnek:

x = [1, 2, 3]
Enter fullscreen mode Exit fullscreen mode

Heap üzerinde:

Stack                    Heap

x  ------------->   [PyListObject]
Enter fullscreen mode Exit fullscreen mode

3. Reference Counting Sistemi

CPython'un temel Garbage Collection mekanizmasıdır.


Referans Ekleme

a = [1,2,3]

b = a
Enter fullscreen mode Exit fullscreen mode

Önce:

ob_refcnt = 1
Enter fullscreen mode Exit fullscreen mode

Sonra:

ob_refcnt = 2
Enter fullscreen mode Exit fullscreen mode

olur.


Referans Silme

del b
Enter fullscreen mode Exit fullscreen mode

sonrası:

ob_refcnt = 1
Enter fullscreen mode Exit fullscreen mode

Sıfıra Düşerse

ob_refcnt = 0
Enter fullscreen mode Exit fullscreen mode

olursa:

free(object)
Enter fullscreen mode Exit fullscreen mode

çalışır.

Nesne yok edilir.


4. fork() Sistem Çağrısı

Linux'ta yeni süreç oluşturmanın temel yöntemidir.

pid = fork();
Enter fullscreen mode Exit fullscreen mode

Çağrı sonrası:

Parent Process
      |
      |
    fork()
      |
      |
      +------ Child Process
Enter fullscreen mode Exit fullscreen mode

İ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
Enter fullscreen mode Exit fullscreen mode

aynı fiziksel sayfa hem parent hem child tarafından paylaşılır.


Örnek

Parent:

data = [1] * 1000000
Enter fullscreen mode Exit fullscreen mode

100 MB bellek kullansın.

Fork sonrası:

pid = os.fork()
Enter fullscreen mode Exit fullscreen mode

Normalde:

100 MB + 100 MB
Enter fullscreen mode Exit fullscreen mode

olması gerekir.

Ancak Linux:

100 MB
Enter fullscreen mode Exit fullscreen mode

kullanır.

Çünkü aynı sayfalar paylaşılır.


Sayfa Tablosu

Parent Page Table
      |
      +------> Physical Page

Child Page Table
      |
      +------> Physical Page
Enter fullscreen mode Exit fullscreen mode

6. CoW Ne Zaman Tetiklenir?

Bir süreç yazma yaparsa.

Örnek:

data[0] = 999
Enter fullscreen mode Exit fullscreen mode

İşletim sistemi şunu görür:

WRITE DETECTED
Enter fullscreen mode Exit fullscreen mode

ve ilgili sayfayı kopyalar.

Parent ----> Page A

Child -----> Page B
Enter fullscreen mode Exit fullscreen mode

Artık paylaşım sona ermiştir.


7. CPython ve Büyük Problem

Teorik olarak:

obj[0]
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

CPython arka planda:

Py_INCREF(item);
Enter fullscreen mode Exit fullscreen mode

çağırır.

Bu da:

ob_refcnt
Enter fullscreen mode Exit fullscreen mode

alanına yazı yazılması anlamına gelir.


Bellekte:

+----------------+
| ob_refcnt = 7  |
+----------------+
Enter fullscreen mode Exit fullscreen mode

olur:

+----------------+
| ob_refcnt = 8  |
+----------------+
Enter fullscreen mode Exit fullscreen mode

Bu bir WRITE işlemidir.


9. CoW Neden Bozuluyor?

İşletim sistemi açısından:

READ OBJECT
Enter fullscreen mode Exit fullscreen mode

değil,

WRITE MEMORY
Enter fullscreen mode Exit fullscreen mode

gerçekleşmiştir.

Çünkü:

ob_refcnt++
Enter fullscreen mode Exit fullscreen mode

yapılmıştır.

Sonuç:

Copy-on-Write Triggered
Enter fullscreen mode Exit fullscreen mode

10. Sayfa Kopyalanması

Diyelim ki:

huge_data = [...]
Enter fullscreen mode Exit fullscreen mode

10 milyon nesne içeriyor.

Fork sonrası:

pid = os.fork()
Enter fullscreen mode Exit fullscreen mode

Bellek paylaşılır.


Child:

for x in huge_data:
    pass
Enter fullscreen mode Exit fullscreen mode

çalıştırıyor.

Görünürde sadece okuma yapıyor.

Ancak gerçekte:

Py_INCREF(x);
Py_DECREF(x);
Enter fullscreen mode Exit fullscreen mode

işlemleri sürekli çalışıyor.


Sonuç:

Page Modified
Enter fullscreen mode Exit fullscreen mode

olur.

Linux:

Copy Page
Copy Page
Copy Page
Copy Page
Enter fullscreen mode Exit fullscreen mode

işlemine başlar.


11. Bellek Patlaması

Başlangıç:

Parent = 4 GB
Child  = 0 GB ekstra
Enter fullscreen mode Exit fullscreen mode

Toplam:

4 GB
Enter fullscreen mode Exit fullscreen mode

Bir süre sonra:

Parent = 4 GB
Child  = 4 GB
Enter fullscreen mode Exit fullscreen mode

Toplam:

8 GB
Enter fullscreen mode Exit fullscreen mode

Buna literatürde sıklıkla:

Memory Double-Up
Enter fullscreen mode Exit fullscreen mode

denir.


12. Multiprocessing'de Neden Görülür?

Örnek:

from multiprocessing import Process
Enter fullscreen mode Exit fullscreen mode

Linux'ta varsayılan olarak:

fork
Enter fullscreen mode Exit fullscreen mode

kullanılır.

Dolayısıyla:

100 GB dataset
Enter fullscreen mode Exit fullscreen mode

yükleyip ardından:

8 worker
Enter fullscreen mode Exit fullscreen mode

oluşturmak teoride:

100 GB
Enter fullscreen mode Exit fullscreen mode

kullanmalı gibi görünür.

Pratikte:

800 GB+
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

Ç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")
Enter fullscreen mode Exit fullscreen mode

Forkserver Kullanmak

multiprocessing.set_start_method(
    "forkserver"
)
Enter fullscreen mode Exit fullscreen mode

Shared Memory Kullanmak

Python 3.8+

from multiprocessing import shared_memory
Enter fullscreen mode Exit fullscreen mode

NumPy Shared Memory

numpy.memmap
Enter fullscreen mode Exit fullscreen mode

PyTorch

torch.multiprocessing
Enter fullscreen mode Exit fullscreen mode

ve

share_memory_()
Enter fullscreen mode Exit fullscreen mode

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.

Source: dev.to

arrow_back Back to Tutorials