!null schreef op donderdag 02 juli 2009 @ 10:53:
Maar .oisyn, uit jou stukje begrijp ik dat je een system call moet doen welke zich vertaalt in bepaalde instructies (die lock inc ..), waarom kan dit niet vanuit C gerealiseerd worden? (dus buiten het OS om)
Dat kan prima in C, alleen niet in standaard C. Meestal ondersteunt de compiler zogenaamde intrinsics - functies die je aanroept die in feite direct naar assembly instructies vertalen. strlen() of memcpy() zijn goede voorbeelden van dergelijke functies.
Het OS biedt meestal wat functionaliteit aan voor atomaire operaties. De compiler implementatie herkent die functies en kan dat direct vertalen naar assembly instructies. Het mooie hiervan is dat als je een compiler gebruikt die dat niet kan, je alsnog gewoon die functies kunt gebruiken. Maar als je een compiler gebruikt die het wel kan performt het een stuk beter. Zonder dat je de code hoeft aan te passen

Ik snap dat echte semaphoren etc platform specifiek zouden kunnen zijn, komt zo'n lock instructie niet op andere architecturen zoals ARM of MIPS voor?
De x86 is een CISC architectuur. Daarmee kun je in een enkele instructie bijv. een waarde bij een waarde dat in het geheugen staat optellen. (Met een RISC CPU heb je daar meestal 3 instructies voor nodig (een read, een add en een write. Overigens zijn hedendaagse x86 CPUs intern ook gewoon RISC chips, en worden de instructies eerst vertaald naar RISC microcode instructies.). Echter, de geheugenmodules zelf hebben die logica niet, dus de waarde zal alsnog eerst uit het geheugen gehaald moeten worden, vervolgens opgehoogd, en daarna weer weggeschreven. Met de 'lock' prefix zorg je ervoor dat dat geheugenadres wordt gelocked door de memory controller zodat een andere CPU niet ook wat met dat geheugen kan doen voordat de lock weer is vrijgegeven. Een soort hardwarematige mutex dus

.
Voor dingen als mutices en semaphores heb je meestal
test-and-set of
load-link/store-conditional. Daarmee is het mogelijk om read-modify-write operaties uit te voeren, waarbij de write in feite alleen maar lukt als de waarde op dat geheugenadres niet in de tussentijd is veranderd.
Voor een mutex heb je dan bijv. een variabele die aangeeft welke thread de mutex owned, of 0 als hij vrij is. Een thread die een lock doet, leest de waarde. Als die niet 0 is dan gaat ie in slaapstand. Als hij wel 0 is dan probeert hij zijn eigen thread id in het geheugenadres te zetten. Is de waarde in tussentijd verandert, dan probeert hij het van voor af aan.
De x86 CPU heeft hiervoor de compareexchange functie, die een geheugenwaarde vergelijkt met een bepaalde waarde, en indien het gelijk is die waarde vervangt met een nieuwe waarde. Het resultaat in het register is de oude waarde, onafhankelijk van of het schrijven gelukt is of niet. De win32 API functie hiervoor heet InterlockedCompareExchange.
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| struct mutex
{
int owner;
}
void lock(mutex * m)
{
int mytid = GetCurrentThreadId();
// mytid blijven proberen te schrijven totdat er geen owner is.
while(InterlockedCompareExchange(&m->owner, mytid, 0));
// ^ ^----- waarde waarmee owner wordt vergeleken
// \---------- nieuwe waarde van 'owner' als de oude waarde overeenkomt
}
void unlock(mutex * m)
{
int mytid = GetCurrentThreadId();
// alleen een 0 schrijven als de huidige owner onze eigen thread is.
if (InterlockedCompareExchange(&m->owner, 0, mytid) != mytid)
error("you didn't own the lock");
} |
Nou zijn dit natuurlijk busy-waits en niet 'fair'. Een goed OS zal een thread in slaapstand plaatsen en de thread toevoegen aan de queue van de mutex, zodat als ie geunlocked wordt hij de volgende wachtende thread de lock geeft.
[
Voor 3% gewijzigd door
.oisyn op 02-07-2009 14:36
]