Φροντιστήριο κλήσεων συστήματος Linux με C

Linux System Call Tutorial With C



Στο τελευταίο μας άρθρο για Κλήσεις συστήματος Linux , Καθόρισα μια κλήση συστήματος, συζήτησα τους λόγους που θα μπορούσε κάποιος να τις χρησιμοποιήσει σε ένα πρόγραμμα και ερεύνησα στα πλεονεκτήματα και τα μειονεκτήματά τους. Έδωσα ακόμη και ένα σύντομο παράδειγμα στη συνέλευση εντός του C. Απεικόνισε το σημείο και περιέγραψε πώς να πραγματοποιήσετε την κλήση, αλλά δεν έκανε τίποτα παραγωγικό. Δεν είναι ακριβώς μια συναρπαστική άσκηση ανάπτυξης, αλλά απεικονίζει το νόημα.

Σε αυτό το άρθρο, θα χρησιμοποιήσουμε πραγματικές κλήσεις συστήματος για να κάνουμε πραγματική εργασία στο πρόγραμμα C μας. Αρχικά, θα εξετάσουμε εάν πρέπει να χρησιμοποιήσετε μια κλήση συστήματος και, στη συνέχεια, δώστε ένα παράδειγμα χρησιμοποιώντας την κλήση sendfile () που μπορεί να βελτιώσει δραματικά την απόδοση αντιγραφής αρχείων. Τέλος, θα ξεπεράσουμε ορισμένα σημεία που πρέπει να θυμόμαστε ενώ χρησιμοποιούμε κλήσεις συστήματος Linux.







Παρόλο που είναι αναπόφευκτο, θα χρησιμοποιήσετε μια κλήση συστήματος κάποια στιγμή στην καριέρα σας στην ανάπτυξη του C, εκτός εάν στοχεύετε σε υψηλές επιδόσεις ή λειτουργίες συγκεκριμένου τύπου, η βιβλιοθήκη glibc και άλλες βασικές βιβλιοθήκες που περιλαμβάνονται σε μεγάλες διανομές Linux θα φροντίσουν την πλειοψηφία των οι ανάγκες σου.



Η τυπική βιβλιοθήκη glibc παρέχει ένα πλατφόρμα, καλά δοκιμασμένο πλαίσιο για την εκτέλεση λειτουργιών που διαφορετικά θα απαιτούσαν κλήσεις συστήματος ειδικά για το σύστημα. Για παράδειγμα, μπορείτε να διαβάσετε ένα αρχείο με fscanf (), fread (), getc () κ.λπ. ή μπορείτε να χρησιμοποιήσετε την κλήση συστήματος read () Linux. Οι λειτουργίες glibc παρέχουν περισσότερες δυνατότητες (δηλ. Καλύτερο χειρισμό σφαλμάτων, μορφοποιημένο IO κ.λπ.) και θα λειτουργήσουν σε οποιοδήποτε σύστημα υποστηρίζει glibc.



Από την άλλη πλευρά, υπάρχουν στιγμές όπου η ασυμβίβαστη απόδοση και η ακριβής εκτέλεση είναι κρίσιμης σημασίας. Το περιτύλιγμα που παρέχει το fread () θα προσθέσει γενικά έξοδα, και αν και μικρό, δεν είναι εντελώς διαφανές. Επιπλέον, μπορεί να μην θέλετε ή να χρειάζεστε τις επιπλέον δυνατότητες που παρέχει το περιτύλιγμα. Σε αυτήν την περίπτωση, εξυπηρετείτε καλύτερα με μια κλήση συστήματος.





Μπορείτε επίσης να χρησιμοποιήσετε κλήσεις συστήματος για να εκτελέσετε λειτουργίες που δεν υποστηρίζονται ακόμη από το glibc. Εάν το αντίγραφο του glibc είναι ενημερωμένο, αυτό δεν θα είναι πρόβλημα, αλλά η ανάπτυξη σε παλαιότερες διανομές με νεότερους πυρήνες μπορεί να απαιτεί αυτήν την τεχνική.

Τώρα που διαβάσατε τις αποποιήσεις, τις προειδοποιήσεις και τις πιθανές παρακάμψεις, τώρα ας εμβαθύνουμε σε μερικά πρακτικά παραδείγματα.



Σε ποια CPU είμαστε;

Μια ερώτηση που τα περισσότερα προγράμματα πιθανότατα δεν σκέφτονται να κάνουν, αλλά μια έγκυρη παρόλα αυτά. Αυτό είναι ένα παράδειγμα κλήσης συστήματος που δεν μπορεί να αντιγραφεί με glibc και δεν καλύπτεται με περιτύλιγμα glibc. Σε αυτόν τον κώδικα, θα καλέσουμε την κλήση getcpu () απευθείας μέσω της συνάρτησης syscall (). Η συνάρτηση syscall λειτουργεί ως εξής:

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

Το πρώτο όρισμα, SYS_call, είναι ένας ορισμός που αντιπροσωπεύει τον αριθμό της κλήσης συστήματος. Όταν συμπεριλαμβάνετε sys/syscall.h, αυτά περιλαμβάνονται. Το πρώτο μέρος είναι SYS_ και το δεύτερο μέρος είναι το όνομα της κλήσης συστήματος.

Τα επιχειρήματα για την κλήση μπαίνουν στο arg1, arg2 παραπάνω. Ορισμένες κλήσεις απαιτούν περισσότερα επιχειρήματα και θα συνεχίσουν με τη σειρά από τη σελίδα σελίδων τους. Θυμηθείτε ότι τα περισσότερα ορίσματα, ειδικά για τις επιστροφές, θα απαιτούν δείκτες για συστοιχίες char ή μνήμη που διατίθεται μέσω της συνάρτησης malloc.

παράδειγμα1.γ

#περιλαμβάνω
#περιλαμβάνω
#περιλαμβάνω
#περιλαμβάνω

intκύριος() {

ανυπόγραφοΕΠΕΞΕΡΓΑΣΤΗΣ,κόμβος?

// Λάβετε τον τρέχοντα πυρήνα CPU και τον κόμβο NUMA μέσω κλήσης συστήματος
// Σημειώστε ότι δεν έχει περιτύλιγμα glibc, οπότε πρέπει να το καλέσετε απευθείας
syscall(SYS_getcpu, &ΕΠΕΞΕΡΓΑΣΤΗΣ, &κόμβος,ΜΗΔΕΝΙΚΟ)?

// Εμφάνιση πληροφοριών
printf ('Αυτό το πρόγραμμα εκτελείται στον πυρήνα CPU %u και στον κόμβο NUMA %u. n n',ΕΠΕΞΕΡΓΑΣΤΗΣ,κόμβος)?

ΕΠΙΣΤΡΟΦΗ 0?

}

Για μεταγλώττιση και εκτέλεση:

gcc παράδειγμα 1.ντο -o παράδειγμα 1
Το/παράδειγμα 1

Για πιο ενδιαφέροντα αποτελέσματα, μπορείτε να περιστρέψετε νήματα μέσω της βιβλιοθήκης pthreads και στη συνέχεια να καλέσετε αυτήν τη λειτουργία για να δείτε σε ποιον επεξεργαστή τρέχει το νήμα σας.

Sendfile: Ανώτερη απόδοση

Το Sendfile παρέχει ένα εξαιρετικό παράδειγμα βελτίωσης της απόδοσης μέσω κλήσεων συστήματος. Η συνάρτηση sendfile () αντιγράφει δεδομένα από έναν περιγραφέα αρχείων σε έναν άλλο. Αντί να χρησιμοποιεί πολλαπλές λειτουργίες fread () και fwrite (), το sendfile εκτελεί τη μεταφορά στο χώρο του πυρήνα, μειώνοντας τα γενικά έξοδα και αυξάνοντας έτσι την απόδοση.

Σε αυτό το παράδειγμα, θα αντιγράψουμε 64 MB δεδομένων από το ένα αρχείο στο άλλο. Σε μια δοκιμή, θα χρησιμοποιήσουμε τις τυπικές μεθόδους ανάγνωσης/εγγραφής στην τυπική βιβλιοθήκη. Στην άλλη, θα χρησιμοποιήσουμε τις κλήσεις συστήματος και την κλήση sendfile () για να αναδείξουμε αυτά τα δεδομένα από τη μία τοποθεσία στην άλλη.

test1.c (glibc)

#περιλαμβάνω
#περιλαμβάνω
#περιλαμβάνω
#περιλαμβάνω

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

intκύριος() {

ΑΡΧΕΙΟ*λανθασμένος, *τέλος?

printf (' nΔοκιμή εισόδου/εξόδου με παραδοσιακές λειτουργίες glibc. n n')?

// Πιάστε ένα buffer BUFFER_SIZE.
// Το buffer θα έχει τυχαία δεδομένα, αλλά δεν μας ενδιαφέρει αυτό.
printf ('Κατανομή buffer 64 MB:')?
απανθρακώνω *ρυθμιστής= (απανθρακώνω *) malloc (BUFFER_SIZE)?
printf ('ΕΓΙΝΕ n')?

// Γράψτε το buffer στο fOut
printf ('Γράψιμο δεδομένων στο πρώτο buffer:')?
λανθασμένος= ανοίγω (BUFFER_1, 'wb')?
γράφω (ρυθμιστής, μέγεθος του(απανθρακώνω),BUFFER_SIZE,λανθασμένος)?
κλείσιμο (λανθασμένος)?
printf ('ΕΓΙΝΕ n')?

printf ('Αντιγραφή δεδομένων από το πρώτο αρχείο στο δεύτερο:')?
τέλος= ανοίγω (BUFFER_1, 'rb')?
λανθασμένος= ανοίγω (BUFFER_2, 'wb')?
ψέμα (ρυθμιστής, μέγεθος του(απανθρακώνω),BUFFER_SIZE,τέλος)?
γράφω (ρυθμιστής, μέγεθος του(απανθρακώνω),BUFFER_SIZE,λανθασμένος)?
κλείσιμο (τέλος)?
κλείσιμο (λανθασμένος)?
printf ('ΕΓΙΝΕ n')?

printf ('Αποδέσμευση αποδέσμευσης:')?
Ελεύθερος (ρυθμιστής)?
printf ('ΕΓΙΝΕ n')?

printf ('Διαγραφή αρχείων:')?
αφαιρώ (BUFFER_1)?
αφαιρώ (BUFFER_2)?
printf ('ΕΓΙΝΕ n')?

ΕΠΙΣΤΡΟΦΗ 0?

}

test2.c (κλήσεις συστήματος)

#περιλαμβάνω
#περιλαμβάνω
#περιλαμβάνω
#περιλαμβάνω
#περιλαμβάνω
#περιλαμβάνω
#περιλαμβάνω
#περιλαμβάνω
#περιλαμβάνω

#define BUFFER_SIZE 67108864

intκύριος() {

intλανθασμένος,τέλος?

printf (' nΔοκιμή εισόδου/εξόδου με sendfile () και σχετικές κλήσεις συστήματος. n n')?

// Πιάστε ένα buffer BUFFER_SIZE.
// Το buffer θα έχει τυχαία δεδομένα, αλλά δεν μας ενδιαφέρει αυτό.
printf ('Κατανομή buffer 64 MB:')?
απανθρακώνω *ρυθμιστής= (απανθρακώνω *) malloc (BUFFER_SIZE)?
printf ('ΕΓΙΝΕ n')?


// Γράψτε το buffer στο fOut
printf ('Γράψιμο δεδομένων στο πρώτο buffer:')?
λανθασμένος=Άνοιξε('buffer1',O_RDONLY)?
γράφω(λανθασμένος, &ρυθμιστής,BUFFER_SIZE)?
Κλείσε(λανθασμένος)?
printf ('ΕΓΙΝΕ n')?

printf ('Αντιγραφή δεδομένων από το πρώτο αρχείο στο δεύτερο:')?
τέλος=Άνοιξε('buffer1',O_RDONLY)?
λανθασμένος=Άνοιξε('buffer2',O_RDONLY)?
αποστολή αρχείου(λανθασμένος,τέλος, 0,BUFFER_SIZE)?
Κλείσε(τέλος)?
Κλείσε(λανθασμένος)?
printf ('ΕΓΙΝΕ n')?

printf ('Αποδέσμευση αποδέσμευσης:')?
Ελεύθερος (ρυθμιστής)?
printf ('ΕΓΙΝΕ n')?

printf ('Διαγραφή αρχείων:')?
αποσύνδεση('buffer1')?
αποσύνδεση('buffer2')?
printf ('ΕΓΙΝΕ n')?

ΕΠΙΣΤΡΟΦΗ 0?

}

Συγκέντρωση και εκτέλεση δοκιμών 1 & 2

Για να δημιουργήσετε αυτά τα παραδείγματα, θα χρειαστείτε τα εργαλεία ανάπτυξης που είναι εγκατεστημένα στη διανομή σας. Σε Debian και Ubuntu, μπορείτε να το εγκαταστήσετε με:

κατάλληλοςεγκαθιστώβασικά

Στη συνέχεια, συντάξτε με:

gccδοκιμή1.γδοκιμή 1&& gccδοκιμή2.γδοκιμή2

Για να εκτελέσετε και τα δύο και να δοκιμάσετε την απόδοση, εκτελέστε:

χρόνοςΤο/δοκιμή 1&& χρόνοςΤο/δοκιμή2

Θα πρέπει να έχετε τέτοια αποτελέσματα:

Δοκιμή εισόδου/εξόδου με παραδοσιακές λειτουργίες glibc.

Κατανομή buffer 64 MB: DONE
Γράψιμο δεδομένων στο πρώτο buffer: DONE
Αντιγραφή δεδομένων από το πρώτο αρχείο στο δεύτερο: ΤΕΛΟΣ
Απελευθέρωση buffer: DONE
Διαγραφή αρχείων: ΤΕΛΟΣ
πραγματικό 0m0.397s
χρήστη 0m0.000s
sys 0m0.203s
Δοκιμή εισόδου/εξόδου με sendfile () και σχετικές κλήσεις συστήματος.
Κατανομή buffer 64 MB: DONE
Γράψιμο δεδομένων στο πρώτο buffer: DONE
Αντιγραφή δεδομένων από το πρώτο αρχείο στο δεύτερο: ΤΕΛΟΣ
Απελευθέρωση buffer: DONE
Διαγραφή αρχείων: ΤΕΛΟΣ
πραγματικό 0m0.019s
χρήστη 0m0.000s
sys 0m0.016s

Όπως μπορείτε να δείτε, ο κώδικας που χρησιμοποιεί τις κλήσεις συστήματος τρέχει πολύ πιο γρήγορα από το ισοδύναμο glibc.

Πράγματα που πρέπει να θυμάστε

Οι κλήσεις συστήματος μπορούν να αυξήσουν την απόδοση και να παρέχουν πρόσθετη λειτουργικότητα, αλλά δεν είναι χωρίς τα μειονεκτήματά τους. Θα πρέπει να σταθμίσετε τα οφέλη που παρέχουν οι κλήσεις συστήματος έναντι της έλλειψης φορητότητας της πλατφόρμας και μερικές φορές μειωμένης λειτουργικότητας σε σύγκριση με τις λειτουργίες της βιβλιοθήκης.

Όταν χρησιμοποιείτε ορισμένες κλήσεις συστήματος, πρέπει να φροντίσετε να χρησιμοποιείτε πόρους που επιστρέφονται από κλήσεις συστήματος και όχι λειτουργίες βιβλιοθήκης. Για παράδειγμα, η δομή FILE που χρησιμοποιείται για τις λειτουργίες glibc's fopen (), fread (), fwrite () και fclose () δεν είναι ίδιες με τον αριθμό περιγραφής αρχείου από την κλήση συστήματος ανοιχτού () (επιστρέφεται ως ακέραιος αριθμός). Η ανάμειξη αυτών μπορεί να οδηγήσει σε προβλήματα.

Γενικά, οι κλήσεις συστήματος Linux έχουν λιγότερες λωρίδες προφυλακτήρα από τις λειτουργίες glibc. Παρόλο που είναι αλήθεια ότι οι κλήσεις συστήματος έχουν κάποιο χειρισμό και αναφορά σφαλμάτων, θα λάβετε πιο λεπτομερείς λειτουργίες από μια λειτουργία glibc.

Και τέλος, μια λέξη για την ασφάλεια. Οι κλήσεις συστήματος διασυνδέονται απευθείας με τον πυρήνα. Ο πυρήνας του Linux έχει εκτεταμένη προστασία ενάντια σε απάτες από τη γη των χρηστών, αλλά υπάρχουν ανεξερεύνητα σφάλματα. Μην εμπιστεύεστε ότι μια κλήση συστήματος θα επικυρώσει την εισαγωγή σας ή θα σας απομονώσει από ζητήματα ασφαλείας. Είναι σοφό να διασφαλίσετε ότι τα δεδομένα που παραδίδετε σε μια κλήση συστήματος απολυμαίνονται. Φυσικά, αυτή είναι μια καλή συμβουλή για κάθε κλήση API, αλλά δεν μπορείτε να είστε προσεκτικοί όταν εργάζεστε με τον πυρήνα.

Ελπίζω να απολαύσατε αυτή τη βαθύτερη βουτιά στη χώρα των κλήσεων συστήματος Linux. Για μια πλήρη λίστα των κλήσεων συστήματος Linux, ανατρέξτε στην κύρια λίστα μας.