Sistem Çağrıları

Modern işletim sistemlerinde, çekirdek kipinde çalışma ve kullanıcı kipinde çalışma modları, sert bariyerlerle birbirinden ayrılmış durumdadır.

Bu şekildeki bir tasarım, sistemin güvenli ve sağlıklı çalışması için elzemdir.

Kullanıcı kipinde çalışan bir uygulamanın, sistem çağrıları aracılığıyla işletim sistemi çekirdeğinden ihtiyaç duyduğu servisleri alabilmesi sağlanır.

Her uygulama, mutlaka sistem çağrısı yapmak durumundadır. Yapılan sistem çağrıları disk üzerinden okuma, yazma gibi daha "fiziksel" ve donanıma yakın olabileceği gibi o anki sistem zamanını alma gettimeofday, çalışan uygulamanın process id değerini öğrenme getpid, uygulamaya öncelikler atama setpriority gibi doğrudan çekirdek içerisindeki belirli mekanizmalarla ilgili de olabilir. Bir uygulamanın hayata gelmesi ve çalışmaya başlayabilmesi için öncesinde bir başka uygulamanın fork() sistem çağrısıyla yeni bir process üretmesi de gereklidir.

Linux çekirdeğinde X86 mimarisi için yaklaşık 380 civarında sistem çağrısı bulunur.

Sistem Çağrısı Nasıl Gerçekleşir?

Sistem çağrılarının çekirdek tarafındaki gerçekleştirimi mimariden mimariye değişkenlik gösterir. Linux çekirdeğinin farklı bir mimariye port edilirken yapılan temel işlem adımlarından biri, sistem çağrılarının en verimli şekilde yapacak şekilde uygun bir kodlamanın, mimari spesifik olarak yapılmasıdır.

Her sistem çağrısının 1, 5, 27 gibi ilişkili bir numarası vardır. Bu numaralar da mimariden mimariye değişkenlik göstermektedir. Temel sistem çağrıları tüm mimarilerde bulunmakla birlikte, tüm mimarilerde eşit sayıda sistem çağrısı bulunmaz.

Konunun devamında aksi belirtilmedikçe verilen örnekler 32 bit Intel mimarisi için geçerlidir.

Kullanıcı kipindeyken herhangi bir sistem çağrısı yapıldığında INT 0x80 makine dili kodu ile trap oluşturulur.

Aynı zamanda talep edilen sistem çağrısının numarası, EAX yazmacına yazılır.

Talep edilen sistem çağrısının parametreleri var ise, bu parametrelerin diğer yazmaçlar kullanılarak belirtilmesi gerekir. Ancak her mimaride bu amaçla kullanılabilecek yazmaç sayısı limitlidir. Bazılarında daha çok genel amaçlı yazmaç var iken bazılarında daha az olduğu görülmektedir.

32 bitlik Intel platformu için Linux çekirdek versiyonu 2.3.31 ve sonrası, maksimum 6 sistem çağrısı parametresini desteklemektedir. Bu parametreler sırasıyla EBX, ECX, EDX, ESI, EDI ve EBP yazmaçlarında saklanır.

Sistem çağrısı için 6'dan fazla parametre gerekli olduğunda, bellekteki bir veri yapısı hazırlanarak parametrelar burada saklanır, sonrasında ilgili bellek adresi sistem çağrısına parametre olarak geçirilir.

Mimari Bağımlılığı

Sistem çağrılarının doğrudan işlemci mimarisine bağımlı olduğuna değinmiştik.

Örnek olarak Intel 32 bitlik işlemcilerde INT 0x80 ile trap oluşturulurken, ARM mimarisinde aynı işlem supervisor call SVC ile yapılır

Benzer şekilde Intel mimarisinde yapılan sistem çağrısının numarası için EAX yazmacı kullanılırken, ARM mimarisinde R8 yazmacı kullanılır. ARM mimarisinde sistem çağrısına ait 4 adede kadar parametre, R9, R10, R11 ve R12 yazmaçlarına aktarılır. 4 adetten fazla parametre geçilmesi gerektiğinde, bellek üzerinde veri yapısı hazırlanarak bu bölümün adresi geçirilir.

Genel olarak sistem çağrıları performansının ARM mimarisinde X86'ya göre daha düşük olduğunu söyleyebiliriz (yazmaç/register sayısının azlığı bunda etken olabilir mi düşününüz).

Sistem Çağrısı Nasıl Yapılır?

Sistem çağrılarını daha zor bir yoldan doğrudan yapmak mümkün olsa da bu önerilen bir durum değildir.

Sistem çağrıları, glibc kütüphanesindeki wrapper fonksiyonlar üzerinden kullanılır.

glibc kütüphanesi, üzerinde çalıştığı çekirdek versiyonuna göre, hangi Linux sistem çağrısını yapacağını belirler.

Bazı durumlarda ise bundan daha fazlasını yaparak, üzerinde çalışılan çekirdek versiyonunda hiç desteklenmeyen bir özelliği de sunuyor olabilir. Örnek olarak, Linux 2.6 versiyonuyla birlikte gelen POSIX Timer API'nin olmadığı Linux 2.4 versiyonu üzerinde çalışan ve aynı anda pek çok timer kullanan bir uygulamanız var ise, glibc çekirdek tarafından alamadığı desteği kullanıcı kipinde her timer için bir thread açarak sağlar. Elbette timer sayınız fazla ise bu çok yavaş bir çözüm olur ancak uygulamanın çalışmasını da mümkün kılar.

Sistem Çağrıları → Glibc Fonksiyonları İlişkisi

Pek çok sistem çağrısı, aynı isimdeki glibc wrapper fonksiyonları üzerinden çağrılmaktadır.

Not: Bu duruma strace çıktılarını okurken de dikkat etmemiz gereklidir.

Örnek olarak sistem çağrılarını takip etmede sık kullanacağımız strace aracının çıktısındaki open() çağrısına bakalım:

open("/tmp/index.jpeg", O_RDONLY)       = 3

Burada kastedilen glibc içerisindeki open() fonksiyonu değil, open sistem çağrısıdır.

Strace üzerinden sistem çağrısına geçirilen argümanları ve geri dönüş değerini (3) görmekteyiz.

Normalde bu yöntemle sistem çağrılarının ismi değil numarası izlenebilir. Strace uygulaması elde ettiği sistem çağrısı numarasını kendi veritabanında arayıp bizler için daha okunabilir bir formda gösterir. Strace uygulamasının bu işlemi hangi yöntemle gerçekleştirdiğine dair detaylı bilgileri ilerleyen bölümlerimizde bulabilirsiniz.

Performans

Sistem çağrıları normal fonksiyon çağrılarına oranla oldukça yüksek maliyetli işlemlerdir.

Her sistem çağrısında uygulamanın o anki durumunun saklanması, çekirdeğin işlemcinin kontrolünü ele alması ve ilgili sistem çağrısı ile çekirdek kipinde talep edilen işlemleri gerçekleştirmesi, sonra ilgili uygulamanın tekrar çalışma sırası geldiğinde, uygulamanın saklanan durumunun yeniden üretilip işlemlerin kaldığı yerden devamının sağlanması gereklidir.

2002 yılında Linux Kernel eposta listelerine Mike Hayward'ın şaşkınlığını içeren bir eposta düştü. Hayward elindeki Pentium 3 - 850 Mhz dizüstü bilgisayarıyla Pentium 4 - 2 Ghz ve Xeon - 2.4 Ghz sistemlerinin, sistem çağrıları açısından performansını ölçmek için bir test uygulaması yazdı ve 1K'lık buffered dosya okuma testinde aşağıdaki şaşırtıcı sonuçları elde etti:

Sistem Saniyedeki IO
Pentium 3 - 850 Mhz 149
Pentium 4 - 2 Ghz 108
Xeon - 2.4 Ghz 69

Aynı testi dosya okuma yerine farklı sistem çağrılarıyla da test ettiğinde benzer sonuçların alındığını tespi etti.

Bunun sebebi, bazı x86 serisi işlemcilerde çekirdek kipine daha hızlı geçiş için SYSENTER/SYSEXIT özel instruction'ının bulunmasıydı. Pentium 3 serisinde varolan bu destek, Pentium 4 ve Xeon işlemcilere yeterince olgunlaşmadığından konulmamıştı. Pentium 3'teki bu imkanı iyi değerlendiren Linux çekirdeği, kendisinden daha üstün Xeon işlemcilerden bile daha iyi performans göstermekteydi.

Benzer zamanlarda AMD de benzer şekilde SYSCALL/SYSRET özel instruction'ınını sunmaya başlamıştır.

Linux çekirdeği de bu yeni imkanları kullanarak geleneksel INT 0x80 kesme yöntemine göre önemli oranda performans iyileşmesi sağlanmıştır.

Günümüz x86 ve x86_64 işlemcilerinde bu mekanizma tümüyle desteklenmektedir.

Performans Problemi - Detaylı Bakış

Özellikle x86 tabanlı mimarilerde SYSENTER özel yolu sayesinde sistem çağrılarının hızlanmasını sağladık. Ancak bu yeterli olacak mıdır?

Bir çok uygulamada, özellikle gettimeofday() gibi sistem çağrılarının çok sık kullanıldığını görürürüz.

Uygulamalarınızı strace ile incelediğinizde, bilginiz dahilinde olmayan pek çok farklı gettimeofday() çağrısını yapıldığını görebilirsiniz.

glibc kütüphanesinden kullandığınız bazı fonksiyonlar, internal olarak bu fonksiyonaliteyi kullanıyor olabilir.

Java Virtual Machine gibi bir VM üzerinde çalışan uygulamalar için de benzer bir durum söz konusudur.

Görece basit bir işlem olmasına rağmen sık kullanılan bu operasyon yüzünden sistemlerde nasıl bir sistem çağrısı yükü oluşmaktadır? sorusunu kendimize sorabiliriz.

Linux Dynamic Linker/Loader: ld.so

Paylaşımlı kütüphaneler kullanan uygulamaların çalıştırılması sırasında, Linux Loader tarafından gereken kütüphaneler yüklenerek uygulamanın çalışacağı ortam hazırlanır. En basit Hello World uygulamamız bile libc kütüphanesine bağımlı olacaktır.

ldd ile uygulamanın linklenmiş olduğu kütüphanelerin listesini alabiliriz:

$ ldd hello
    linux-vdso.so.1 (0x00007fff7d88a000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f2fb43a0000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f2fb476d000)

Görüldüğü üzere libc ve ld.so bağımlılıkları listelendi. Fakat linux-vdso.so.1 kütüphanesi nedir?

find komutu ile tüm sistemimizi arattığımızda neden bu kütüphaneyi bulamıyoruz?

Virtual DSO: linux-vdso.so.1

linux-vdso.so.1 sanal bir Dnamically Linked Shared Object dosyasıdır. Gerçekte böyle bir kütüphane dosya sistemi üzerinde yer almaz. Linux çekirdeği, çok sık kullanılan bazı sistem çağrılarını, bu şekilde bir hile kullanarak kullanıcı kipinde daha hızlı gerçekleştirmektedir.

Örnek olarak, sistem saati her değişiminde sonucu tüm çalışan uygulamaların adres haritalarına da eklenmiş olan özel bir bellek alanına koyarsa, gettimeofday() işlemi gerçekte bir sistem çağrısına yol açmadan kullanıcı kipinde tamamlanabilir.

Şimdi bu konuları biraz daha detaylandıralım.

Not: Konunun bundan sonrası meraklıları için olup, çok gerekli olmayan bu bölümün yeni başlayan kullanıcılar için atlanması önerilir.

/proc/self/maps

Linux proc dosya sisteminde /proc/<PID>/maps dosyasında ilgili PID (process id) için çekirdek tarafından yapılmış olan adres haritalaması gösterilir.

Özel bir durum olarak, <PID> yerine self ibaresi kullanıldığında, o an bu dosya erişimini yapan process ile ilgili dizinde işlem yapılmış olur.

$ cat /proc/self/maps
    00400000-0040c000 r-xp 00000000 08:02 1703938   /bin/cat
    0060b000-0060c000 r--p 0000b000 08:02 1703938   /bin/cat
    0060c000-0060d000 rw-p 0000c000 08:02 1703938   /bin/cat
    024de000-024ff000 rw-p 00000000 00:00 0         [heap]
    7ff7033c5000-7ff70369f000 r--p 00000000 08:02 2362900 /usr/lib/locale/locale-archive
    7ff70369f000-7ff70383e000 r-xp 00000000 08:02 393230  /lib/x86_64-linux-gnu/libc-2.19.so
    7ff70383e000-7ff703a3d000 ---p 0019f000 08:02 393230  /lib/x86_64-linux-gnu/libc-2.19.so
    7ff703c69000-7ff703c6a000 rw-p 00000000 00:00 0 
    7fff8cd95000-7fff8cdb6000 rw-p 00000000 00:00 0 [stack]
    7fff8cdfc000-7fff8cdfe000 r-xp 00000000 00:00 0 [vdso]
    7fff8cdfe000-7fff8ce00000 r--p 00000000 00:00 0 [vvar]
    ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0   [vsyscall]

vsyscall

/proc/self/maps dosyasına cat uygulaması ile bir kaç defa baktığınızda, vsyscall (Virtual System Call Page) haricindeki bölümlerin başlangıç adreslerinin değiştiğini görmekteyiz.

vsyscall bölümü, sadece bir uygulama için değil, sistemdeki tüm uygulamalar için aynı statik yeri göstermektedir.

Bu sayede dinamik linkleme (dolayısıyla paylaşımlı kütüphane) kullanmayan, tamamen statik uygulamaların da bu statik adres üzerinden vsyscall bölümüne erişimi mümkün olmaktadır.

Bu bölgenin uzunluğu kısıtlı olduğundan, sadece belirli sayıda girdiye sahiptir: vgettimeofday(), vtime(), vgetcpu()

Tüm uygulamalar için aynı adrese haritalanması, özellikle return to libc türü ataklarıyla sistem çağrısı yapılabilmesine neden olmaktadır.

Linux 3.0 versiyonuna kadar vsyscall tablosu kullanılmış olmakla birlikte, 3.1 ve sonrasında bu yöntem artık önerilmiyor. vDSO mekanizması hem daha güvenli hem daha hızlı.

vdso Bölümünü Dışarı Çıkartmak

İnceleme amacıyla uygulamanın adres haritasındaki [vdso] biçiminde işaretlenmiş alanı diske çıkartmaya çalışalım.

Örneğimizde bu bölümün 7fff8cdfc000 ile 7fff8cdfe000 adresleri arasında, 2 adet Page büyüklüğünde olduğunu görüyoruz.

Acaba dd komutu ile bu bölümü dışarı çıkartabilir miyiz:

$ dd if=/proc/self/mem of=dso.out bs=1 skip=$((0x7fff8cdfc000)) count=8192

Maalesef bu yöntem artık çalışmıyor. Bunun 2 nedeni var:

  1. /proc sanal dosya sistemi altındaki girdiler normal bir dosya gibi görünmesine karşılık, stat() ile bakıldığında st_size değeri 0 olmaktadır. Bu durum dd uygulamasının ilgili offset adresine seek yapılamayacağını söylemesine neden oluyor. Çözüm için ufak bir yama gerekiyor

  2. Yeni Linux çekirdek versiyonlarında buradaki başlangıç değer adresi, 7fff8cdfc000 her uygulama için aynı değildir. Return to libc tarzı atakları zorlaştırmak için bu değer ancak çalışan uygulama içerisinden öğrenilebilir. Bunun için dd kodunun değiştirilmesi veya ufak bir test uygulaması yazılması gerekiyor.

extract_region.c

Bu işlemi yapabilmek için extract_region adını verdiğimiz aşağıdaki gibi bir uygulama hazırlayalım:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define _FILE_OFFSET_BITS 64

int main (int argc, char *argv[])
{
    if (argc != 3) {
        fprintf(stderr,
            "Kullanım: %s <cikti> <section>\n"
            "\t<cikti>\t\t: export edilecek dosya\n"
            "\t<section>\t: export edilecek map region\n\n", argv[0]);
        return 1;
    }

    off_t start_addr, end_addr;
    char buf[4096];
    const char *out = argv[1];
    const char *region = argv[2];

    int found = 0;
    FILE *fp = fopen("/proc/self/maps", "r");
    while (fgets(buf, sizeof(buf), fp)) {
        printf("%s", buf);
        if (strstr(buf, region)) {
            found = 1;
            break;
        }
    }
    fclose(fp);
    if (!found) {
        fprintf(stderr, "%s bölümü bulunamadı\n", region);
        return 1;
    }
    end_addr = strtoull((strchr(buf, '-') + 1), NULL, 16);
    *(strchr(buf, '-')) = '\0';
    start_addr = strtoull(buf, NULL, 16);

    printf("\nÇıkartılacak Alan Başlangıç: 0x%llx, Bitiş: 0x%llx\n\n", start_addr, end_addr);

    FILE *dst = fopen(out, "w+");
    if (dst == NULL) {
        fprintf(stderr, "%s açılamadı\n", out);
        return 1;
    }
    FILE *src = fopen("/proc/self/mem", "r");
    char *tmp = malloc(end_addr - start_addr);
    fseeko(src, start_addr, SEEK_SET);
    fread(tmp, end_addr - start_addr, 1, src);
    fwrite(tmp, end_addr - start_addr, 1, dst);
    fclose(src);
    fclose(dst);
    return 0;
}

Test Uygulamamızı Çalıştıralım

$ ./extract_region vdso.out vdso
...
7ff6db951000-7ff6dbaf0000 r-xp 00000000 08:02 393230    /lib/x86_64-linux-gnu/libc-2.19.so
7ff6dbf19000-7ff6dbf1a000 r--p 00020000 08:02 393236    /lib/x86_64-linux-gnu/ld-2.19.so
7ff6dbf1a000-7ff6dbf1b000 rw-p 00021000 08:02 393236    /lib/x86_64-linux-gnu/ld-2.19.so
7ff6dbf1b000-7ff6dbf1c000 rw-p 00000000 00:00 0 
7fffc477d000-7fffc479e000 rw-p 00000000 00:00 0         [stack]
7fffc47fc000-7fffc47fe000 r-xp 00000000 00:00 0         [vdso]

Çıkartılacak Alan Başlangıç: 0x7fffc47fc000, Bitiş: 0x7fffc47fe000

İşlem bitiminde 8192 byte uzunluğunda vdso.out dosyası oluşacaktır.

file komutu ile dosyanın tipine baktığımızda standart bir kütüphane gibi görünecektir:

$ file vdso.out 
vdso.out: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID
[sha1]=538bea2738a229413dcc98af8f4f7127f9bca874, stripped

vdso İçine Bakalım

objdump ile dışarı çıkarttığımız bu bölüme bir bakalım:

$ objdump -T vdso.out 

vdso.out:     file format elf64-x86-64

DYNAMIC SYMBOL TABLE:
0000000000000418 l    d  .rodata    0000000000000000              .rodata
0000000000000970  w   DF .text    000000000000057d  LINUX_2.6   clock_gettime
0000000000000000 g    DO *ABS*    0000000000000000  LINUX_2.6   LINUX_2.6
0000000000000ef0 g    DF .text    00000000000002b9  LINUX_2.6   __vdso_gettimeofday
00000000000011d0 g    DF .text    000000000000003d  LINUX_2.6   __vdso_getcpu
0000000000000ef0  w   DF .text    00000000000002b9  LINUX_2.6   gettimeofday
00000000000011b0  w   DF .text    0000000000000015  LINUX_2.6   time
00000000000011d0  w   DF .text    000000000000003d  LINUX_2.6   getcpu
0000000000000970 g    DF .text    000000000000057d  LINUX_2.6   __vdso_clock_gettime
00000000000011b0 g    DF .text    0000000000000015  LINUX_2.6   __vdso_time

Basit Bir Test Uygulaması

100.000 defa gettimeofday() fonksiyonunu çağıran ve işlem bitiminde başlangıç ve bitiş zamanlarını gösteren örnek bir uygulama yapalım:

#include <stdio.h>
#include <sys/time.h>

int main ()
{
    int i;
    struct timeval now;
    struct timeval before;
    struct timeval after;
    gettimeofday(&before, NULL);
    for (i = 0; i < 100000; i++) gettimeofday(&now, NULL);
    gettimeofday(&after, NULL);
    printf("Before: %li.%li\n", before.tv_sec, before.tv_usec);
    printf("After : %li.%li\n", after.tv_sec, after.tv_usec);
    return 0;
}

X86_64 ve ARM Üzerinde Test

100.000 defa gettimeofday çağrısı yapan time_test örnek uygulamasını, 1 Ghz hızına düşürülmüş X86_64 i5 işlemcili platform ile 1 Ghz saat frekansındaki ARM BeagleBoneBlack platformunda karşılaştıralım

(X86_64) $ ./time_test
Before: 1419786575.719463
After : 1419786575.722560

(ARM) $ ./time_test
Before: 1419786909.186960
After : 1419786909.252160

Görüldüğü üzere X86_64'te 3-4 milisaniyede gerçekleşen işlem, ARM sistemimizde 70 milisaniyelerde gerçekleşmektedir.

Şimdi test uygulamamımızı bir de strace kontrolünde her iki platformda çalıştıralım:

(X86_64) $ strace ./time_test
...
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7ff4ee1f1000
write(1, "Before: 1419787456.897162\n", 26Before: 1419787456.897162) = 26
write(1, "After : 1419787456.904669\n", 26After : 1419787456.904669) = 26
exit_group(0)

##########################

(ARM) $ strace ./time_test
...
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x400b3000
gettimeofday({1419787571, 234967}, NULL) = 0
gettimeofday({1419787571, 235064}, NULL) = 0
gettimeofday({1419787571, 235142}, NULL) = 0
gettimeofday({1419787571, 235257}, NULL) = 0
gettimeofday({1419787571, 235394}, NULL) = 0
...
...
write(1, "Before: 1419787571.234967\n", 26) = 26
write(1, "After : 1419787571.976285\n", 26) = 26
exit_group(0) = ?
  • X86_64 platformunda strace ile yaptığımız incelemede, herhangi bir gettimeofday() sistem çağrısı göremedik

  • Beklediğimiz şekilde linux-vdso.so mekanizması sayesinde, işlem tamamen kullanıcı kipinde gerçekleştirildi, herhangi bir sistem çağrısı yapılmadı

  • Sadece printf() fonksiyonu nedeniyle write() sistem çağrısı kullanılarak son çıktı konsola gönderildi

  • ARM mimarisindeki örneğimize baktığımızda, benzeri bir mekanizma olmadığı için, her defasında karşılık gelen bir gettimeofday() sistem çağrısı yapıldığını görüyoruz (örnek ekran çıktımızda ... olarak belirttiğimiz bölümde 100000 adet benzer çağrı bulunmaktadır)

Sistem Çağrılarının Kesintiye Uğraması

Kendimize şu soruyu soralım: uygulamamız bir sistem çağrısı yaparak çekirdek kipinde kod işletiliyor durumunda iken sinyal (software interrupt) gelirse ne olur?

Bu durumda sistem çağrısı sona erecek ve EINTERRUPTED hatası dönecektir.

Sistem çağrılarını glibc üzerinden kullandığımız için, glibc tarafında sistem çağrısından EINTR hatası geldiğinde, uygulamaya geri dönüş değeri olarak -1 dönülür fakat errno global değişkeni EINTR şeklinde ayarlanır.

Bu aslında hata olmayan istisnai durum, zaman zaman pek çok uygulama kodunda gözardı edilmektedir.

Bazı kullanım senaryolarında yukarıdaki senaryo istisnai olmaktan çıkıp, ilgili yazılımın doğası gereği sürekli veya sıklıkla da (read, write, open, connect vb.) oluşabilir.

Uygulama perspektifinden baktığımızda tüm sistem çağrılarını sarmalayan fonksiyonlar için aşağıdaki kural geçerlidir:

  • Eğer bir sistem çağrısının geri dönüş değeri 0'dan küçükse ve errno değişkeni EINTR sabitine eşitse, herhangi bir hata söz konusu değildir.

Bahsedilen senaryo oluştuğunda ilgili fonksiyonun (yani sistem çağrısının) yeniden çağrılması gerekir.

Bu süreç, ilgili sinyallerin oluşturulmasında SA_RESTART bayrağının işaretlenmesi suretiyle otomatik hale getirilebilir. Peki neden öntanımlı olarak bu şekilde değil?

Esasen bir zamanlar öyleydi. Ancak sistem çağrısının otomatik olarak yeniden başlatılmasını istemeyeceğimiz durumlar da olabilir. Bu yüzden öntanımlı olarak bir aksiyon alınmıyor.

results matching ""

    No results matching ""