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

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

进程的创建——fork函数

發布時間:2023/12/14 编程问答 27 豆豆
生活随笔 收集整理的這篇文章主要介紹了 进程的创建——fork函数 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

1. 進程的信息

  • 進程的結構
    在Linux中,一切皆文件,進程也是保存在內存中的一個實例,下圖描述了進程的結構:
  • 堆棧:保存局部變量
  • 數據段:一般存放全局變量和靜態變量
  • 代碼段:存儲進程的代碼文件
  • TSS狀態段:進程做切換時,需要保存進程現場以便恢復,一般存儲一些寄存器的值。
  • task_struct : 進程的結構體描述符,用來描述一個進程的屬性,這是一種面向對象的編程思想,task_struct的結構大致如下:
struct task_struct {long state; //* -1 unrunnable, 0 runnable, >0 stopped * 進程的狀態long counter; // 時間片long priority; //優先級long signal; //信號struct sigaction sigaction[32];long blocked;int exit_code;unsigned long start_code,end_code,end_data,brk,start_stack;long pid,father,pgrp,session,leader; //進程號,父進程號,會話等unsigned short uid,euid,suid;unsigned short gid,egid,sgid;long alarm; //警告long utime,stime,cutime,cstime,start_time;//用戶態、內核態執行時間unsigned short used_math;int tty;unsigned short umask;struct m_inode * pwd;struct m_inode * root;struct m_inode * executable;unsigned long close_on_exec;struct file * filp[NR_OPEN]; //打開的文件列表struct desc_struct ldt[3]; //ldt段struct tss_struct tss; //tss段 };

通過管理進程對應的task_struct,可以完成進程的相關操作。

  • GDT和LDT

操作系統在保護模式下,內存管理分為分段模式和分頁模式。分段模式下內存的尋址為「段基址:偏移地址」。對一個段的描述包括以下三個方面:【Base Address,Limit,Access】,他們加在一起被放在一個64bit長的數據結構中,被稱為段描述符。因此需要用64bit的寄存器去存儲段描述符,但是操作系統的段基址寄存器只能存儲16bit的數據,因此無法直接存儲64bit的段描述符,為了解決這個問題,操作系統將段描述符存放在一個全局的數組中,而段寄存器直接存儲對應的段描述符對應的下標,這個全局的數組叫做GDT

由于GDT也需要直接存在內存中,所以操作系統用GDTR寄存器來存儲GDT的基地址,因此尋 址的過程為:

1. 通過GDTR寄存器找到GDT的基地址 2. 通過段寄存器找到段描述符的索引 3. 通過GDT基地址+索引從GDT數組中找到段描述符 4. 通過段描述符基地址+偏移地址找到線性地址

至于LDT,本質上和GDT是類似的,但也有不一樣的地方,LDT本身也是一段內存,因此需要段描述符去描述它,它的段描述符存在GDT中,而LDT有LDTR寄存器,LDTR并不存儲LDT的段基址,而是一個段選擇子,是LDT的索引。

用一張圖來詮釋GDT和LDT的尋址過程:

  • 進程的狀態

進程一共有五種狀態,分別是:

  • TASK_RUNNING 運行狀態

    運行狀態表示正在運行,只有這個狀態的進程才能被執行

  • TASK_INTERRUPTIBLE 可中斷睡眠狀態

    當一個進程處于可中斷睡眠狀態時,它不會占用cpu資源,但是它可以響應中斷或者信號。如socket等待連接建立時,它是睡眠的,但是連接一旦建立就會被喚醒。這種狀態就是阻塞狀態。

  • TASK_UNINTERRUPTIBLE 不可中斷睡眠狀態

和TASK_INTERRUPTIBLE不同,處于TASK_UNINTERRUPTIBLE狀態的進程無法被中斷或者信號喚醒,假設一個進程是TASK_UNINTERRUPTIBLE狀態,你會驚奇的發現通過kill -9無法殺死該進程,因為它無法響應異步信號。這種狀態很少見,一般發生在內核態程序中,如讀取某個設備的文件,需要通過read系統調用通過驅動操作硬件設備讀取,這個過程是無法被中斷的。

  • TASK_ZOMBIE 僵死狀態

    處于TASK_ZOMBIE狀態的進程并不代表著進程已經被銷毀,此時除了task_struct,進程占有的所有資源將被釋放,之所以不釋放task_struct是因為task_struct保存著一些統計信息,其父進程可能需要這些信息。

  • TASK_STOPPED

    不保留task_struct,進程資源全部被釋放

2. 系統初始化——main函數

上面大致介紹了和進程相關的一些信息說明,本節將從kernel的main.c方法開始,分析進程的創建過程.

Linux的main.c文件,是Linux開機時內核初始化函數,在初始化的過程中,內核將創建系統的第一個進程:0號進程,0號進程不做任何操作,也不能被終止(除非系統異常或者關機),以后創建的每一個進程都是0號進程的子孫進程。

main.c的入口是main()函數,main()函數的主要實現:

void main(void){ROOT_DEV = ORIG_ROOT_DEV;drive_info = DRIVE_INFO; //省略了一段內存初始化操作mem_init(main_memory_start,memory_end); // 主內存區初始化trap_init(); // 陷阱門(硬件中斷向量)初始化blk_dev_init(); // 塊設備初始化chr_dev_init(); // 字符設備初始化tty_init(); // tty初始化time_init(); // 設置開機啟動時間 startup_timesched_init(); // 調度程序初始化(加載任務0的tr,ldtr)buffer_init(buffer_memory_end); // 緩沖管理初始化,建內存鏈表等。hd_init(); // 硬盤初始化floppy_init(); // 軟驅初始化sti(); // 所有初始化工作都做完了,開啟中斷// 下面過程通過在堆棧中設置的參數,利用中斷返回指令啟動任務0執行。move_to_user_mode(); // 移到用戶模式下執行if (!fork()) { init(); // 在新建的子進程(任務1)中執行。}for(;;) pause(); }

我們來看一看和進程管理相關的兩個初始化過程:

  • time_init()
  • static void time_init(void) {struct tm time;do {time.tm_sec = CMOS_READ(0);time.tm_min = CMOS_READ(2);time.tm_hour = CMOS_READ(4);time.tm_mday = CMOS_READ(7);time.tm_mon = CMOS_READ(8);time.tm_year = CMOS_READ(9);} while (time.tm_sec != CMOS_READ(0));BCD_TO_BIN(time.tm_sec);BCD_TO_BIN(time.tm_min);BCD_TO_BIN(time.tm_hour);BCD_TO_BIN(time.tm_mday);BCD_TO_BIN(time.tm_mon);BCD_TO_BIN(time.tm_year);time.tm_mon--; // tm_mon中月份的范圍是0-11startup_time = kernel_mktime(&time); // 計算開機時間 }

    time_init()函數主要從CMOS管中讀取一個實時時鐘的年月日時分秒等信息并保存起來,并且通過kernel_mktime()函數來計算一個startup_time作為系統的開機時間,而kernel_time()函數就是根據從CMOS讀出的信息計算出1970年1月1日到現在的一個時間。

    分析這個函數主要想介紹一下內核一個很重要的時間概念:jiffies(系統滴答)
    jiffies是系統的脈搏,或者說是系統的節拍。在內核中,系統會以一定的頻率發生定時中斷,也就是說,某個進程正在運行,運行一段時間后系統會暫停這個進程運行,然后切換到另一個進程運行,jiffies決定了系統發生中斷的頻率,因此它和進程的調度息息相關。

  • sched_init()
  • void sched_init(void) {int i;struct desc_struct * p; // 描述符表結構指針if (sizeof(struct sigaction) != 16) // sigaction 是存放有關信號狀態的結構panic("Struct sigaction MUST be 16 bytes");set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));p = gdt+2+FIRST_TSS_ENTRY;for(i=1;i<NR_TASKS;i++) {task[i] = NULL;p->a=p->b=0;p++;p->a=p->b=0;p++;} }

    首先來看一個變量task[],它的定義為:

    struct task_struct * task[NR_TASKS] = {&(init_task.task), }

    在sched.c文件中,定義了一個task數組,這個數組的類型為task_struct結構體,它的最大容量NR_TASKS=64,它的初始值也就是task[0] = init_task.task。

    這個task數組的意義是:

    task是一個保存進程結構體的數組,最大容量為64,task[0]的位置保存了0號進程task_struct

    再回到sched_init()的代碼,首先定義了一個desc_struct指針,然后為0號進程設置了它的ldt段和tss段,ldt段是由數據段和代碼段構成的。然后從1開始遍歷task數組,將每個槽設置為null,并將其gdt設置為空,由于是從1開始遍歷,因此處于index=0的0號進程不會被置空。可見,sched_init()函數創建了0號進程。

    3. 進程的創建——fork()函數

    進行一系列初始化后,執行了這句代碼:

    move_to_user_mode();

    這個函數是將當前模式由內核態轉為用戶態。

    內核態:不可搶占的
    用戶態:可搶占,可以進行調度的

    也就是說,上述所有的初始化操作都是在內核態執行的,這么做的目的是,內核初始化過程是不能被中斷的,在內核態運行可以保證這一點。

    切換到用戶態以后,便開始創建進程了:

    if (!fork()) { init(); }

    這里的fork()函數,就是linux創建進程的函數,進入這個函數,它的聲明為:

    static inline _syscall0(int,fork)

    syscall是系統調用函數,就是內核自己實現的一些函數,如 read,open,chmod等,這些函數可以直接提供給開發人員調用,而調用的過程需要切換到內核態進行,因為函數調用過程中不允許被中斷。這個調用過程稱為系統調用。

    _syscall0的函數定義如下:

    #define _syscall0(type,name) \ type name(void) \ { \ long __res; \ __asm__ volatile ("int $0x80"\: "=a" (__res) \: "0" (__NR_##name)); \ if (__res >= 0) \return (type) __res; \ errno = -__res; \ return -1; \ }

    把參數替換成fork以后,是這個樣子:

    #define _syscall0(type,fork) \ type name(void) \ { \ long __res; \ __asm__ volatile ("int $0x80"\: "=a" (__res) \: "0" (__NR_FORK)); \ if (__res >= 0) \return (type) __res; \ errno = -__res; \ return -1; \ }

    這是一段匯編代碼,這段代碼的執行過程是這樣的:

  • 將_res變量和eax寄存器綁定,后面_res變量的值就是從eax寄存器讀出來的值
  • 將_NR_FORK=2 賦值給eax寄存器
  • "int $0x80"產生一個軟中斷,由于之前sched_init()函數中設置了0x80中斷服務函數:
    set_system_gate(0x80,&system_call),因此產生0x80中斷以后會調用system_call,system_call在system_call.s中定義,
  • system_call:cmpl $nr_system_calls-1,%eax # 調用號如果超出范圍的話就在eax中置-1并退出ja bad_sys_callpush %ds # 保存原段寄存器值push %espush %fspushl %edxpushl %ecx pushl %ebx movl $0x10,%edx mov %dx,%dsmov %dx,%esmovl $0x17,%edxmov %dx,%fscall sys_call_table(,%eax,4) # 間接調用指定功能C函數pushl %eax # 把系統調用返回值入棧movl current,%eax # 取當前任務(進程)數據結構地址→eaxcmpl $0,state(%eax) jne reschedulecmpl $0,counter(%eax)je rescheduleret_from_sys_call:movl current,%eax # task[0] cannot have signalscmpl task,%eaxje 3f # 向前(forward)跳轉到標號3處退出中斷處理cmpw $0x0f,CS(%esp) jne 3fcmpw $0x17,OLDSS(%esp) jne 3fmovl signal(%eax),%ebx # 取信號位圖→ebx,1位代表1種信號,共32個信號movl blocked(%eax),%ecx # 取阻塞(屏蔽)信號位圖→ecxnotl %ecx # 每位取反andl %ebx,%ecx # 獲得許可信號位圖bsfl %ecx,%ecx # 從低位(0)開始掃描位圖,看是否有1的位,若有,則ecx保留該位的偏移值je 3f # 如果沒有信號則向前跳轉退出btrl %ecx,%ebx # 復位該信號(ebx含有原signal位圖)movl %ebx,signal(%eax) # 重新保存signal位圖信息→current->signal.incl %ecx # 將信號調整為從1開始的數(1-32)pushl %ecx # 信號值入棧作為調用do_signal的參數之一call do_signal # 調用C函數信號處理程序(kernel/signal.c)popl %eax # 彈出入棧的信號值 3: popl %eax # eax中含有上面入棧系統調用的返回值popl %ebxpopl %ecxpopl %edxpop %fspop %espop %dsiret

    首先將各寄存器入棧,然后調用了關鍵的一個函數:

    call sys_call_table(,%eax,4)

    sys_call_table是一個數組,在sys.h中定義,它保存著所有系統調用的函數名,%eax就是eax寄存器的值,前面提到過0x80中斷產生之前將_NR_FORK=2加入到了eax寄存器中,因此調用的就是sys_call_table[2],也就是sys_fork函數:

    sys_fork:call find_empty_processtestl %eax,%eax js 1fpush %gspushl %esipushl %edipushl %ebppushl %eaxcall copy_processaddl $20,%esp 1: ret

    這是一段匯編代碼,首先調用了fork.c文件中的find_empty_process函數,目的在于從task進程數組中找到一個空的槽用于保存要創建的進程的task_struct,這個函數會返回進程的pid。

    testl %eax,%eax 指令作用是將call find_empty_process函數的返回值保存到eax寄存器中。隨后進行了一系列寄存器數的壓棧,最后調用了copy_process()函數:

    int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,long ebx,long ecx,long edx,long fs,long es,long ds,long eip,long cs,long eflags,long esp,long ss) {struct task_struct *p;int i;struct file *f;p = (struct task_struct *) get_free_page();if (!p)return -EAGAIN;task[nr] = p;*p = *current; p->state = TASK_UNINTERRUPTIBLE;p->pid = last_pid; // 新進程號。也由find_empty_process()得到。p->father = current->pid; // 設置父進程p->counter = p->priority; // 運行時間片值p->signal = 0; // 信號位圖置0p->alarm = 0; // 報警定時值(滴答數)p->leader = 0; p->utime = p->stime = 0; // 用戶態時間和和心態運行時間p->cutime = p->cstime = 0; // 子進程用戶態和和心態運行時間p->start_time = jiffies; // 進程開始運行時間(當前時間滴答數)p->tss.back_link = 0;p->tss.esp0 = PAGE_SIZE + (long) p; // 任務內核態棧指針。p->tss.ss0 = 0x10; // 內核態棧的段選擇符(與內核數據段相同)p->tss.eip = eip; // 指令代碼指針p->tss.eflags = eflags; // 標志寄存器p->tss.eax = 0; // 這是當fork()返回時新進程會返回0的原因所在p->tss.es = es & 0xffff; // 段寄存器僅16位有效p->tss.ldt = _LDT(nr); // 任務局部表描述符的選擇符(LDT描述符在GDT中)p->tss.trace_bitmap = 0x80000000; // 高16位有效if (copy_mem(nr,p)) {task[nr] = NULL;free_page((long) p);return -EAGAIN;}set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss));set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));p->state = TASK_RUNNING; /* do this last, just in case */return last_pid; }

    這個函數非常長,省略了一些無關代碼,主要有以下幾個重要步驟:

  • 定義了一個task_struct *p,并為其分配了內存,然后放到task數組對應的槽當中
  • 將當前進程指針賦值給p
  • *p = *current

    *current指向當前進程,也就是調用fork函數的進程,本過程中就是0號進程,*p指向要創建的進程,本過程中就是1號進程。*p=*current,這不就是將0號進程的task_struct直接賦值給了1號進程嗎?原來進程的創建第一步都是先把它的父進程拿來拷貝一份。

  • 將進程p的狀態設置為不可中斷睡眠狀態
  • p->state = TASK_UNINTERRUPTIBLE

    這么做的目的是當前進程既不能處理信號,也無法參與調度。

  • 設置task_struct特定屬性
    要創建的進程p是通過拷貝父進程task_struct而來,但是作為一個進程,它需要有自己特定的屬性,因此需要對其特定的屬性進行賦值:
  • p->pid = last_pid; // 新進程號。也由find_empty_process()得到。p->father = current->pid; // 設置父進程p->counter = p->priority; // 運行時間片值p->signal = 0; // 信號位圖置0p->alarm = 0; // 報警定時值(滴答數)p->utime = p->stime = 0; // 用戶態時間和和內核運行時間p->cutime = p->cstime = 0; // 子進程用戶態和和內核運行時間p->start_time = jiffies; // 進程開始運行時間(當前時間滴答數)p->tss.esp0 = PAGE_SIZE + (long) p; // 任務內核態棧指針。p->tss.ss0 = 0x10; // 內核態棧的段選擇符(與內核數據段相同)p->tss.eip = eip; // 指令代碼指針p->tss.eflags = eflags; // 標志寄存器p->tss.eax = 0; //eax寄存器p->tss.ldt = _LDT(nr); // 任務局部表描述符的選擇符(LDT描述符在GDT中)

    大部分屬性都容易看懂,但是有兩個地方卻暗藏玄機:

    p->tss.eax = 0; p->tss.eip = eip;

    看似很常規的兩行代碼: 將子進程的eax寄存器設置為0,將子進程eip寄存器設置為父進程的eip寄存器值。

    eax寄存器存儲著函數的返回值,而eip寄存器,存儲著cpu要去讀取的下一行指令代碼的位置,那尋根溯源一下,父進程下一行代碼是哪一行呢?

    事實上,我們正在分析的fork系統調用的代碼并不屬于父進程執行的代碼,它屬于內核態程序,真正父進程執行的代碼應該是它產生軟中斷而調用fork系統調用的下一行,也就是這個:

    #define _syscall0(type,fork) \ type name(void) \ { \ long __res; \ __asm__ volatile ("int $0x80"\: "=a" (__res) \: "0" (__NR_FORK)); \ if (__res >= 0) \return (type) __res; \ errno = -__res; \ return -1; \

    中的這一行

    if (__res >= 0)

    這也就意味著,當子進程開始運行的時候,會從這一行開始執行。

  • 設置進程的tss和ldt
  • set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss)); set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));
  • 將進程p狀態改為運行狀態
  • p->state = TASK_RUNNING;

    進程創建到這里,已經為要創建的進程創建了task_struct,并完成了task_struct初始化,也設置了進程的ldt段和tss段,那么這個進程已經可以開始運行并可以參與調度了,因此將進程設置為就緒狀態。

  • 返回進程id
  • return last_pid

    就是返回子進程的id,這里返回的是一號進程的id 1。

    copy_process()函數返回了,sys_fork也就返回了,返回的值就是copy_process函數的返回值,這里就是一號進程的id=1,然后就返回到system_call執行,將各寄存器值出棧,然后0x80中斷就返回了,將切換到用戶態繼續執行0號進程的代碼,_syscall0將繼續往下執行

    #define _syscall0(type,fork) \ type name(void) \ { \ long __res; \ __asm__ volatile ("int $0x80"\: "=a" (__res) \: "0" (__NR_FORK)); \ if (__res >= 0) \return (type) __res; \ errno = -__res; \ return -1; \ }

    前面提到過_res的值就是eax,當前0號進程eax值就是sys_fork調用的copy_process()的返回值,前面提到了copy_process()返回的是子進程1號進程的進程id,也就是1。因此if條件符合,_syscall0返回1。

    到這里fork()函數執行完畢并返回了1,回到fork調用的地方

    if (!fork()) {init(); } for(;;) pause();

    由于fork()返回了1,所以這里if條件不符合,往下走到一段死循環,循環里調用了pause()函數,點開pause函數的聲明:

    static inline _syscall0(int,pause)

    啊這。。又是這玩意,看到這里立馬就懂了,pause()函數也是一個系統調用,省去中間系統調用的過程,直接來到pause調用的函數:

    int sys_pause(void) {current->state = TASK_INTERRUPTIBLE;schedule();return 0; }

    與前面fork系統調用不同的是,pause系統調用是c語言實現的。pause先將進程狀態設置為可中斷睡眠狀態,然后進行了一次schedule()也就是進行了一次進程調度,由于當前只有0號和1號兩個進程,所以進程調度的結果肯定是由0號進程切換到1號進程,至于進程的調度和進程的切換,后面會有詳細介紹這里就不展開了。

    現在正在運行的是1號進程,cpu就會找到gdt找到1號進程的ldt,就會讀取1號進程的eip寄存器去讀取指令。現在重點來了,我們在介紹0號進程fork1號進程的時候提示過,當時0號進程將自己的eip寄存器值賦給了1號進程,所以1號進程eip寄存器存儲的下一行代碼是:

    if (__res >= 0)

    _res是eax寄存器的值,而fork的時候0號進程將1號進程的eax寄存器值得設置為了0

    p->tss.eax = 0;

    因此if條件也是符合的,就將_res 返回了,返回到哪里了呢?返回的肯定是fork()函數被調用的地方,也就是:

    if (!fork()) {init(); } for(;;) pause();

    看到這你可能有點懵逼了,怎么又到這來了,0號進程不是已經執行過一次了嗎,又來。。

    但是和之前不同的是,這里的fork()返回的_res值是0,是符合if條件的,然后會執行init()。。

    這就是fork()函數很神秘的地方,它實現了一個函數"return了兩次",一次是父進程返回,一次是子進程返回。

    至于init()函數,里面涉及了一些shell初始化,tty0初始化,輸入輸出設備初始化操作,跟進程的創建沒有太大的關系,就不繼續展開了。

    4. 總結

    本文以0號進程創建1號進程為例,分析了fork()函數詳細的過程,有以下:

  • sched_init()定義了task[64],并將第0個位置保存0號進程task_struct,隨后0號進程被創建
  • 從內核態切換到用戶態,開始運行0號進程
  • 0號進程調用fork()系統調用,產生一個0x80軟中斷,調用了sys_fork函數
  • sys_fork先為1號進程分配了id,然后調用了copy_process函數
  • copy_process函數將0號進程的task_struct賦值給1號進程,然后設置了1號進程特定的屬性,并設置了eip和eax寄存器
  • 1號進程創建完畢,返回0號進程執行,0號進程調用pause()休眠,進程調度到了1號進程執行
  • 1號進程返回_res=0,然后執行init()
  • 總結

    以上是生活随笔為你收集整理的进程的创建——fork函数的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。