Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> การเขียนโปรแกรม C

เรียนรู้ Non-Blocking I/O ใน C ด้วย System Call select()

เรียนรู้ Non-Blocking I/O ใน C ด้วย System Call select()

บทนำ

เราทราบดีว่าการดำเนินการ I/O มีบทบาทสำคัญในการอ่านและเขียนไฟล์ แต่การดำเนินการ I/O แบบเดิม (blocking) อาจทำให้โปรแกรมหยุดรอและก่อให้เกิดความล่าช้า วิธีการ Non-blocking I/O สามารถแก้ไขปัญหานี้ได้ เนื่องจาก I/O เป็น non-blocking โปรแกรมจึงสามารถทำงานต่อไปได้ขณะที่ I/O operations กำลังดำเนินการ ฟังก์ชัน select เป็นเครื่องมือที่ใช้บ่อยในภาษา C สำหรับ non-blocking I/O ด้วย select() เราสามารถติดตาม file descriptors หลายตัว เช่น sockets หรือ file handles เพื่อตรวจสอบความพร้อมในการอ่าน/เขียน หรือข้อผิดพลาด ฟังก์ชัน select() ช่วยจัดการงาน I/O หลายอย่างได้อย่างมีประสิทธิภาพโดยไม่บล็อกการทำงานของโปรแกรม

เราจะอภิปรายวิธี implement non-blocking I/O ด้วยฟังก์ชัน select() ในภาษา C รวมถึงการใช้งานพื้นฐานและตัวอย่างโปรแกรม

ฟังก์ชัน Select คืออะไร?

ฟังก์ชัน select เป็นเครื่องมืออันทรงพลังในภาษา C ช่วย implement non-blocking I/O ฟังก์ชันนี้ช่วยให้เราติดตาม file descriptors หลายตัว เช่น sockets หรือ file handles เพื่อตรวจสอบว่าพร้อมอ่านหรือเขียนหรือไม่ ฟังก์ชันรับ 3 ชุดของ file descriptors ได้แก่ read set, write set, และ exception set ผ่านชุดเหล่านี้ เราสามารถระบุได้ว่า descriptors ไหนที่ต้องการติดตามสำหรับการดำเนินการเฉพาะ ฟังก์ชันยังรับค่า timeout เพื่อระบุเวลารอสูงสุดสำหรับเหตุการณ์ เมื่อมีเหตุการณ์เกิดขึ้นบน descriptors ที่ติดตาม หรือ timeout หมดอายุ select() จะคืนค่าและให้ข้อมูลเกี่ยวกับ descriptors ที่พร้อม เราจึงสามารถดำเนินการ I/O ได้อย่างมีประสิทธิภาพโดยไม่บล็อกการทำงานของโปรแกรม

ข้อดีของ select()

  • จัดการหลาย I/O operations ได้อย่างมีประสิทธิภาพโดยไม่ต้องการ thread ต่อ connection ลดการใช้ทรัพยากร
  • รองรับในระบบ Unix/Linux เกือบทุกตัว
  • ใช้งานได้กับ file descriptors ทุกประเภท (files, pipes, sockets, terminals)

ข้อจำกัดของ select()

  • จำนวน file descriptors สูงสุดที่ติดตามได้ จำกัดโดยระบบปฏิบัติการ (มักเป็น FD_SETSIZE = 1024)
  • ประสิทธิภาพลดลงเมื่อจำนวน file descriptors เพิ่มมาก (O(n) scanning)
  • ต้อง rebuild fd_set ทุกครั้งที่เรียก select()

ตัวอย่างโปรแกรม: Non-blocking I/O ด้วย select()

#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main() {
    // File descriptors ที่จะติดตาม
    int fd1, fd2;
    
    // เปิด/สร้างไฟล์และตั้งค่าเป็น non-blocking mode
    fd1 = open("file1.txt", O_RDONLY | O_NONBLOCK);
    fd2 = open("file2.txt", O_WRONLY | O_NONBLOCK | O_CREAT, 0644);
    
    if (fd1 == -1 || fd2 == -1) {
        perror("Error opening files");
        exit(EXIT_FAILURE);
    }

    fd_set read_fds, write_fds;  // File descriptor sets
    struct timeval timeout;      // Timeout สำหรับ select

    while (1) {
        FD_ZERO(&read_fds);   // ล้าง read set
        FD_ZERO(&write_fds);  // ล้าง write set

        FD_SET(fd1, &read_fds);   // เพิ่ม fd1 เข้า read set
        FD_SET(fd2, &write_fds);  // เพิ่ม fd2 เข้า write set
        
        timeout.tv_sec = 4;       // ตั้ง timeout 4 วินาที
        timeout.tv_usec = 0;
        
        int ready_fds = select(fd2 + 1, &read_fds, &write_fds, NULL, &timeout);
        
        if (ready_fds == -1) {
            perror("select");
            exit(EXIT_FAILURE);
        } else if (ready_fds == 0) {
            printf("Timeout occurred\n");
        } else {
            // ตรวจสอบ fd1 พร้อมอ่านหรือไม่
            if (FD_ISSET(fd1, &read_fds)) {
                char buffer[100];
                ssize_t bytesRead = read(fd1, buffer, sizeof(buffer) - 1);
                if (bytesRead > 0) {
                    buffer[bytesRead] = '\0';
                    printf("Read from file1.txt: %s \n", buffer);
                }
            }

            // ตรวจสอบ fd2 พร้อมเขียนหรือไม่
            if (FD_ISSET(fd2, &write_fds)) {
                const char* message = "Good morning";
                ssize_t bytesWritten = write(fd2, message, strlen(message));
                if (bytesWritten > 0) {
                    printf("Wrote to file2.txt: %s \n", message);
                }
            }
        }
    }

    // ปิด file descriptors
    close(fd1);
    close(fd2);
    return 0;
}

ผลลัพธ์การรัน

Wrote to file2.txt: Good morning
Wrote to file2.txt: Good morning
Wrote to file2.txt: Good morning
Wrote to file2.txt: Good morning
Timeout occurred

คำอธิบาย

ในโปรแกรม เรา implement non-blocking I/O ด้วย select() ใน C เพื่อติดตาม 2 ไฟล์คือ "file1.txt" และ "file2.txt" ตั้งค่าไฟล์เป็น non-blocking mode ทำให้โปรแกรมทำงานต่อไปได้โดยไม่ต้องรอจนกว่าไฟล์จะอ่านหรือเขียนเสร็จ ใช้ select() ตรวจสอบกิจกรรมบนไฟล์ภายใน timeout ที่กำหนด ถ้าไม่มีกิจกรรมพิมพ์ "Timeout occurred" ถ้ามีกิจกรรม จะตรวจสอบว่าไฟล์ไหนมีกิจกรรม ถ้า file1.txt มีกิจกรรม โปรแกรมอ่านเนื้อหาและพิมพ์ ถ้า file2.txt มีกิจกรรม จะเขียนข้อความ "Good morning" ลงไฟล์ โปรแกรมติดตามไฟล์ไปเรื่อยๆ จนกว่าจะถูกยกเลิก สุดท้ายปิด file descriptors เพื่อปล่อยทรัพยากรระบบ

ทางเลือกอื่น: poll() และ epoll()

สำหรับแอปพลิเคชันที่ต้องการติดตาม file descriptors จำนวนมาก (high concurrency) ควรพิจารณา:

  • poll() - คล้าย select() แต่ไม่มีข้อจำกัด FD_SETSIZE ใช้โครงสร้าง pollfd array แทน
  • epoll() (Linux-specific) - ประสิทธิภาพสูงกว่ามากสำหรับจำนวน descriptors มาก ใช้ event-driven model O(1) หรือ O(log n)

สรุป

ฟังก์ชัน select() ใน C ให้โซลูชันที่ดีสำหรับ non-blocking I/O operations ด้วยการติดตาม multiple file descriptors ช่วยจัดการหลาย I/O tasks ได้อย่างมีประสิทธิภาพโดยไม่บล็อกการทำงานของโปรแกรม แม้จะมีข้อจำกัด เช่น จำนวน file descriptors สูงสุด และปัญหาประสิทธิภาพกับ descriptors จำนวนมาก แต่ select() ยังคงเป็นตัวเลือกที่ดีสำหรับจัดการ non-blocking I/O ในโปรแกรม C

เกี่ยวกับผู้เขียน

เรียนรู้ Non-Blocking I/O ใน C ด้วย System Call select()

Bamdeb Ghosh

Bamdeb Ghosh มีประสบการณ์ hands-on ในด้าน Wireless networking เป็นผู้เชี่ยวชาญด้านการวิเคราะห์ Wireshark capture บนเครือข่าย Wireless และ Wired พร้อมความรู้เรื่อง Android, Bluetooth, คำสั่ง Linux และ Python ติดตามเว็บไซต์: wifisharks.com