এই ওয়েবসাইটটি Google Analytics এবং Google Adsense এবং Giscus ব্যবহার করে। আমাদের পড়ুন
শর্তাবলী এবং গোপনীয়তা নীতি
প্রকাশিত

সি সহ লো-লেভেল সিস্টেম প্রোগ্রামিং পার্ট ৪ (৬টির মধ্যে) অ্যাডভান্সড সি টপিকস

লেখক
লেখক
  • avatar
    নাম
    মো: নাসিম শেখ
    টুইটার
    টুইটার
    @nasimStg

অ্যাবস্ট্রাকশনের স্তরগুলি ব্যবহার করার পরিবর্তে সরাসরি অপারেটিং সিস্টেম এবং হার্ডওয়্যার সাথে ইন্টারঅ্যাক্ট করুন। সি-তে লো-লেভেল সিস্টেম প্রোগ্রামিং অতুলনীয় নিয়ন্ত্রণ এবং দক্ষতা প্রদান করে, যা অপারেটিং সিস্টেম, ডিভাইস ড্রাইভার, এমবেডেড সিস্টেম এবং উচ্চ-পারফরম্যান্স অ্যাপ্লিকেশনগুলির মূল ভিত্তি তৈরি করে। উচ্চ-স্তরের ভাষা এবং লাইব্রেরিগুলি সুবিধা প্রদান করলেও, জিনিসগুলি "under the hood" কীভাবে কাজ করে তা বোঝা আপনাকে অবিশ্বাস্যভাবে অপ্টিমাইজড এবং শক্তিশালী কোড লিখতে সক্ষম করে। এই আর্টিকেলটি সি ভাষা ব্যবহার করে লো-লেভেল সিস্টেম প্রোগ্রামিংয়ের মৌলিক বিষয়গুলি অন্বেষণ করে, যা সিস্টেম কল, ফাইল ডিসক্রিপ্টর এবং সরাসরি মেমরি অ্যাক্সেসের উপর দৃষ্টি নিবদ্ধ করে।

সুচিপত্র

লো-লেভেল সিস্টেম প্রোগ্রামিং কি?

লো-লেভেল সিস্টেম প্রোগ্রামিং বলতে এমন কোড লেখাকে বোঝায় যা স্ট্যান্ডার্ড লাইব্রেরি বা রানটাইম এনভায়রনমেন্ট দ্বারা সরবরাহ করা অনেক অ্যাবস্ট্রাকশন বাইপাস করে সরাসরি অপারেটিং সিস্টেমের কার্নেল বা এমনকি হার্ডওয়্যার কম্পোনেন্টগুলির সাথে ইন্টারঅ্যাক্ট করে। নিম্নলিখিত কারণে সি এই কাজের জন্য বিশেষভাবে উপযুক্ত:

  1. ন্যূনতম রানটাইম: অন্যান্য অনেক ভাষার তুলনায় সি প্রোগ্রামগুলিতে খুব কম ওভারহেড থাকে।
  2. পয়েন্টার অ্যারিথমেটিক: সরাসরি মেমরি ম্যানিপুলেশন করার অনুমতি দেয়।
  3. হার্ডওয়্যারের সাথে ঘনিষ্ঠ ম্যাপিং: সি কনস্ট্রাক্টগুলি প্রায়শই সরাসরি মেশিন নির্দেশাবলীতে অনুবাদ হয়।
  4. পোর্টেবিলিটি (সতর্কতা সহ): সি স্ট্যান্ডার্ড লাইব্রেরি পোর্টেবল হলেও, সিস্টেম-লেভেল কলগুলি প্রায়শই OS-নির্দিষ্ট হয় (যেমন, POSIX বনাম Windows API)।

সাধারণত কাজগুলির মধ্যে মেমরি এবং প্রসেসের মতো রিসোর্স পরিচালনা করা, সিস্টেম কল ব্যবহার করে I/O অপারেশন সম্পাদন করা এবং কখনও কখনও সরাসরি হার্ডওয়্যার রেজিস্টার ম্যানিপুলেশন করা (বিশেষ করে এমবেডেড সিস্টেমে) অন্তর্ভুক্ত থাকে।

গেটওয়ে: সিস্টেম কল (Syscalls)

ইউজার-স্পেস অ্যাপ্লিকেশনগুলি অপারেটিং সিস্টেম কার্নেলের কাছ থেকে পরিষেবা অনুরোধ করার প্রাথমিক উপায় হল সিস্টেম কল। এগুলি কার্নেল দ্বারা সরবরাহ করা ফাংশন যা অ্যাপ্লিকেশনগুলি ফাইল I/O, প্রসেস তৈরি, নেটওয়ার্ক যোগাযোগ এবং মেমরি ব্যবস্থাপনার মতো প্রিভিলেজড অপারেশন সম্পাদন করতে আহ্বান করতে পারে।

স্ট্যান্ডার্ড সি লাইব্রেরি ফাংশন (যেমন printf, fopen, malloc) প্রায়শই এই অন্তর্নিহিত সিস্টেম কলগুলির চারপাশে রেপার হিসাবে কাজ করে। তারা একটি আরও পোর্টেবল এবং প্রায়শই ব্যবহার করা সহজ ইন্টারফেস সরবরাহ করে। তবে, লো-লেভেল নিয়ন্ত্রণের জন্য, আপনাকে সরাসরি সিস্টেম কল ব্যবহার করার প্রয়োজন হতে পারে।

উদাহরণ: সিস্টেম কল ব্যবহার করে ফাইল I/O (পোসিক্স)

চলুন একটি POSIX-কম্প্লায়েন্ট সিস্টেমে (যেমন Linux বা macOS) fopen/fwrite (স্ট্যান্ডার্ড লাইব্রেরি) বনাম open/write (সিস্টেম কল) তুলনা করি।

স্ট্যান্ডার্ড লাইব্রেরি (stdio.h)

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

int main() {
    FILE *fp;
    char *message = "Hello from stdio!n";

    // Open file for writing (creates if not exists, truncates if exists)
    fp = fopen("stdio_example.txt", "w");
    if (fp == NULL) {
        perror("fopen failed");
        return 1;
    }

    // Write data
    size_t written = fwrite(message, sizeof(char), strlen(message), fp);
    if (written < strlen(message)) {
        perror("fwrite failed");
        fclose(fp); // Close file on error
        return 1;
    }

    printf("Successfully wrote %zu bytes using fwrite.n", written);

    // Close file
    if (fclose(fp) != 0) {
        perror("fclose failed");
        return 1;
    }

    return 0;
}

সিস্টেম কল (fcntl.h, unistd.h)

#include <stdio.h>      // For perror
#include <string.h>     // For strlen
#include <fcntl.h>      // For open() flags (O_WRONLY, O_CREAT, O_TRUNC)
#include <unistd.h>     // For open(), write(), close() syscalls
#include <sys/stat.h>   // For mode constants (S_IRUSR, S_IWUSR)

int main() {
    int fd; // File descriptor (an integer)
    char *message = "Hello from syscalls!n";
    ssize_t bytes_written;

    // Open file for writing
    // O_WRONLY: Write-only
    // O_CREAT: Create if it doesn't exist
    // O_TRUNC: Truncate to zero length if it exists
    // 0644 (octal) or S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH: File permissions
    fd = open("syscall_example.txt", O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
    if (fd == -1) { // open returns -1 on error
        perror("open failed");
        return 1;
    }

    // Write data
    bytes_written = write(fd, message, strlen(message));
    if (bytes_written == -1) { // write returns -1 on error
        perror("write failed");
        close(fd); // Close file descriptor on error
        return 1;
    }
     if (bytes_written < strlen(message)) {
        fprintf(stderr, "Warning: Partial write occurred.n");
    }


    printf("Successfully wrote %zd bytes using write syscall.n", bytes_written);

    // Close file descriptor
    if (close(fd) == -1) { // close returns -1 on error
        perror("close failed");
        return 1;
    }

    return 0;
}

মূল পার্থক্য:

_ ইন্টারফেস: stdio.h FILE_পয়েন্টার ব্যবহার করে (স্ট্রাকচার যাতে বাফার তথ্য ইত্যাদি থাকে), অন্যদিকে সিস্টেম কলগুলি ইন্টিজার ফাইল ডিসক্রিপ্টর ব্যবহার করে। * বাফারিং:stdio.hফাংশনগুলি সাধারণত বাফার করা হয় (অনেক ছোট লেখা/পড়ার জন্য পারফরম্যান্স উন্নত করে), অন্যদিকেwrite-এর মতো সিস্টেম কলগুলি প্রায়শই সরাসরি OS-এ যায় (যদিও OS এর নিজস্ব ক্যাশিং থাকে)। * পোর্টেবিলিটি: stdio.h সি স্ট্যান্ডার্ডের অংশ এবং অত্যন্ত পোর্টেবল। সিস্টেম কলগুলি (open, write, read, closeinunistd.h) POSIX স্ট্যান্ডার্ডের অংশ, যা ইউনিউক্স-সদৃশ সিস্টেমগুলিতে সাধারণ, তবে Windows-এ ভিন্ন (যা windows.hথেকেCreateFile, WriteFile-এর মতো ফাংশন ব্যবহার করে)। * নিয়ন্ত্রণ: সিস্টেম কলগুলি open কলের সময় সরাসরি ফ্ল্যাগ (যেমন, নন-ব্লকিং I/O, ডাইরেক্ট I/O) এবং পারমিশনগুলির উপর আরও ফাইন-গ্রেইনড নিয়ন্ত্রণ প্রদান করে।

ফাইল ডিসক্রিপ্টর নিয়ে কাজ করা

উপরে দেখা গেছে, সিস্টেম কলগুলি ফাইল ডিসক্রিপ্টর (FDs) ব্যবহার করে কাজ করে। একটি FD হল একটি ছোট, নন-নেগেটিভ ইন্টিজার যা কার্নেল একটি খোলা ফাইল, সকেট, পাইপ বা অন্যান্য I/O রিসোর্স সনাক্ত করতে ব্যবহার করে।

_ নিয়ম অনুযায়ী, প্রথম তিনটি FD প্রায়শই পূর্ব-নির্ধারিত থাকে:       _ 0: স্ট্যান্ডার্ড ইনপুট (stdin)       _ 1: স্ট্যান্ডার্ড আউটপুট (stdout)       _ 2: স্ট্যান্ডার্ড ত্রুটি (stderr)

আপনি এই ইন্টিজার ডিসক্রিপ্টরগুলির সাথে সরাসরি read, write, lseek (ফাইল অবস্থান পরিবর্তন করতে), fcntl (FD বৈশিষ্ট্য ম্যানিপুলেট করতে), এবং close-এর মতো সিস্টেম কল ব্যবহার করতে পারেন।

#include <unistd.h>
#include <string.h>
#include <stdio.h> // for perror

int main() {
    char *msg = "Writing directly to standard output (FD 1).n";
    ssize_t written = write(1, msg, strlen(msg)); // Write to stdout
    if (written == -1) {
        perror("write to stdout failed");
        return 1;
    }
    return 0;
}

সরাসরি মেমরি ম্যানিপুলেশন এবং mmap

malloc এবং free স্ট্যান্ডার্ড লাইব্রেরির মাধ্যমে হিপ মেমরি পরিচালনা করলেও, লো-লেভেল প্রোগ্রামিং প্রায়শই আরও সরাসরি মেমরি নিয়ন্ত্রণ করে। একটি শক্তিশালী টুল হল mmap সিস্টেম কল (মেমরি ম্যাপ)।

mmap আপনাকে ফাইল বা ডিভাইসগুলিকে সরাসরি প্রসেসের অ্যাড্রেস স্পেসে ম্যাপ করার অনুমতি দেয়। এর কয়েকটি ব্যবহার রয়েছে:

  1. ফাইল I/O: read/write-এর পরিবর্তে, আপনি মেমরিতে একটি ফাইল ম্যাপ করতে পারেন এবং পয়েন্টার অ্যারিথমেটিক ব্যবহার করে সরাসরি এর বিষয়বস্তু অ্যাক্সেস করতে পারেন। এটি বৃহৎ ফাইল বা র্যান্ডম অ্যাক্সেস প্যাটার্নের জন্য অত্যন্ত কার্যকর হতে পারে, কারণ OS চাহিদা অনুযায়ী পেজ লোড করা পরিচালনা করে।
  2. শেয়ার্ড মেমরি: একাধিক প্রসেস একই ফাইল (বা একটি অ্যানোনিমাস ম্যাপিং) তাদের অ্যাড্রেস স্পেসে ম্যাপ করতে পারে, যা কার্যকর ইন্টার-প্রসেস কমিউনিকেশন (IPC) সক্ষম করে।
  3. ডিভাইস অ্যাক্সেস: কিছু সিস্টেমে, হার্ডওয়্যার ডিভাইস রেজিস্টার মেমরি-ম্যাপ করা যেতে পারে, যা ইউজার স্পেস থেকে সরাসরি নিয়ন্ত্রণের অনুমতি দেয় (প্রায়শই বিশেষ পারমিশন প্রয়োজন)।

উদাহরণ: ফাইল পড়ার জন্য mmap ব্যবহার করা (সরলীকৃত)

#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>   // For mmap, munmap
#include <sys/stat.h>   // For stat
#include <fcntl.h>      // For open
#include <unistd.h>     // For close, fstat

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "Usage: %s <filename>n", argv[0]);
        return 1;
    }

    const char *filename = argv[1];
    int fd;
    struct stat sb; // To get file size
    char *mapped_mem;

    // 1. Open the file
    fd = open(filename, O_RDONLY);
    if (fd == -1) {
        perror("open failed");
        return 1;
    }

    // 2. Get file size
    if (fstat(fd, &sb) == -1) {
        perror("fstat failed");
        close(fd);
        return 1;
    }
     // Cannot map empty file
    if (sb.st_size == 0) {
        fprintf(stderr, "File is empty, cannot map.n");
        close(fd);
        return 1;
    }


    // 3. Map the file into memory
    //    NULL: let kernel choose address
    //    sb.st_size: length of mapping
    //    PROT_READ: pages may be read
    //    MAP_PRIVATE: private copy-on-write mapping
    //    fd: file descriptor of file to map
    //    0: offset within the file
    mapped_mem = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
    if (mapped_mem == MAP_FAILED) { // Check for error
        perror("mmap failed");
        close(fd);
        return 1;
    }

    // 4. File descriptor no longer needed after mapping
    if (close(fd) == -1) {
        perror("close failed");
        // proceed, but maybe log warning
    }


    // 5. Access the file content directly via memory pointer
    printf("File content mapped at address %p:n", mapped_mem);
    // Example: Print the first 100 bytes or until end of file
    for (off_t i = 0; i < sb.st_size && i < 100; ++i) {
        putchar(mapped_mem[i]);
    }
     if (sb.st_size > 100) {
         printf("n... (content truncated) ...");
     }
     printf("n");


    // 6. Unmap the memory region
    if (munmap(mapped_mem, sb.st_size) == -1) {
        perror("munmap failed");
        return 1; // Even on munmap failure, memory might be leaked
    }

    return 0;
}

লো-লেভেল প্রোগ্রামিংয়ে বিবেচ্য বিষয়

_ ত্রুটি পরিচালনা: সিস্টেম কলগুলি প্রায়শই ত্রুটিতে -1 রিটার্ন করে এবং গ্লোবাল errno ভ্যারিয়েবল সেট করে। আপনার রিটার্ন ভ্যালুগুলি নিখুঁতভাবে চেক করতে হবে এবং সমস্যা নির্ণয়ের জন্য perror বা strerror(errno) ব্যবহার করতে হবে।   _ রিসোর্স ব্যবস্থাপনা: ফাইল ডিসক্রিপ্টর বন্ধ করার (close), মেমরি আনম্যাপ করার (munmap), এবং প্রসেস লাইফটাইম স্পষ্টভাবে পরিচালনা করার জন্য আপনি দায়ী। লিক তৈরি করা সহজ।   _ পোর্টেবিলিটি: POSIX সিস্টেম কল ব্যবহার করে লেখা কোড Windows-এ সরাসরি কম্পাইল বা রান হবে না, এবং এর বিপরীতটিও সত্য। ক্রস-প্ল্যাটফর্ম কোডের জন্য কন্ডিশনাল কম্পাইলেশন (#ifdef _WIN32...) বা অ্যাবস্ট্রাকশন স্তরের প্রয়োজন হয়।   _ নিরাপত্তা: সরাসরি সিস্টেম ইন্টারঅ্যাকশন ঝুঁকি বহন করে। ইনপুট ভ্যালিডেশন, সঠিক পারমিশন হ্যান্ডলিং এবং বাফার ওভারফ্লো এড়ানো গুরুত্বপূর্ণ।   * জটিলতা: লো-লেভেল কোড প্রায়শই স্ট্যান্ডার্ড লাইব্রেরি সমতুল্যগুলির চেয়ে বেশি ভার্বোস এবং জটিল হয়।

উপসংহার

সি-তে লো-লেভেল সিস্টেম প্রোগ্রামিং অপারেটিং সিস্টেম কার্নেলের সাথে সরাসরি ইন্টারঅ্যাক্ট করার দরজা খুলে দেয়। open, read, write, close-এর মতো সিস্টেম কল এবং mmap-এর মতো মেমরি ম্যাপিং কৌশলগুলি আয়ত্ত করার মাধ্যমে, আপনি সিস্টেম রিসোর্সগুলির উপর ফাইন-গ্রেইনড নিয়ন্ত্রণ অর্জন করেন। এই ক্ষমতা অপারেটিং সিস্টেম, ডিভাইস ড্রাইভার, এমবেডেড অ্যাপ্লিকেশন এবং কর্মক্ষমতা-সংবেদনশীল সফটওয়্যার ডেভেলপ করার জন্য অপরিহার্য, যেখানে দক্ষতা এবং সরাসরি হার্ডওয়্যার/OS ইন্টারঅ্যাকশন সবচেয়ে গুরুত্বপূর্ণ। যদিও এর জন্য সতর্ক ত্রুটি পরিচালনা এবং রিসোর্স ব্যবস্থাপনার প্রয়োজন হয়, তবে অর্জিত শক্তি এবং বোঝাপড়া যেকোনো গুরুতর সি প্রোগ্রামারের জন্য অমূল্য।