日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

理解进程创建、可执行文件的加载和进程执行进程切换,重点理解分析fork、execve和进程切换

發(fā)布時(shí)間:2025/4/16 编程问答 47 豆豆
生活随笔 收集整理的這篇文章主要介紹了 理解进程创建、可执行文件的加载和进程执行进程切换,重点理解分析fork、execve和进程切换 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

學(xué)號(hào):384
原創(chuàng)作品轉(zhuǎn)載請(qǐng)注明出處 + https://github.com/mengning/linuxkernel/

實(shí)驗(yàn)?zāi)繕?biāo)

1.分析fork函數(shù)對(duì)應(yīng)的內(nèi)核處理過程do_fork,理解創(chuàng)建一個(gè)新進(jìn)程如何創(chuàng)建和修改task_struct數(shù)據(jù)結(jié)構(gòu)

2.使用gdb跟蹤分析一個(gè)fork系統(tǒng)調(diào)用內(nèi)核處理函數(shù)do_fork

3.理解編譯鏈接的過程和ELF可執(zhí)行文件格式

實(shí)驗(yàn)環(huán)境

ubuntu系統(tǒng)(ubuntu-16.04.2-desktop-amd64)+ VMware Workstation Pro

一、閱讀理解task_struct數(shù)據(jù)結(jié)構(gòu)

代碼來源:http://codelab.shiyanlou.com/xref/linux-3.18.6/include/linux/sched.h#1235
該結(jié)構(gòu)部分代碼:

struct task_struct {volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */void *stack;atomic_t usage;unsigned int flags; /* per process flags, defined below */unsigned int ptrace;#ifdef CONFIG_SMPstruct llist_node wake_entry;int on_cpu;struct task_struct *last_wakee;unsigned long wakee_flips;unsigned long wakee_flip_decay_ts;int wake_cpu; #endifint on_rq;int prio, static_prio, normal_prio;unsigned int rt_priority;const struct sched_class *sched_class;struct sched_entity se;struct sched_rt_entity rt; #ifdef CONFIG_CGROUP_SCHEDstruct task_group *sched_task_group; #endifstruct sched_dl_entity dl;#ifdef CONFIG_PREEMPT_NOTIFIERS/* list of struct preempt_notifier: */struct hlist_head preempt_notifiers; #endif#ifdef CONFIG_BLK_DEV_IO_TRACEunsigned int btrace_seq; #endifunsigned int policy;int nr_cpus_allowed;cpumask_t cpus_allowed;... }

在閱讀這個(gè)結(jié)構(gòu)體之前,我們必須了解進(jìn)程與程序的區(qū)別,進(jìn)程是程序的一個(gè)執(zhí)行的實(shí)例,為了管理進(jìn)程,操作系統(tǒng)必須對(duì)每個(gè)進(jìn)程所做的事情進(jìn)行清楚的描述,為此,操作系統(tǒng)使用數(shù)據(jù)結(jié)構(gòu)來代表處理不同的實(shí)體,這個(gè)數(shù)據(jù)結(jié)構(gòu)就是通常所說的進(jìn)程描述符或進(jìn)程控制塊(PCB),在linux操作系統(tǒng)下這就是task_struct結(jié)構(gòu) ,它包含了這個(gè)進(jìn)程的所有信息,在任何時(shí)候操作系統(tǒng)都能夠跟蹤這個(gè)結(jié)構(gòu)的信息。該結(jié)構(gòu)定義位于/include/linux/sched.h

對(duì)于進(jìn)程控制塊PCB—task_struct:

狀態(tài)信息:如就緒、執(zhí)行等狀態(tài)
鏈接信息:用來描述進(jìn)程之間的家庭關(guān)系,例如指向父進(jìn)程、子進(jìn)程、兄弟進(jìn)程等PCB的指針
各種標(biāo)識(shí)符:如進(jìn)程標(biāo)識(shí)符、用戶及組標(biāo)識(shí)符等
時(shí)間和定時(shí)器信息:進(jìn)程使用CPU時(shí)間的統(tǒng)計(jì)等
調(diào)度信息:調(diào)度策略、進(jìn)程優(yōu)先級(jí)、剩余時(shí)間片大小等
處理機(jī)環(huán)境信息:處理器的各種寄存器以及堆棧情況等
虛擬內(nèi)存信息:描述每個(gè)進(jìn)程所擁有的地址空間

文件系統(tǒng)信息:記錄進(jìn)程使用文件的情況

PCB幾個(gè)重要參數(shù)

volatile long state;//表示進(jìn)程的當(dāng)前狀態(tài) unsigned long flags; //進(jìn)程標(biāo)志 long priority; //進(jìn)程優(yōu)先級(jí)。 Priority的值給出進(jìn)程每次獲取CPU后可使用的時(shí)間(按jiffies計(jì))。優(yōu)先級(jí)可通過系統(tǒng)調(diào)用sys_setpriorty改變(在kernel/sys.c中)。 long counter; //在輪轉(zhuǎn)法調(diào)度時(shí)表示進(jìn)程當(dāng)前還可運(yùn)行多久。unsigned long policy; //該進(jìn)程的進(jìn)程調(diào)度策略,可以通過系統(tǒng)調(diào)用sys_sched_setscheduler()更改(見kernel/sched.c)。

二、分析fork函數(shù)對(duì)應(yīng)的內(nèi)核處理過程do_fork

fork、vfork和clone三個(gè)系統(tǒng)調(diào)用都可以創(chuàng)建一個(gè)新進(jìn)程,而且都是通過調(diào)用do_fork來實(shí)現(xiàn)進(jìn)程的創(chuàng)建;

具體過程如下:fork() -> sys_clone() -> do_fork() -> dup_task_struct() -> copy_process() -> copy_thread() -> ret_from_fork()

do_fork代碼如下:

long do_fork(unsigned long clone_flags,unsigned long stack_start,unsigned long stack_size,int __user *parent_tidptr,int __user *child_tidptr) {struct task_struct *p;int trace = 0;long nr;// ...// 復(fù)制進(jìn)程描述符,返回創(chuàng)建的task_struct的指針p = copy_process(clone_flags, stack_start, stack_size,child_tidptr, NULL, trace);if (!IS_ERR(p)) {struct completion vfork;struct pid *pid;trace_sched_process_fork(current, p);// 取出task結(jié)構(gòu)體內(nèi)的pidpid = get_task_pid(p, PIDTYPE_PID);nr = pid_vnr(pid);if (clone_flags & CLONE_PARENT_SETTID)put_user(nr, parent_tidptr);// 如果使用的是vfork,那么必須采用某種完成機(jī)制,確保父進(jìn)程后運(yùn)行if (clone_flags & CLONE_VFORK) {p->vfork_done = &vfork;init_completion(&vfork);get_task_struct(p);}// 將子進(jìn)程添加到調(diào)度器的隊(duì)列,使得子進(jìn)程有機(jī)會(huì)獲得CPUwake_up_new_task(p);// ...// 如果設(shè)置了 CLONE_VFORK 則將父進(jìn)程插入等待隊(duì)列,并掛起父進(jìn)程直到子進(jìn)程釋放自己的內(nèi)存空間// 保證子進(jìn)程優(yōu)先于父進(jìn)程運(yùn)行if (clone_flags & CLONE_VFORK) {if (!wait_for_vfork_done(p, &vfork))ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);}put_pid(pid);} else {nr = PTR_ERR(p);}return nr; }

do_fork處理了以下內(nèi)容:

1.調(diào)用copy_process,將當(dāng)期進(jìn)程復(fù)制一份出來為子進(jìn)程,并且為子進(jìn)程設(shè)置相應(yīng)地上下文信息。
2.初始化vfork的完成處理信息(如果是vfork調(diào)用)
3.調(diào)用wake_up_new_task,將子進(jìn)程放入調(diào)度器的隊(duì)列中,此時(shí)的子進(jìn)程就可以被調(diào)度進(jìn)程選中,得以運(yùn)行。
4.如果是vfork調(diào)用,需要阻塞父進(jìn)程,知道子進(jìn)程執(zhí)行exec。

如何創(chuàng)建一個(gè)新進(jìn)程:
1.通過調(diào)用do_fork來實(shí)現(xiàn)進(jìn)程的創(chuàng)建;
2.復(fù)制父進(jìn)程PCB–task_struct來創(chuàng)建一個(gè)新進(jìn)程,要給新進(jìn)程分配一個(gè)新的內(nèi)核堆棧;
3.修改復(fù)制過來的進(jìn)程數(shù)據(jù),比如pid、進(jìn)程鏈表等等執(zhí)行copy_process和copy_thread
4.成功創(chuàng)建新進(jìn)程

三、使用gdb跟蹤分析一個(gè)fork系統(tǒng)調(diào)用內(nèi)核處理函數(shù)do_fork

本次實(shí)驗(yàn)是基于實(shí)驗(yàn)樓中現(xiàn)有的實(shí)驗(yàn)環(huán)境進(jìn)行的。
進(jìn)入menu文件夾,編輯test.c文件:

cd ~/LinuxKernel/menu/ sudo vim test.c

給qemu增加一個(gè)使用fork系統(tǒng)調(diào)用的菜單命令,如下所示:


在menu目錄下執(zhí)行如下命令:make rootfs啟動(dòng)MenuOS,結(jié)果如下所示:

使用GDB進(jìn)行跟蹤調(diào)試,設(shè)置如下斷點(diǎn):

在MenuOS中輸入fork菜單命令以后,后面的斷點(diǎn)依次如圖所示:
首先停在sys_clone位置處:

然后進(jìn)入do_fork中:

接著進(jìn)入copy_process中:

接著進(jìn)入copy_thread中:

最后進(jìn)入ret_from_fork中:

整個(gè)fork系統(tǒng)調(diào)用的執(zhí)行流程如下:
fork->sys_clone->do_fork->copy_process->dup_task_struct->copy_thread->ret_from_fork

Linux內(nèi)核通過復(fù)制父進(jìn)程來創(chuàng)建一個(gè)新進(jìn)程,調(diào)用do_fork為每個(gè)新創(chuàng)建的進(jìn)程動(dòng)態(tài)地分配一個(gè)task_struct結(jié)構(gòu)。copy_thread()函數(shù)中的代碼p->thread.ip = (unsigned long) ret_from_fork;將子進(jìn)程的 ip 設(shè)置為 ret_form_fork 的首地址,所以fork系統(tǒng)調(diào)用產(chǎn)生的子進(jìn)程在系統(tǒng)調(diào)用處理過程中從ret_from_fork處開始執(zhí)行。
copy_thread()函數(shù)中的代碼*childregs = *current_pt_regs();將父進(jìn)程的regs參數(shù)賦值到子進(jìn)程的內(nèi)核堆棧,里面存放了SAVE ALL中壓入棧的參數(shù),之后的RESTORE_ALL宏定義會(huì)恢復(fù)保存到堆棧中的寄存器的值。
fork系統(tǒng)調(diào)用發(fā)生一次,但是返回兩次。父進(jìn)程中返回值是子進(jìn)程的進(jìn)程號(hào),子進(jìn)程中返回值為0,可以通過返回值來判斷當(dāng)前進(jìn)程是父進(jìn)程還是子進(jìn)程。

四、理解編譯鏈接的過程和ELF可執(zhí)行文件格式

從源文件Hello.c編譯鏈接成Hello.out,需要經(jīng)歷如下步驟:

ELF可執(zhí)行文件格式具體分析代碼:https://blog.csdn.net/wu5795175/article/details/7657580
ELF文件格式包括三種主要的類型:可執(zhí)行文件、可重定向文件、共享庫:
1.一個(gè)可執(zhí)行(executable)文件保存著一個(gè)用來執(zhí)行的程序;該文件指出了exec(BA_OS)如何來創(chuàng)建程序進(jìn)程映象。
2.一個(gè)可重定位(relocatable)文件保存著代碼和適當(dāng)?shù)臄?shù)據(jù),用來和其他的object文件一起來創(chuàng)建一個(gè)可執(zhí)行文件或者是一個(gè)共享文件。
3.一個(gè)共享庫文件保存著代碼和合適的數(shù)據(jù),用來被不同的兩個(gè)鏈接器鏈接。

五、編程使用exec*庫函數(shù)加載一個(gè)可執(zhí)行文件,動(dòng)態(tài)鏈接分為可執(zhí)行程序裝載時(shí)動(dòng)態(tài)鏈接和運(yùn)行時(shí)動(dòng)態(tài)鏈接

第一步:先編輯一個(gè)hello.c

#include <stdio.h> #include <stdlib.h> int main() {printf("Hello World!\n");return 0; }

第二步:生成預(yù)處理文件hello.cpp(預(yù)處理負(fù)責(zé)把include的文件包含進(jìn)來及宏替換等工作)
第三步:編譯成匯編代碼hello.s
第四步:編譯成目標(biāo)代碼,得到二進(jìn)制文件hello.o
第五步:鏈接成可執(zhí)行文件hello,(它是二進(jìn)制文件)
第六步:運(yùn)行一下./hello

動(dòng)態(tài)鏈接分為可執(zhí)行程序裝載時(shí)動(dòng)態(tài)鏈接和運(yùn)行時(shí)動(dòng)態(tài)鏈接。

六、使用gdb跟蹤分析一個(gè)execve系統(tǒng)調(diào)用內(nèi)核處理函數(shù)do_execve

在實(shí)驗(yàn)樓提供的環(huán)境中,給qemu增加一個(gè)使用execve系統(tǒng)調(diào)用的菜單命令,如下所示:


在menu目錄下執(zhí)行如下命令:make rootfs啟動(dòng)MenuOS,結(jié)果如下所示:

使用GDB進(jìn)行跟蹤調(diào)試,設(shè)置如下斷點(diǎn):

在MenuOS中輸入execve菜單命令以后,截圖如下所示:


do_execve函數(shù)源代碼如下所示:

int do_execve(struct filename *filename,const char __user *const __user *__argv,const char __user *const __user *__envp) {struct user_arg_ptr argv = { .ptr.native = __argv };struct user_arg_ptr envp = { .ptr.native = __envp };return do_execve_common(filename, argv, envp); // 此處調(diào)用do_execve_common }

裝載和啟動(dòng)一個(gè)可執(zhí)行程序的大致流程如下所示:
sys_execve -> do_execve-> do_execve_common-> exec_binprm-> search_binary_handler -> load_elf_binary-> start_thread

  • 對(duì)于靜態(tài)鏈接的可執(zhí)行文件,eip指向該文件的文件頭e_entry所指的入口地址;對(duì)于動(dòng)態(tài)鏈接的可執(zhí)行文件,eip指向動(dòng)態(tài)鏈接器。執(zhí)行靜態(tài)鏈接程序時(shí),execve系統(tǒng)調(diào)用修改內(nèi)核堆棧中保存的eip的值作為新的進(jìn)程的起點(diǎn)。
  • 新的可執(zhí)行程序修改內(nèi)核堆棧eip為新程序的起點(diǎn),從new_ip開始執(zhí)行,start_thread把返回到用戶態(tài)的位置從int 0x80的下一條指令變成新的可執(zhí)行文件的入口地址。
  • 執(zhí)行execve系統(tǒng)調(diào)用時(shí),調(diào)用execve的可執(zhí)行程序陷入內(nèi)核態(tài),使用execve加載的可執(zhí)行文件覆蓋當(dāng)前進(jìn)程的可執(zhí)行程序,當(dāng)execve系統(tǒng)調(diào)用返回時(shí),返回新的可執(zhí)行程序的起點(diǎn)(main函數(shù)),故新的可執(zhí)行程序能夠順利執(zhí)行。

八、理解Linux系統(tǒng)中進(jìn)程調(diào)度的時(shí)機(jī)

可以在內(nèi)核代碼中搜索schedule()函數(shù),看都是哪里調(diào)用了schedule(),判斷我們課程內(nèi)容中的總結(jié)是否準(zhǔn)確:

  • 中斷處理過程(時(shí)鐘中斷、I/O中斷、系統(tǒng)調(diào)用和異常)中,直接調(diào)用schedule(),或者返回用戶態(tài)時(shí)根據(jù)need_resched標(biāo)記調(diào)用schedule();
  • 內(nèi)核線程可以直接調(diào)用schedule()進(jìn)行進(jìn)程切換,也可以在中斷處理過程中進(jìn)行調(diào)度,內(nèi)核線程作為一類的特殊的進(jìn)程既可以進(jìn)行主動(dòng)調(diào)度,也可以進(jìn)行被動(dòng)調(diào)度;
  • 用戶態(tài)進(jìn)程無法實(shí)現(xiàn)主動(dòng)調(diào)度,只能夠通過陷入內(nèi)核態(tài)后的某個(gè)時(shí)機(jī)點(diǎn)進(jìn)行調(diào)度,即在中斷處理過程中進(jìn)行調(diào)度。

九、使用gdb跟蹤分析一個(gè)schedule()函數(shù)

在實(shí)驗(yàn)樓提供的環(huán)境中,設(shè)置斷點(diǎn)如下所示:


schedule()函數(shù)用于實(shí)現(xiàn)進(jìn)程調(diào)度,它的任務(wù)是從運(yùn)行隊(duì)列的鏈表中找到一個(gè)進(jìn)程,并且隨后將CPU分配給這個(gè)進(jìn)程。
從本質(zhì)上來說,每個(gè)進(jìn)程切換分為兩步:
1.切換頁全局目錄以安裝一個(gè)新的地址空間;
2.切換內(nèi)核態(tài)堆棧和硬件上下文,因?yàn)橛布舷挛奶峁┝藘?nèi)核執(zhí)行新進(jìn)程所需要的所有信息,包括CPU寄存器。

十、分析switch_to中的匯編代碼

#define switch_to(prev, next, last) // prev指向當(dāng)前進(jìn)程,next指向被調(diào)度的進(jìn)程 do { unsigned long ebx, ecx, edx, esi, edi; asm volatile("pushfl\n\t" /* 將標(biāo)志位壓棧 */ "pushl %%ebp\n\t" /* 將當(dāng)前ebp壓棧 */ "movl %%esp,%[prev_sp]\n\t" /* 保存當(dāng)前進(jìn)程的堆棧棧頂*/ "movl %[next_sp],%%esp\n\t" /* 將下一個(gè)進(jìn)程的堆棧棧頂保存到esp寄存器,完成內(nèi)核堆棧的切換*/ "movl $1f,%[prev_ip]\n\t" /* 保存當(dāng)前進(jìn)程的eip*/ "pushl %[next_ip]\n\t" /*將下一個(gè)進(jìn)程的eip壓棧 */ "jmp __switch_to\n" /*jmp通過后面的寄存器eax、edx來傳遞參數(shù),__switch_to()函數(shù)通過return把next_ip彈出來 */ "1:\t" "popl %%ebp\n\t" /*恢復(fù)當(dāng)前堆棧的ebp*/ "popfl\n" /* 恢復(fù)當(dāng)前堆棧的寄存器標(biāo)志位*/ /* output parameters */ : [prev_sp] "=m" (prev->thread.sp), // 當(dāng)前內(nèi)核堆棧的棧頂[prev_ip] "=m" (prev->thread.ip), // 當(dāng)前進(jìn)程的eip "=a" (last), /* clobbered output registers: */ "=b" (ebx), "=c" (ecx), "=d" (edx), "=S" (esi), "=D" (edi) /* input parameters: */ : [next_sp] "m" (next->thread.sp), // 下一個(gè)進(jìn)程的內(nèi)核堆棧的棧頂[next_ip] "m" (next->thread.ip), // 下一個(gè)進(jìn)程的eip/* regparm parameters for __switch_to(): */ [prev] "a" (prev), // 寄存器的傳遞[next] "d" (next)); __switch_canary_iparam : /* reloaded segment registers */ "memory"); } while (0)

switch_to實(shí)現(xiàn)了進(jìn)程之間的真正切換:
1.首先在當(dāng)前進(jìn)程prev的內(nèi)核棧中保存esi,edi及ebp寄存器的內(nèi)容。
2.然后將prev的內(nèi)核堆棧指針ebp存入prev->thread.esp中。
3.把將要運(yùn)行進(jìn)程next的內(nèi)核棧指針next->thread.esp置入esp寄存器中
4.將popl指令所在的地址保存在prev->thread.eip中,這個(gè)地址就是prev下一次被調(diào)度
5.通過jmp指令(而不是call指令)轉(zhuǎn)入一個(gè)函數(shù)__switch_to()
6.恢復(fù)next上次被調(diào)離時(shí)推進(jìn)堆棧的內(nèi)容。從現(xiàn)在開始,next進(jìn)程就成為當(dāng)前進(jìn)程而真正開始執(zhí)行

總結(jié)

1.Linux通過復(fù)制父進(jìn)程來創(chuàng)建一個(gè)新進(jìn)程,通過調(diào)用do_fork來實(shí)現(xiàn)并為每個(gè)新創(chuàng)建的進(jìn)程動(dòng)態(tài)地分配一個(gè)task_struct結(jié)構(gòu)。fork()函數(shù)被調(diào)用一次,但返回兩次。可以通過fork,復(fù)制一個(gè)已有的進(jìn)程,進(jìn)而產(chǎn)生一個(gè)子進(jìn)程。
2.Linux的進(jìn)程調(diào)度基于分時(shí)技術(shù)和進(jìn)程的優(yōu)先級(jí),內(nèi)核通過調(diào)用schedule()函數(shù)來實(shí)現(xiàn)進(jìn)程調(diào)度,其中context_switch宏用于完成進(jìn)程上下文切換,它通過調(diào)用switch_to宏來實(shí)現(xiàn)關(guān)鍵上下文切換。
3.進(jìn)程上下文切換需要保存切換進(jìn)程的相關(guān)信息(thread.sp和thread.ip);中斷上下文的切換是在一個(gè)進(jìn)程的用戶態(tài)到一個(gè)進(jìn)程的內(nèi)核態(tài),或從進(jìn)程的內(nèi)核態(tài)到用戶態(tài),切換進(jìn)程需要在不同的進(jìn)程間切換,但一般進(jìn)程上下文切換是套在中斷上下文切換中的。
4.Linux系統(tǒng)的一般執(zhí)行過程可以抽象成正在運(yùn)行的用戶態(tài)進(jìn)程X切換到運(yùn)行用戶態(tài)進(jìn)程Y的過程。

總結(jié)

以上是生活随笔為你收集整理的理解进程创建、可执行文件的加载和进程执行进程切换,重点理解分析fork、execve和进程切换的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。