Αναγνώστες σαν εσάς βοηθούν στην υποστήριξη του MUO. Όταν κάνετε μια αγορά χρησιμοποιώντας συνδέσμους στον ιστότοπό μας, ενδέχεται να κερδίσουμε μια προμήθεια θυγατρικών.

Μια συνθήκη αγώνα εμφανίζεται όταν δύο λειτουργίες πρέπει να πραγματοποιηθούν με μια συγκεκριμένη σειρά, αλλά μπορεί να εκτελεστούν με την αντίθετη σειρά.

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

Για να κατανοήσουμε καλύτερα αυτό το μοντέλο, καλό θα ήταν να εξετάσουμε προσεκτικά τη διαδικασία εναλλαγής διεργασιών του επεξεργαστή.

Πώς ένας επεξεργαστής αλλάζει διεργασίες

Σύγχρονα λειτουργικά συστήματα μπορεί να εκτελέσει περισσότερες από μία διεργασίες ταυτόχρονα, που ονομάζεται multitasking. Όταν εξετάζετε αυτή τη διαδικασία από την άποψη του κύκλος εκτέλεσης της CPU

, μπορεί να διαπιστώσετε ότι το multitasking δεν υπάρχει στην πραγματικότητα.

Αντίθετα, οι επεξεργαστές αλλάζουν συνεχώς μεταξύ διεργασιών για να τις εκτελούν ταυτόχρονα ή τουλάχιστον να ενεργούν σαν να το κάνουν. Η CPU μπορεί να διακόψει μια διαδικασία πριν ολοκληρωθεί και να συνεχίσει μια διαφορετική διαδικασία. Το λειτουργικό σύστημα ελέγχει τη διαχείριση αυτών των διεργασιών.

Για παράδειγμα, ο αλγόριθμος Round Robin, ένας από τους απλούστερους αλγόριθμους μεταγωγής, λειτουργεί ως εξής:

Γενικά, αυτός ο αλγόριθμος επιτρέπει σε κάθε διεργασία να εκτελείται για πολύ μικρά κομμάτια χρόνου, όπως καθορίζει το λειτουργικό σύστημα. Για παράδειγμα, αυτό θα μπορούσε να είναι μια περίοδος δύο μικροδευτερόλεπτων.

Η CPU παίρνει κάθε διεργασία με τη σειρά και εκτελεί εντολές που θα εκτελούνται για δύο μικροδευτερόλεπτα. Στη συνέχεια, συνεχίζει στην επόμενη διαδικασία, ανεξάρτητα από το αν η τρέχουσα έχει τελειώσει ή όχι. Έτσι, από τη σκοπιά ενός τελικού χρήστη, περισσότερες από μία διεργασίες φαίνεται να εκτελούνται ταυτόχρονα. Ωστόσο, όταν κοιτάτε πίσω από τις σκηνές, η CPU εξακολουθεί να κάνει τα πράγματα με τη σειρά.

Παρεμπιπτόντως, όπως δείχνει το παραπάνω διάγραμμα, ο αλγόριθμος Round Robin στερείται οποιασδήποτε έννοιας προτεραιότητας βελτιστοποίησης ή επεξεργασίας. Ως αποτέλεσμα, είναι μια μάλλον στοιχειώδης μέθοδος που χρησιμοποιείται σπάνια σε πραγματικά συστήματα.

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

Ένα παράδειγμα εφαρμογής Ιστού και συνθήκης αγώνα

Ρίξτε μια ματιά στην απλή εφαρμογή Flask παρακάτω για να σκεφτείτε ένα συγκεκριμένο παράδειγμα όλων όσων έχετε διαβάσει μέχρι τώρα. Σκοπός αυτής της εφαρμογής είναι η διαχείριση χρηματικών συναλλαγών που θα πραγματοποιούνται στο διαδίκτυο. Αποθηκεύστε τα παρακάτω σε ένα αρχείο με όνομα χρήματα.py:

από φλάσκα εισαγωγή Φλάσκα
από φιάλη.ext.sqlalchemy εισαγωγή SQLAlchemy

app = Flask (__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
db = SQLAlchemy (εφαρμογή)

τάξηλογαριασμός(δβ. Μοντέλο):
id = db. Στήλη (db. Ακέραιος, πρωτεύον_κλειδί = Αληθής)
ποσό = db. Στήλη (db. Σειρά(80), μοναδικός = Αληθής)

def__μέσα σε αυτό__(αυτός, μετρήστε):
αυτο.ποσό = ποσό

def__repr__(εαυτός):
ΕΠΙΣΤΡΟΦΗ '' % αυτο.ποσό

@app.route("/")
defγεια():
λογαριασμός = Account.query.get(1) # Υπάρχει μόνο ένα πορτοφόλι.
ΕΠΙΣΤΡΟΦΗ "Total Money = {}".format (account.amount)

@app.route("/send/")
defστείλετε(ποσό):
λογαριασμός = Account.query.get(1)

αν int (account.amount) < ποσό:
ΕΠΙΣΤΡΟΦΗ «Ανεπαρκής ισορροπία. Επαναφορά χρημάτων με /reset!)"

account.amount = int (account.amount) - ποσό
db.session.commit()
ΕΠΙΣΤΡΟΦΗ "Ποσό που εστάλη = {}".μορφή (ποσό)

@app.route("/reset")
defεπαναφορά():
λογαριασμός = Account.query.get(1)
λογαριασμός.ποσό = 5000
db.session.commit()
ΕΠΙΣΤΡΟΦΗ "Επαναφορά χρημάτων."

αν __όνομα__ == "__κύριος__":
app.secret_key = 'heLLoTHisIsSeCReTKey!'
app.run()

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

από χρήματα εισαγωγή db
db.create_all()
από χρήματα εισαγωγή λογαριασμός
λογαριασμός = Λογαριασμός (5000)
db.συνεδρία.Προσθήκη(λογαριασμός)
db.συνεδρία.διαπράττω()

Έχετε πλέον δημιουργήσει έναν λογαριασμό με υπόλοιπο 5.000 $. Τέλος, εκτελέστε τον παραπάνω πηγαίο κώδικα χρησιμοποιώντας την ακόλουθη εντολή, με την προϋπόθεση ότι έχετε εγκαταστήσει τα πακέτα Flask και Flask-SQLAlchemy:

Πύθωνχρήματα.py

Έτσι έχετε την εφαρμογή web Flask που κάνει μια απλή διαδικασία εξαγωγής. Αυτή η εφαρμογή μπορεί να εκτελέσει τις ακόλουθες λειτουργίες με συνδέσμους αιτήματος GET. Εφόσον το Flask εκτελείται στη θύρα 5000 από προεπιλογή, η διεύθυνση στην οποία έχετε πρόσβαση είναι 127.0.0.1:5000/. Η εφαρμογή παρέχει τα ακόλουθα τελικά σημεία:

  • 127.0.0.1:5000/ εμφανίζει το τρέχον υπόλοιπο.
  • 127.0.0.1:5000/send/{amount} αφαιρεί ποσό από τον λογαριασμό.
  • 127.0.0.1:5000/επαναφορά επαναφέρει τον λογαριασμό στα 5.000 $.

Τώρα, σε αυτό το στάδιο, μπορείτε να εξετάσετε πώς εμφανίζεται η ευπάθεια της συνθήκης αγώνα.

Πιθανότητα ευπάθειας αγωνιστικής συνθήκης

Η παραπάνω εφαρμογή web περιέχει μια πιθανή ευπάθεια συνθήκης φυλής.

Φανταστείτε ότι έχετε 5.000 $ για να ξεκινήσετε και δημιουργήστε δύο διαφορετικά αιτήματα HTTP που θα στείλουν 1 $. Για αυτό, μπορείτε να στείλετε δύο διαφορετικά αιτήματα HTTP στον σύνδεσμο 127.0.0.1:5000/αποστολή/1. Ας υποθέσουμε ότι, μόλις ο διακομιστής Ιστού επεξεργάζεται το πρώτο αίτημα, η CPU διακόπτει αυτή τη διαδικασία και επεξεργάζεται το δεύτερο αίτημα. Για παράδειγμα, η πρώτη διαδικασία μπορεί να σταματήσει μετά την εκτέλεση της ακόλουθης γραμμής κώδικα:

λογαριασμός.ποσό = ενθ(λογαριασμός.ποσό) - ποσό

Αυτός ο κωδικός έχει υπολογίσει ένα νέο σύνολο, αλλά δεν έχει αποθηκεύσει ακόμη την εγγραφή στη βάση δεδομένων. Όταν ξεκινήσει το δεύτερο αίτημα, θα εκτελέσει τον ίδιο υπολογισμό, αφαιρώντας $1 από την τιμή στη βάση δεδομένων—5.000$—και αποθηκεύοντας το αποτέλεσμα. Όταν συνεχιστεί η πρώτη διαδικασία, θα αποθηκεύσει τη δική της αξία—4.999$—η οποία δεν θα αντικατοπτρίζει το πιο πρόσφατο υπόλοιπο του λογαριασμού.

Έτσι, δύο αιτήματα έχουν ολοκληρωθεί και το καθένα θα έπρεπε να έχει αφαιρέσει 1 $ από το υπόλοιπο του λογαριασμού, με αποτέλεσμα ένα νέο υπόλοιπο 4.998 $. Ωστόσο, ανάλογα με τη σειρά με την οποία ο διακομιστής ιστού τα επεξεργάζεται, το τελικό υπόλοιπο του λογαριασμού μπορεί να είναι 4.999 $.

Φανταστείτε ότι στέλνετε 128 αιτήματα για να κάνετε μια μεταφορά $1 στο σύστημα προορισμού σε χρονικό διάστημα πέντε δευτερολέπτων. Ως αποτέλεσμα αυτής της συναλλαγής, η αναμενόμενη κατάσταση λογαριασμού θα είναι 5.000 $ - 128 $ = 4.875 $. Ωστόσο, λόγω της συνθήκης του αγώνα, το τελικό υπόλοιπο μπορεί να ποικίλλει μεταξύ $4.875 και $4.999.

Οι προγραμματιστές είναι ένα από τα πιο σημαντικά στοιχεία της ασφάλειας

Σε ένα έργο λογισμικού, ως προγραμματιστής, έχετε αρκετές ευθύνες. Το παραπάνω παράδειγμα ήταν για μια απλή εφαρμογή μεταφοράς χρημάτων. Φανταστείτε να εργάζεστε σε ένα έργο λογισμικού που διαχειρίζεται έναν τραπεζικό λογαριασμό ή το backend ενός μεγάλου ιστότοπου ηλεκτρονικού εμπορίου.

Πρέπει να είστε εξοικειωμένοι με τέτοια τρωτά σημεία, ώστε το πρόγραμμα που έχετε γράψει για την προστασία τους να είναι απαλλαγμένο από τρωτά σημεία. Αυτό απαιτεί ισχυρή ευθύνη.

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