บทแนะนำการเรียกระบบ Linux ด้วย C

Linux System Call Tutorial With C



ในบทความล่าสุดของเราเกี่ยวกับ การเรียกระบบ Linux ฉันได้กำหนดการโทรของระบบ อภิปรายถึงสาเหตุที่อาจใช้ในโปรแกรม และเจาะลึกถึงข้อดีและข้อเสีย ฉันยังยกตัวอย่างสั้นๆ ในการชุมนุมภายใน C ซึ่งแสดงให้เห็นประเด็นและอธิบายวิธีการโทร แต่ไม่ได้ผล ไม่ใช่แบบฝึกหัดการพัฒนาที่น่าตื่นเต้นอย่างแน่นอน แต่แสดงให้เห็นประเด็นนี้

ในบทความนี้ เราจะใช้การเรียกระบบจริงเพื่อทำงานจริงในโปรแกรม C ของเรา ขั้นแรก เราจะตรวจสอบว่าคุณต้องการใช้การเรียกของระบบหรือไม่ จากนั้นให้ตัวอย่างโดยใช้การเรียก sendfile() ที่สามารถปรับปรุงประสิทธิภาพการคัดลอกไฟล์ได้อย่างมาก สุดท้าย เราจะพูดถึงบางประเด็นที่ต้องจำในขณะที่ใช้การเรียกระบบ Linux







แม้ว่าจะหลีกเลี่ยงไม่ได้ คุณจะต้องใช้การเรียกระบบในบางจุดในอาชีพการพัฒนา C ของคุณ เว้นแต่คุณจะกำหนดเป้าหมายที่ประสิทธิภาพสูงหรือฟังก์ชันประเภทเฉพาะ ไลบรารี glibc และไลบรารีพื้นฐานอื่นๆ ที่รวมอยู่ในลีนุกซ์รุ่นหลักๆ จะดูแลส่วนใหญ่ ความต้องการของคุณ



ไลบรารีมาตรฐาน glibc จัดเตรียมเฟรมเวิร์กข้ามแพลตฟอร์มที่ได้รับการทดสอบอย่างดีเพื่อเรียกใช้ฟังก์ชันที่อาจต้องใช้การเรียกระบบเฉพาะระบบ ตัวอย่างเช่น คุณสามารถอ่านไฟล์ด้วย fscanf(), fread(), getc() เป็นต้น หรือคุณสามารถใช้ read() การเรียกระบบ Linux ฟังก์ชัน glibc มีคุณสมบัติเพิ่มเติม (เช่น การจัดการข้อผิดพลาดที่ดีขึ้น, ฟอร์แมต IO เป็นต้น) และจะทำงานบนระบบที่รองรับ glibc



ในทางกลับกัน มีบางครั้งที่ประสิทธิภาพที่แน่วแน่และการดำเนินการที่แน่นอนเป็นสิ่งสำคัญ เสื้อคลุมที่ fread() จัดเตรียมไว้จะเพิ่มโอเวอร์เฮด และถึงแม้จะเล็กน้อย แต่ก็ไม่โปร่งใสทั้งหมด นอกจากนี้ คุณอาจไม่ต้องการหรือต้องการคุณสมบัติพิเศษที่ Wrapper มีให้ ในกรณีนั้น คุณจะได้รับบริการที่ดีที่สุดด้วยการเรียกระบบ





คุณยังสามารถใช้การเรียกของระบบเพื่อใช้งานฟังก์ชันที่ glibc ยังไม่รองรับได้ หากสำเนา glibc ของคุณเป็นเวอร์ชันล่าสุด สิ่งนี้แทบจะไม่เป็นปัญหา แต่การพัฒนาบนเวอร์ชันเก่าที่มีเมล็ดที่ใหม่กว่าอาจต้องใช้เทคนิคนี้

เมื่อคุณได้อ่านข้อจำกัดความรับผิดชอบ คำเตือน และทางเลี่ยงที่อาจเกิดขึ้นแล้ว มาดูตัวอย่างการใช้งานจริงกัน



เรากำลังใช้ CPU อะไรอยู่?

คำถามที่โปรแกรมส่วนใหญ่ไม่คิดว่าจะถาม แต่ก็เป็นคำถามที่ถูกต้อง นี่คือตัวอย่างของการเรียกระบบที่ไม่สามารถทำซ้ำด้วย glibc และไม่ได้ครอบคลุมด้วย glibc wrapper ในโค้ดนี้ เราจะเรียกการเรียก getcpu() โดยตรงผ่านฟังก์ชัน syscall() ฟังก์ชัน syscall ทำงานดังนี้:

syscall(SYS_call,arg1,arg2,...);

อาร์กิวเมนต์แรก SYS_call เป็นคำจำกัดความที่แสดงจำนวนการเรียกของระบบ เมื่อคุณรวม sys/syscall.h ไว้ สิ่งเหล่านี้จะถูกรวมไว้ด้วย ส่วนแรกคือ SYS_ และส่วนที่สองคือชื่อของการเรียกระบบ

อาร์กิวเมนต์สำหรับการโทรไปที่ arg1, arg2 ด้านบน การโทรบางรายการจำเป็นต้องมีการโต้แย้งมากขึ้นและจะดำเนินการต่อตามลำดับจากหน้าคน โปรดจำไว้ว่าอาร์กิวเมนต์ส่วนใหญ่ โดยเฉพาะอย่างยิ่งสำหรับผลตอบแทน จะต้องมีตัวชี้ไปยังอาร์เรย์ถ่านหรือหน่วยความจำที่จัดสรรผ่านฟังก์ชัน malloc

example1.c

#รวม
#รวม
#รวม
#รวม

intหลัก() {

ไม่ได้ลงนามซีพียู,โหนด;

// รับแกน CPU ปัจจุบันและโหนด NUMA ผ่านการเรียกระบบ
// โปรดทราบว่าไม่มีตัวห่อหุ้ม glibc ดังนั้นเราต้องเรียกมันโดยตรง
syscall(SYS_getcpu, &ซีพียู, &โหนด,โมฆะ);

// แสดงข้อมูล
printf ('โปรแกรมนี้กำลังทำงานบน CPU core %u และ NUMA node %uNSNS',ซีพียู,โหนด);

กลับ 0;

}

เพื่อคอมไพล์และรัน:

ตัวอย่าง gcc1. -o ตัวอย่าง1
./ตัวอย่าง1

เพื่อผลลัพธ์ที่น่าสนใจยิ่งขึ้น คุณสามารถหมุนเธรดผ่านไลบรารี pthreads แล้วเรียกใช้ฟังก์ชันนี้เพื่อดูว่าเธรดของคุณทำงานบนโปรเซสเซอร์ตัวใด

Sendfile: ประสิทธิภาพที่เหนือกว่า

Sendfile ให้ตัวอย่างที่ยอดเยี่ยมในการเพิ่มประสิทธิภาพผ่านการเรียกระบบ ฟังก์ชัน sendfile() คัดลอกข้อมูลจาก file descriptor หนึ่งไปยังอีกไฟล์หนึ่ง แทนที่จะใช้หลายฟังก์ชัน fread() และ fwrite() sendfile จะทำการถ่ายโอนในพื้นที่เคอร์เนล ลดโอเวอร์เฮดและเพิ่มประสิทธิภาพการทำงาน

ในตัวอย่างนี้ เราจะคัดลอกข้อมูล 64 MB จากไฟล์หนึ่งไปยังอีกไฟล์หนึ่ง ในการทดสอบหนึ่งครั้ง เราจะใช้วิธีการอ่าน/เขียนมาตรฐานในไลบรารีมาตรฐาน ในอีกทางหนึ่ง เราจะใช้การเรียกของระบบและการเรียก sendfile() เพื่อกระจายข้อมูลนี้จากที่หนึ่งไปยังอีกที่หนึ่ง

test1.c (glibc)

#รวม
#รวม
#รวม
#รวม

#define BUFFER_SIZE 67108864
#define BUFFER_1 'บัฟเฟอร์1'
#define BUFFER_2 'บัฟเฟอร์2'

intหลัก() {

ไฟล์*ผิด, *จบ;

printf ('NSการทดสอบ I/O ด้วยฟังก์ชัน glibc แบบดั้งเดิมNSNS');

// หยิบบัฟเฟอร์ BUFFER_SIZE
// บัฟเฟอร์จะมีข้อมูลสุ่มอยู่ในนั้น แต่เราไม่สนใจเรื่องนั้น
printf ('การจัดสรรบัฟเฟอร์ 64 MB:');
char *กันชน= (char *) malloc (BUFFER_SIZE);
printf ('เสร็จแล้วNS');

// เขียนบัฟเฟอร์ไปที่ fOut
printf ('กำลังเขียนข้อมูลไปยังบัฟเฟอร์แรก:');
ผิด= fopen (BUFFER_1, 'wb');
fwrite (กันชน, ขนาดของ(char),BUFFER_SIZE,ผิด);
fclose (ผิด);
printf ('เสร็จแล้วNS');

printf ('กำลังคัดลอกข้อมูลจากไฟล์แรกไปยังไฟล์ที่สอง:');
จบ= fopen (BUFFER_1, 'อาร์บี');
ผิด= fopen (BUFFER_2, 'wb');
ขนมปัง (กันชน, ขนาดของ(char),BUFFER_SIZE,จบ);
fwrite (กันชน, ขนาดของ(char),BUFFER_SIZE,ผิด);
fclose (จบ);
fclose (ผิด);
printf ('เสร็จแล้วNS');

printf ('การปลดปล่อยบัฟเฟอร์: ');
ฟรี (กันชน);
printf ('เสร็จแล้วNS');

printf ('กำลังลบไฟล์:');
ลบ (BUFFER_1);
ลบ (BUFFER_2);
printf ('เสร็จแล้วNS');

กลับ 0;

}

test2.c (การเรียกของระบบ)

#รวม
#รวม
#รวม
#รวม
#รวม
#รวม
#รวม
#รวม
#รวม

#define BUFFER_SIZE 67108864

intหลัก() {

intผิด,จบ;

printf ('NSทดสอบ I/O ด้วย sendfile() และการเรียกระบบที่เกี่ยวข้องNSNS');

// หยิบบัฟเฟอร์ BUFFER_SIZE
// บัฟเฟอร์จะมีข้อมูลสุ่มอยู่ในนั้น แต่เราไม่สนใจเรื่องนั้น
printf ('การจัดสรรบัฟเฟอร์ 64 MB:');
char *กันชน= (char *) malloc (BUFFER_SIZE);
printf ('เสร็จแล้วNS');


// เขียนบัฟเฟอร์ไปที่ fOut
printf ('กำลังเขียนข้อมูลไปยังบัฟเฟอร์แรก:');
ผิด=เปิด('บัฟเฟอร์1',O_RDONLY);
เขียน(ผิด, &กันชน,BUFFER_SIZE);
ปิด(ผิด);
printf ('เสร็จแล้วNS');

printf ('กำลังคัดลอกข้อมูลจากไฟล์แรกไปยังไฟล์ที่สอง:');
จบ=เปิด('บัฟเฟอร์1',O_RDONLY);
ผิด=เปิด('บัฟเฟอร์2',O_RDONLY);
sendfile(ผิด,จบ, 0,BUFFER_SIZE);
ปิด(จบ);
ปิด(ผิด);
printf ('เสร็จแล้วNS');

printf ('การปลดปล่อยบัฟเฟอร์: ');
ฟรี (กันชน);
printf ('เสร็จแล้วNS');

printf ('กำลังลบไฟล์:');
ยกเลิกการลิงก์('บัฟเฟอร์1');
ยกเลิกการลิงก์('บัฟเฟอร์2');
printf ('เสร็จแล้วNS');

กลับ 0;

}

การรวบรวมและดำเนินการทดสอบ 1 & 2

ในการสร้างตัวอย่างเหล่านี้ คุณจะต้องติดตั้งเครื่องมือสำหรับการพัฒนาในการเผยแพร่ของคุณ บน Debian และ Ubuntu คุณสามารถติดตั้งสิ่งนี้ด้วย:

ฉลาดติดตั้งbuild-essentials

จากนั้นคอมไพล์ด้วย:

gcctest1.c-หรือทดสอบ1&& gcctest2.c-หรือทดสอบ2

ในการรันทั้งสองอย่างและทดสอบประสิทธิภาพ ให้รัน:

เวลา./ทดสอบ1&& เวลา./ทดสอบ2

คุณควรได้ผลลัพธ์ดังนี้:

การทดสอบ I/O ด้วยฟังก์ชัน glibc แบบดั้งเดิม

การจัดสรรบัฟเฟอร์ 64 MB: DONE
กำลังเขียนข้อมูลไปยังบัฟเฟอร์แรก: DONE
กำลังคัดลอกข้อมูลจากไฟล์แรกไปยังไฟล์ที่สอง: DONE
บัฟเฟอร์อิสระ: DONE
กำลังลบไฟล์: DONE
จริง 0m0.397s
ผู้ใช้ 0m0.000s
sys 0m0.203s
ทดสอบ I/O ด้วย sendfile() และการเรียกระบบที่เกี่ยวข้อง
การจัดสรรบัฟเฟอร์ 64 MB: DONE
กำลังเขียนข้อมูลไปยังบัฟเฟอร์แรก: DONE
กำลังคัดลอกข้อมูลจากไฟล์แรกไปยังไฟล์ที่สอง: DONE
บัฟเฟอร์อิสระ: DONE
กำลังลบไฟล์: DONE
จริง 0m0.019s
ผู้ใช้ 0m0.000s
sys 0m0.016s

อย่างที่คุณเห็น โค้ดที่ใช้การเรียกของระบบทำงานเร็วกว่า glibc ที่เทียบเท่ากันมาก

สิ่งที่ต้องจำ

การเรียกระบบสามารถเพิ่มประสิทธิภาพและให้ฟังก์ชันการทำงานเพิ่มเติมได้ แต่ก็ไม่มีข้อเสีย คุณจะต้องชั่งน้ำหนักการเรียกระบบของประโยชน์ที่ได้รับจากการขาดความสามารถในการพกพาของแพลตฟอร์มและฟังก์ชันการทำงานที่ลดลงในบางครั้งเมื่อเทียบกับฟังก์ชันของไลบรารี

เมื่อใช้การเรียกระบบ คุณต้องระมัดระวังในการใช้รีซอร์สที่ส่งคืนจากการเรียกของระบบ แทนที่จะใช้ฟังก์ชันไลบรารี ตัวอย่างเช่น โครงสร้าง FILE ที่ใช้สำหรับฟังก์ชัน fopen(), fread(), fwrite() และ fclose() ของ glibc ไม่เหมือนกับหมายเลข file descriptor จากการเรียกระบบ open() (ส่งคืนเป็นจำนวนเต็ม) การผสมสิ่งเหล่านี้อาจทำให้เกิดปัญหาได้

โดยทั่วไป การเรียกระบบ Linux มีเลนบัมเปอร์น้อยกว่าฟังก์ชัน glibc แม้ว่าการเรียกของระบบจะมีการจัดการและการรายงานข้อผิดพลาดอยู่บ้าง แต่คุณจะได้รับฟังก์ชันการทำงานที่มีรายละเอียดมากขึ้นจากฟังก์ชัน glibc

และสุดท้าย คำเกี่ยวกับความปลอดภัย การเรียกระบบจะติดต่อกับเคอร์เนลโดยตรง เคอร์เนลลินุกซ์มีการป้องกันที่กว้างขวางต่อคนฉ้อฉลจากดินแดนของผู้ใช้ แต่มีข้อบกพร่องที่ยังไม่ได้ค้นพบ อย่าวางใจว่าการเรียกของระบบจะตรวจสอบข้อมูลที่คุณป้อนหรือแยกคุณออกจากปัญหาด้านความปลอดภัย ก็ควรที่จะตรวจสอบให้แน่ใจว่าข้อมูลที่คุณส่งไปยังการเรียกระบบนั้นสะอาดแล้ว นี่เป็นคำแนะนำที่ดีสำหรับการเรียก API ใดๆ แต่คุณไม่สามารถระวังเมื่อทำงานกับเคอร์เนลได้

ฉันหวังว่าคุณจะสนุกกับการดำน้ำลึกลงไปในดินแดนของการเรียกระบบ Linux สำหรับรายการทั้งหมดของ Linux System Calls โปรดดูรายการหลักของเรา