Hướng dẫn cuộc gọi hệ thống Linux với C

Linux System Call Tutorial With C



Trong bài viết cuối cùng của chúng tôi về Cuộc gọi hệ thống Linux , Tôi đã xác định một lệnh gọi hệ thống, thảo luận về những lý do người ta có thể sử dụng chúng trong một chương trình và đi sâu vào những ưu điểm và nhược điểm của chúng. Tôi thậm chí còn đưa ra một ví dụ ngắn gọn về assembly trong C. Nó minh họa điểm và mô tả cách thực hiện cuộc gọi, nhưng không có tác dụng gì. Không hẳn là một bài tập phát triển gay cấn, nhưng nó đã minh họa cho vấn đề này.

Trong bài viết này, chúng tôi sẽ sử dụng các lệnh gọi hệ thống thực tế để thực hiện công việc thực tế trong chương trình C của chúng tôi. Trước tiên, chúng tôi sẽ xem xét xem bạn có cần sử dụng lệnh gọi hệ thống hay không, sau đó cung cấp ví dụ sử dụng lệnh gọi sendfile () có thể cải thiện đáng kể hiệu suất sao chép tệp. Cuối cùng, chúng ta sẽ điểm qua một số điểm cần nhớ khi sử dụng lệnh gọi hệ thống Linux.







Mặc dù không thể tránh khỏi việc bạn sẽ sử dụng lệnh gọi hệ thống tại một số thời điểm trong sự nghiệp phát triển C của mình, trừ khi bạn đang nhắm mục tiêu hiệu suất cao hoặc một chức năng loại cụ thể, thư viện glibc và các thư viện cơ bản khác có trong các bản phân phối Linux chính sẽ đảm nhận phần lớn bạn cần.



Thư viện tiêu chuẩn glibc cung cấp một khuôn khổ đa nền tảng, được thử nghiệm tốt để thực thi các chức năng mà nếu không sẽ yêu cầu các lệnh gọi hệ thống dành riêng cho hệ thống. Ví dụ: bạn có thể đọc tệp với fscanf (), fread (), getc (), v.v. hoặc bạn có thể sử dụng lệnh gọi hệ thống Linux read (). Các hàm glibc cung cấp nhiều tính năng hơn (tức là xử lý lỗi tốt hơn, IO được định dạng, v.v.) và sẽ hoạt động trên bất kỳ hệ thống nào hỗ trợ glibc.



Mặt khác, có những thời điểm mà hiệu suất không thỏa hiệp và thực thi chính xác là rất quan trọng. Trình bao bọc mà fread () cung cấp sẽ thêm chi phí và mặc dù nhỏ nhưng không hoàn toàn minh bạch. Ngoài ra, bạn có thể không muốn hoặc cần các tính năng bổ sung mà trình bao bọc cung cấp. Trong trường hợp đó, bạn được phục vụ tốt nhất với cuộc gọi hệ thống.





Bạn cũng có thể sử dụng lệnh gọi hệ thống để thực hiện các chức năng chưa được glibc hỗ trợ. Nếu bản sao glibc của bạn được cập nhật, điều này sẽ khó xảy ra, nhưng việc phát triển trên các bản phân phối cũ hơn với các hạt nhân mới hơn có thể yêu cầu kỹ thuật này.

Bây giờ bạn đã đọc các tuyên bố từ chối trách nhiệm, cảnh báo và các đường vòng tiềm năng, bây giờ hãy đi sâu vào một số ví dụ thực tế.



Chúng ta đang sử dụng CPU nào?

Một câu hỏi mà hầu hết các chương trình có lẽ không nghĩ đến để hỏi, nhưng dù sao cũng là một câu hợp lệ. Đây là một ví dụ về lệnh gọi hệ thống không thể được sao chép với glibc và không được bao phủ bởi một trình bao bọc glibc. Trong mã này, chúng tôi sẽ gọi lệnh gọi getcpu () trực tiếp thông qua hàm syscall (). Hàm syscall hoạt động như sau:

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

Đối số đầu tiên, SYS_call, là một định nghĩa đại diện cho số của lệnh gọi hệ thống. Khi bạn bao gồm sys / syscall.h, chúng sẽ được bao gồm. Phần đầu tiên là SYS_ và phần thứ hai là tên của lệnh gọi hệ thống.

Lập luận cho cuộc gọi đi vào arg1, arg2 ở trên. Một số cuộc gọi yêu cầu nhiều đối số hơn và chúng sẽ tiếp tục theo thứ tự từ trang người của họ. Hãy nhớ rằng hầu hết các đối số, đặc biệt là đối với trả về, sẽ yêu cầu con trỏ đến mảng char hoặc bộ nhớ được cấp phát qua hàm malloc.

example1.c

#bao gồm
#bao gồm
#bao gồm
#bao gồm

NSchủ chốt() {

chưa kýCPU,nút;

// Nhận lõi CPU hiện tại và nút NUMA thông qua lệnh gọi hệ thống
// Lưu ý rằng cái này không có trình bao bọc glibc nên chúng ta phải gọi nó trực tiếp
syscall(SYS_getcpu, &CPU, &nút,VÔ GIÁ TRỊ);

// Hiển thị thông tin
printf ('Chương trình này đang chạy trên lõi CPU% u và nút NUMA% u. ',CPU,nút);

trở lại 0;

}

Để biên dịch và chạy:

ví dụ gcc1.NS -o example1
./ví dụ 1

Để có kết quả thú vị hơn, bạn có thể quay các luồng thông qua thư viện pthreads và sau đó gọi hàm này để xem luồng của bạn đang chạy trên bộ xử lý nào.

Sendfile: Hiệu suất vượt trội

Sendfile cung cấp một ví dụ tuyệt vời về việc nâng cao hiệu suất thông qua các cuộc gọi hệ thống. Hàm sendfile () sao chép dữ liệu từ bộ mô tả tệp này sang bộ mô tả tệp khác. Thay vì sử dụng nhiều hàm fread () và fwrite (), sendfile thực hiện truyền trong không gian hạt nhân, giảm chi phí và do đó tăng hiệu suất.

Trong ví dụ này, chúng tôi sẽ sao chép 64 MB dữ liệu từ tệp này sang tệp khác. Trong một thử nghiệm, chúng tôi sẽ sử dụng các phương pháp đọc / ghi tiêu chuẩn trong thư viện tiêu chuẩn. Mặt khác, chúng tôi sẽ sử dụng lệnh gọi hệ thống và lệnh gọi sendfile () để truyền dữ liệu này từ vị trí này sang vị trí khác.

test1.c (glibc)

#bao gồm
#bao gồm
#bao gồm
#bao gồm

#define BUFFER_SIZE 67108864
#define BUFFER_1 'buffer1'
#define BUFFER_2 'buffer2'

NSchủ chốt() {

TẬP TIN*Sai lầm, *kết thúc;

printf (' Kiểm tra I / O với các chức năng glibc truyền thống. ');

// Lấy bộ đệm BUFFER_SIZE.
// Bộ đệm sẽ có dữ liệu ngẫu nhiên trong đó nhưng chúng ta không quan tâm đến điều đó.
printf ('Phân bổ bộ đệm 64 MB:');
char *đệm= (char *) malloc (BUFFER_SIZE);
printf ('XONG ');

// Ghi bộ đệm vào fOut
printf ('Ghi dữ liệu vào bộ đệm đầu tiên:');
Sai lầm= fopen (BUFFER_1, 'wb');
fwrite (đệm, kích thước(char),BUFFER_SIZE,Sai lầm);
fclose (Sai lầm);
printf ('XONG ');

printf ('Sao chép dữ liệu từ tệp đầu tiên sang tệp thứ hai:');
kết thúc= fopen (BUFFER_1, 'rb');
Sai lầm= fopen (BUFFER_2, 'wb');
bánh mì (đệm, kích thước(char),BUFFER_SIZE,kết thúc);
fwrite (đệm, kích thước(char),BUFFER_SIZE,Sai lầm);
fclose (kết thúc);
fclose (Sai lầm);
printf ('XONG ');

printf ('Giải phóng bộ đệm:');
miễn phí (đệm);
printf ('XONG ');

printf ('Xóa tệp:');
tẩy (BUFFER_1);
tẩy (BUFFER_2);
printf ('XONG ');

trở lại 0;

}

test2.c (lệnh gọi hệ thống)

#bao gồm
#bao gồm
#bao gồm
#bao gồm
#bao gồm
#bao gồm
#bao gồm
#bao gồm
#bao gồm

#define BUFFER_SIZE 67108864

NSchủ chốt() {

NSSai lầm,kết thúc;

printf (' Kiểm tra I / O với sendfile () và các lệnh gọi hệ thống liên quan. ');

// Lấy bộ đệm BUFFER_SIZE.
// Bộ đệm sẽ có dữ liệu ngẫu nhiên trong đó nhưng chúng ta không quan tâm đến điều đó.
printf ('Phân bổ bộ đệm 64 MB:');
char *đệm= (char *) malloc (BUFFER_SIZE);
printf ('XONG ');


// Ghi bộ đệm vào fOut
printf ('Ghi dữ liệu vào bộ đệm đầu tiên:');
Sai lầm=mở ra('buffer1',O_RDONLY);
viết(Sai lầm, &đệm,BUFFER_SIZE);
gần(Sai lầm);
printf ('XONG ');

printf ('Sao chép dữ liệu từ tệp đầu tiên sang tệp thứ hai:');
kết thúc=mở ra('buffer1',O_RDONLY);
Sai lầm=mở ra('buffer2',O_RDONLY);
gửi tập tin(Sai lầm,kết thúc, 0,BUFFER_SIZE);
gần(kết thúc);
gần(Sai lầm);
printf ('XONG ');

printf ('Giải phóng bộ đệm:');
miễn phí (đệm);
printf ('XONG ');

printf ('Xóa tệp:');
hủy liên kết('buffer1');
hủy liên kết('buffer2');
printf ('XONG ');

trở lại 0;

}

Biên dịch và chạy các bài kiểm tra 1 & 2

Để xây dựng các ví dụ này, bạn sẽ cần các công cụ phát triển được cài đặt trên bản phân phối của mình. Trên Debian và Ubuntu, bạn có thể cài đặt cái này bằng:

đúng cáchTải vềxây dựng-thiết yếu

Sau đó, biên dịch với:

gcctest1.c-hoặctest1&& gcctest2.c-hoặctest2

Để chạy cả hai và kiểm tra hiệu suất, hãy chạy:

thời gian./test1&& thời gian./test2

Bạn sẽ nhận được kết quả như thế này:

Kiểm tra I / O với các chức năng glibc truyền thống.

Phân bổ bộ đệm 64 MB: XONG
Ghi dữ liệu vào bộ đệm đầu tiên: DONE
Sao chép dữ liệu từ tệp đầu tiên sang tệp thứ hai: DONE
Giải phóng bộ đệm: XONG
Xóa tệp: DONE
thực 0m0.397s
người dùng 0m0.000s
sys 0m0.203s
Kiểm tra I / O với sendfile () và các lệnh gọi hệ thống liên quan.
Phân bổ bộ đệm 64 MB: XONG
Ghi dữ liệu vào bộ đệm đầu tiên: DONE
Sao chép dữ liệu từ tệp đầu tiên sang tệp thứ hai: DONE
Giải phóng bộ đệm: XONG
Xóa tệp: DONE
thực 0m0.019s
người dùng 0m0.000s
sys 0m0.016s

Như bạn có thể thấy, mã sử dụng lệnh gọi hệ thống chạy nhanh hơn nhiều so với mã glibc tương đương.

Những điều cần nhớ

Các cuộc gọi hệ thống có thể tăng hiệu suất và cung cấp chức năng bổ sung, nhưng chúng không phải là không có nhược điểm của chúng. Bạn sẽ phải cân nhắc giữa lợi ích mà các lệnh gọi hệ thống mang lại so với việc thiếu tính di động của nền tảng và đôi khi bị giảm chức năng so với các chức năng thư viện.

Khi sử dụng một số lệnh gọi hệ thống, bạn phải chú ý sử dụng các tài nguyên được trả về từ các lệnh gọi hệ thống hơn là các hàm thư viện. Ví dụ: cấu trúc FILE được sử dụng cho các hàm glibc’s fopen (), fread (), fwrite () và fclose () không giống với số bộ mô tả tệp từ lệnh gọi hệ thống open () (trả về dưới dạng số nguyên). Trộn những thứ này có thể dẫn đến các vấn đề.

Nói chung, các lệnh gọi hệ thống Linux có ít làn đệm hơn các hàm glibc. Mặc dù đúng là các lệnh gọi hệ thống có một số lỗi xử lý và báo cáo, nhưng bạn sẽ nhận được chức năng chi tiết hơn từ hàm glibc.

Và cuối cùng, một từ về bảo mật. Hệ thống gọi giao diện trực tiếp với hạt nhân. Hạt nhân Linux có các biện pháp bảo vệ rộng rãi chống lại những trò lừa bịp từ vùng đất của người dùng, nhưng vẫn tồn tại những lỗi chưa được phát hiện. Đừng tin rằng một lệnh gọi hệ thống sẽ xác thực thông tin đầu vào của bạn hoặc cách ly bạn khỏi các vấn đề bảo mật. Điều khôn ngoan là đảm bảo dữ liệu bạn chuyển cho một cuộc gọi hệ thống đã được khử trùng. Đương nhiên, đây là lời khuyên hữu ích cho bất kỳ lệnh gọi API nào, nhưng bạn không thể không cẩn thận khi làm việc với hạt nhân.

Tôi hy vọng bạn thích thú khi đi sâu hơn vào vùng đất của các lệnh gọi hệ thống Linux. Để có danh sách đầy đủ các Lệnh gọi Hệ thống Linux, hãy xem danh sách chính của chúng tôi.