CS107-Lecture 7-Note
距離上一次聽CS107已經有一個月了,非常尷尬。最近花一週左右時間粗糙看完了《Linux核心設計與實現》,compile custom Linux kernel,又複習了之前課程的筆記,和CS107結合起來,感覺消化了更多。比如書中介紹的(1)核心程式設計的特點之inline;(2)核心資料結構中對struct、list的使用;(3)malloc、realloc使用時的注意事項,本質上都是CS107中advanced memory management的部分。書的重點是介紹Linux核心的策略和機制,CS107是編寫Linux核心程式碼時的Program Paradigm。
Lecture 7前半部分介紹String Stack的實現,中間介紹幾個和分配記憶體相關的庫函式,如strdup、const char *,最後介紹記憶體中資料塊的分類和儲存,如heap field、stack field等。
Full Implementation of a String Stack
Lecture 6介紹了generic stack,這次介紹的是string stack. 有別於int, double, long等型別,string型別對指標和地址的運用要複雜一點,因為在下文的例子中用到了動態分配的記憶體,就需要手動free這塊記憶體。
typedef struct {
void *elems;
int elemSize;
int logLength;
int allocLength;
//reserved field
}Stack;
void StackNew(Stack *s, int elemSize);
void StackDispose(Stack *s);
void StackPush(Stack *s, void *elemAddr);
void StackPop(Stack *s, void *elemAddr);
函式宣告基本一致,接下來是對string stack的操作:
/* program 1. Operate String Stack */
int main(-,-) {
const char *friends[] = {"Al","Bob","Carl"};
Stack StringStack;
StackNew(&StringStack, sizeof (char*));
for(int i=0; i<3; i++) {
char *copy = strdup(friends[i]); //copy就是friends開掛的副本,後半部分會詳細說明
StackPush(StringStack, ©); //取copy地址傳入,真正push的是copy中存放的地址,該地址指向了friends[0]在heap中的副本:“AI”
}
char *name;
for(int i=0; i<3; i++) {
StackPop(&StringStack, &name);
printf("pop up variable: %s\n", name);
free(name); //這裡釋放name,其實就是釋放“Al”、“Bob”、“Carl”,將它們還給heap
}
StackDispose(&StringStack); //這裡釋放StringStack,就是釋放stack的記憶體
}
問題:如果沒有for迴圈中StackPop和free的操作,直接StackDispose可行嗎?
Jerry: the stack shouldn’t be obligated to be empty, or the client shouldn’t be forced to pop everything off the stack before they call dispose. StackDispose should be able to say, “Ok, I seem to have retained ownership of some elements that were pressed onto me.”
所以,StackDispose()應當有基本的能力:判斷自身棧中(char*)s->elems
是否還儲存有某種內容,如果(char*)s->elems
中儲存的是int、double、long、char等型別的值,不需要清零;如果是pointer,並且其指向了動態分配的記憶體塊,應當先free這塊記憶體,再free StackString。問題的難點在於理解freefn():
釋放elems指向的棧中儲存的三個指標。
Jerry: when we store things, we actually have to potentially set up the stack, or set up the stacks to potentially delete elements for us.
所以,Jerry從StackNew()函式入手,在初始化時就令StackString為elems做了標記:為其定製了一個freefn()來釋放elems中的(我們已知的char**型別的)元素。
/* program 2. modified StringStack */
typedef struct {
void *elems;
int elemSize;
int logLength;
int allocLength;
void (*freenfn)(void*); //為Stack新增一個函式域
}Stack;
void StackNew(Stack *s, int elemSize, void (*freefn)(void *)){略}
void StackDispose() {
/*1. 檢查elems指向的記憶體塊中是否有些複雜的東西(二級指標)
* 2. 有東西,就free掉
* 3. 因為我們已知棧裡這些東西是char*,所以用char*解引用,釋放解引用後指向的堆中的記憶體塊
*/
if(s->freenfn != NULL) {
for(int i=0; i<s->logLength; i++) { //不需要釋放未被使用的棧,所以使用logLength
s->freenfn((char *)s->elems + i*s->elemSize);
}
}
free(s->elems); //釋放elems指向的棧中的記憶體塊
}
Stack StringStack;
//如果是一般型別,StringFree傳入NULL,之後無需釋放記憶體
StackNew(&StringStack, sizeof(char *), StringFree);
//為Stack定製的函式,專門free作為char**存在的elems
StringFree(void *elems) {
free(*(char **)elems); //第一個*的解引用很關鍵,從而釋放棧空間
}
在理解freefn()之前,這篇對C語言中malloc和free函式的理解 歸納了malloc和free的幾個知識點,有助於理解清理堆疊的問題:我們所free的malloc分配的堆記憶體,在釋放後將就可以被記憶體管理者回收再利用,而*elem的值並沒有改變,就很可能變成野指標(因為鬼知道這個地址處的值將會被重新分配來幹什麼),還需要重新指向NULL。
Functions
1. rotate()
rotate()是C++標準庫函式,Jerry將在C裡模擬實現。copy過程中,可能會出現記憶體覆蓋,不能使用memcpy(),而使用memmove():
Jerry: The source regions are actually overlap or they are potentially overlap. The implementation of memcpy is brute force(暴力演算法). It carries things four(這裡是針對他舉的呢個4-byte copy的例子) bytes at a time, and then at the end does whatever mod tricks(求模的方式?) it needs to copy off an odd number of bytes, but it assumes they’re not overlapping. When they’re overlapping, that brute force approach might not work.
關於memcpy()和memmove(),memmove 和 memcpy的區別以及處理記憶體重疊問題 這篇文章介紹了倆函式的使用情境和具體實現。Jerry也補充了一些他的觀點:我們可以在copy之前,check the target address and the source address,然後自行判斷是從前到後還是從後到前copy;如果不想check,那就用mmmove(),但要記得mmmove()只用在非用不可的地方,因為它的效率實在是太低了,比如rotate()裡:
void rotate(void *front, void *middle, void *end) {
//如果是兩個void指標直接相減,就會返回兩個地址之間的int的個數,所以需要轉為char*再減,才能得到兩個地址之間實際物理位元組的個數
int frontSize = (char*)middle - (char*)front;
int backSize = (char*)end - (char*)middle;
char buffer[frontSize]; //開個buffer
memcpy(buffer, front, frontSize);
memmove(front, middle, backSize); //儘可能呼叫memcpy,因為效率更高
memcpy((char*)end-frontSize, buffer, frontSize);
}
2. qsort()
void qsort(void *base, int size, int elemSize, int (*cmpfn)(void *, void *)) {
Lecture 8
}
RAM Memory Management
大三那年在溫老師的課上睡過的覺覺錯過的知識欠下的學術債,早晚要補回來,所以最後十分鐘Jerry概述的RAM Memory Management,句句珠璣都要記下來…
Here’s RAM, and since we’re dealing with an architecture where longs and pointers are four bytes, that means that pointers can distinguish between two to the thirtysecond different addresses. That means the lowest address in memory is zero which is that null that you’re starting to fear a little bit and then the highest address is two to the thirysecond minus one.
stack segment
Whenever you call functions, and the function call forces the allocation of lots of local variables, the memory for those local variables is drawn from a subset of all RAM called the stack. I’m gonna drawn that up here. I drew a little bit bigger than I need to, but here it is, the stack segment. The stack segment is what this thing is called. It doesn’t necessarily use all of the stack, but for reasons that will become clear and there’s even a little bit of intuition, I think, as to why it might be called a stack, when you call main you get all of its local variables, and they’re alive and they’re active. When main calls a helper function it doesn’t like the main functions, main’s variables go away. They’re just temporarily disabled, and don’t have access to .….. at least not via the normal variable names, right. So main calls helper, which calls helper helper, which calls helper helper helper, and you have all of these variables that are allocated. But only the ones on the bottom most function are actually alive and accessible via their variable names. When helper helper helper returns, you return back to the local where helper helper has local variables, you can access those. So basically, when a function calls another function, the first function’s variables are suspended until whatever happens in response to the function call actually ends. And it may itself call several helper functions, and just go through lots of a big code tree of functions calls before it actually returns a value, or not. What happens is that, initially, that much space is set aside from the stack segment to just hold the main’s variables whatever the main’s local variables are. And when main called something, this threshold is lowered to there to just make sure that not only is there space for main’s variables set aside, but also for the helper function’s variables, okay. And it(the threshold) goes down an up, down and up, every time it goes down is because some function was called, and every time it goes up, it’s because some functions returned, okay. And the same argument can be made for methods in C++. It’s called a stack because the most recently called functions is the one that is invited to return before any other ones unless it calls some other function, okay. That’s why it called a stack.
heap segment
Heap in this world(CS 107) doesn’t mean like a priority cube back in data structure. It really means blob of arbitrary bytes which is completepy managed by the hardware, by the assembly code which actually happens to be down here(地址0上面的記憶體塊).**cThis right there, this boundary, and that address, and that address is admitted to software(heap). **Software that implements what we called the heap manager. And the heap manger is software it’s code. The implementation of malloc, realloc and free…and they basically manage this memory right here. …But I wanna …..memory heap …that it is one big linear array of bytes. And so, rather than drawing it as a tall rectangle, I’m gonna draw it as a very wide rectangle.
堆記憶體管理器:
Entirely software managed with very little exception, okay and I say exception because the operate system and what’s called the loader has to admit to the implementation what the boundaries of the stacks of the heap segment are, but everything else is really frame in terms of this raw memory allocator, okay. As far as realloc is concerned, if I pass this address to realloc, and I ask it to become bigger, it’ll have to do a reallocation request, and put it somewhere else probably right there, the way we’ve been talking about it. What happened is that there really is a little bit of a data structure that overlays the entire heap segment, okay. It is manually manged using lots of void * business.
空閒連結串列:
The data structure that’s more or less used by the heap manager overlays a linked list of what are called free notes, okay. And it always keeps the address of the very first free note, and because you’re not usually using this as a client, the heap manger uses it as a variably sized node that right in the first eight or four bytes keeps track of how big that node is. So, it might have subdivided that, and to the left of that line might have the size of that node, and to the right of that line might actually have a pointer to that right there.
參考資料
[1]. 堆(heap)和棧(stack)
這篇文章比較了heap和stack在大小、使用方式、碎片、生長方式等方面的差別,可以幫助理解上面的free操作。在StackNew()中,friends是const型別,儲存在常量區;通過strdup()呼叫malloc(),在heap上分配了一個空間儲存一級指標(指向friends);elem是stack上的變數,儲存了指向friends的二級指標。
作者最後還舉了一個例子,解釋了不同變數在記憶體空間中的位置。其中“還有就是函式呼叫時會在棧上有一系列的保留現場及傳遞引數的操作”,其實Jerry也提了下,在說到free了friends之後,friends所在heap記憶體的去向,是會有專門的記憶體管理者來回收的。如果我理解的沒錯的話,《Linux核心設計與實現》第12章記憶體管理,就有詳細介紹核心如何把記憶體分成頁和區來管理的,這種管理maybe就是他們所說的回收再分配吧。
[2]. 字元陣列和字元指標
看了這篇文章,對字元陣列和字元指標的理解更透徹了。評論裡有兩處對文章有爭議的地方:
(1)str是指標常量還是常量指標?
char str[20] = {'h','e','l','l','o',' ','w','o','r','l','d'};
str++;
int *pstr;
兩個稱呼我也分不清,特意查了下《The C Programming Language (Second Edition)》5.3 Pointers and Array,並沒有對陣列名下這種定義。但我理解評論想表達的意思:A pointer is a variable, and an array name is not a variable; constructions like pstr=str and pstr++ are legal; constructions like str=pstr and str++ are illegal.
(2)字元陣列宣告之後,沒有初始化;或者宣告之後,只是malloc了記憶體,都會因為沒有‘\0’導致直接printf(“%s\n”,str);出來亂碼。這個我沒理解,因為我也運行了下程式碼,結果並沒有亂碼:
即使是作者的例子char str[20];
我執行的也是ok的。奇怪。