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;
}
附:
