exec系統呼叫 && 程序的載入過程
阿新 • • 發佈:2021-10-29
exec系統呼叫會從指定的檔案中讀取並載入指令,並替代當前呼叫程序的指令。從某種程度上來說,這樣相當於丟棄了呼叫程序的記憶體,並開始執行新載入的指令。
-
exec系統呼叫會保留當前的檔案描述符表單。所以任何在exec系統呼叫之前的檔案描述符,例如0,1,2等。它們在新的程式中表示相同的東西。
-
通常來說exec系統呼叫不會返回,因為exec會完全替換當前程序的記憶體,相當於當前程序不復存在了,所以exec系統呼叫已經沒有地方能返回了。
在執行shell時,我們不希望系統呼叫替代了Shell程序,實際上,Shell會執行fork,這是一個非常常見的Unix程式呼叫風格。對於那些想要執行程式,但是還希望能拿回控制權的場景,可以先執行fork系統呼叫,然後在子程序中呼叫exec。
以shell程式執行ls命令為例
int main(){
int pid;
...
if(fork() == 0){
//子程序操作
//載入新的程式後當前的內容將全部被捨棄,所以不會執行到下面列印函式
exec("ls","-al");
} else {
//父程序操作
do something...
}
printf("finish");
}
fork函式和exec函式共同組成了新程序的載入方式,這也是計算機建立新程序的一般方式(也許是唯一的方式)
下面程式碼展示了一個程序的記憶體映像究竟是如何一步一步建立的,還涉及了一些關於ELF可執行檔案的知識(見附)。
希望能通過程式碼,讓大家認識到程序實際上並沒有那麼神祕、複雜,對計算機的程序模型能有個更深的認識。
程式碼解析
int exec(char *path, char **argv) { char *s, *last; int i, off; uint64 argc, sz = 0, sp, ustack[MAXARG+1], stackbase; struct elfhdr elf; struct inode *ip; struct proghdr ph; pagetable_t pagetable = 0, oldpagetable; struct proc *p = myproc(); begin_op(); //獲取path路徑處的檔案,即讀取要載入的可執行檔案 if((ip = namei(path)) == 0){ end_op(); return -1; } ilock(ip); // Check ELF header // 先從檔案中讀取elf資訊 if(readi(ip, 0, (uint64)&elf, 0, sizeof(elf)) != sizeof(elf)) goto bad; if(elf.magic != ELF_MAGIC) goto bad; //建立一個新的頁表 if((pagetable = proc_pagetable(p)) == 0) goto bad; // Load program into memory. // 藉助elf中的phoff屬性(program section header off 程式段頭結點在elf檔案中的偏移量) // 將程式所有的section寫入其指定位置(在可執行程式編譯時,其就指定好了哪個段在哪個邏輯地址) for(i=0, off=elf.phoff; i<elf.phnum; i++, off+=sizeof(ph)){ //從檔案中讀取一個section header到ph中 if(readi(ip, 0, (uint64)&ph, off, sizeof(ph)) != sizeof(ph)) goto bad; if(ph.type != ELF_PROG_LOAD) continue; if(ph.memsz < ph.filesz) goto bad; if(ph.vaddr + ph.memsz < ph.vaddr) goto bad; uint64 sz1; //按照section header中的邏輯地址(ph.vaddr)和段長資訊,在頁表中開闢新的空間 if((sz1 = uvmalloc(pagetable, sz, ph.vaddr + ph.memsz)) == 0) goto bad; sz = sz1; if(ph.vaddr % PGSIZE != 0) goto bad; // Load a program segment into pagetable at virtual address va. // 將segment寫入到頁表(即記憶體)中的對應位置 if(loadseg(pagetable, ph.vaddr, ip, ph.off, ph.filesz) < 0) goto bad; } iunlockput(ip); end_op(); ip = 0; //將可執行檔案的內容全部寫入記憶體後,開始建立堆疊 p = myproc(); uint64 oldsz = p->sz; // Allocate two pages at the next page boundary. // Use the second as the user stack. sz = PGROUNDUP(sz); uint64 sz1; //分配兩個page,第二個用來充當使用者棧 if((sz1 = uvmalloc(pagetable, sz, sz + 2*PGSIZE)) == 0) goto bad; sz = sz1; uvmclear(pagetable, sz-2*PGSIZE); sp = sz; stackbase = sp - PGSIZE; // Push argument strings, prepare rest of stack in ustack. // 把執行引數寫入到棧中 for(argc = 0; argv[argc]; argc++) { if(argc >= MAXARG) goto bad; sp -= strlen(argv[argc]) + 1; //記憶體對齊 sp -= sp % 16; // riscv sp must be 16-byte aligned if(sp < stackbase) goto bad; //拷貝到棧中 if(copyout(pagetable, sp, argv[argc], strlen(argv[argc]) + 1) < 0) goto bad; ustack[argc] = sp; } ustack[argc] = 0; // push the array of argv[] pointers. //把引數陣列的指標拷入到棧中 sp -= (argc+1) * sizeof(uint64); sp -= sp % 16; if(sp < stackbase) goto bad; if(copyout(pagetable, sp, (char *)ustack, (argc+1)*sizeof(uint64)) < 0) goto bad; // arguments to user main(argc, argv) // argc is returned via the system call return // value, which goes in a0. // 把陣列指標(即引數列表)寫入到a1暫存器(該暫存器儲存了函式第二個引數) p->trapframe->a1 = sp; // Save program name for debugging. //把檔名設定成程序名 for(last=s=path; *s; s++) if(*s == '/') last = s+1; safestrcpy(p->name, last, sizeof(p->name)); // Commit to the user image. // 設定程序屬性,並且將相應的暫存器置為初始狀態 oldpagetable = p->pagetable; p->pagetable = pagetable; p->sz = sz; p->trapframe->epc = elf.entry; // initial program counter = main p->trapframe->sp = sp; // initial stack pointer proc_freepagetable(oldpagetable, oldsz); //A0用來儲存返回值/函式引數, return argc; // this ends up in a0, the first argument to main(argc, argv) bad: if(pagetable) proc_freepagetable(pagetable, sz); if(ip){ iunlockput(ip); end_op(); } return -1; }
附: