ในบทความนี้ เราจะใช้การเรียกระบบจริงเพื่อทำงานจริงในโปรแกรม 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 โปรดดูรายการหลักของเรา