內核中current實現


一、當前進程current

在內核中,current絕對是一個出鏡率非常高的變量,在幾乎所有的系統調用中都會用到該變量。由於該變量被使用的頻率比較高,所以它的實現要盡可能的快速高效。在最早的內核版本中,這個實現在內核的不同版本中一直在變化,從這個變量也可以引申出一些有意思的問題。
二、早期內核實現
在1.0內核版本中,current定義為一個全局變量,初始值為init_task,在每次執行進程切換時,更新這個全局變量的值,其它地方通過對區局變量的引用來獲得當前進程。下面是1.0版本中 current更新代碼 ,這里拷貝一份過來
 357#define switch_to(tsk) \
 358__asm__("cmpl %%ecx, _current\n\t" \
 359        "je 1f\n\t" \
 360        "cli\n\t" \
 361        "xchgl %%ecx,_current\n\t" \
 362        "ljmp %0\n\t" \
 363        "sti\n\t" \
 364        "cmpl %%ecx,_last_task_used_math\n\t" \
 365        "jne 1f\n\t" \
 366        "clts\n" \
 367        "1:" \
 368        : /* no output */ \
 369        :"m" (*(((char *)&tsk->tss.tr)-4)), \
 370         "c" (tsk) \
 371        :"cx")
其中使用內嵌匯編在switch時相當於掛了個鈎子,實時更新current變量的值。
老實講,這種方法獲得current的效率是相當高的,在386體系下,如果要引用這個變量的值,使用一條匯編指令即可完成。但是這種方法有一個非常嚴重的缺陷,就是不支持多核。假設系統中有多個CPU,而系統中的全局變量只有一個,所以每個cpu上看到的current相同,這樣多核就沒有意義。
三、2.4.0對多核的實現
內核2.0版本的一個重要特性就是支持多核,所以看下2.0內核中對於current的實現。看了之后發現2.0對於多核的支持可能只限於386和sparc體系結構,而且實現的方法太過詭異,那我們只好直接掠過了。 看下更為近代的2.4.0版本的實現
   6static inline struct task_struct * get_current(void)
   7{
   8        struct task_struct *current;
   9        __asm__("andl %%esp,%0; ":"=r" (current) : "0" (~8191UL));
  10        return current;
  11 }
  12 
  13#define current get_current()
這里獲得current同樣只是使用了一條指令,這也是我們常見的實現描述方式。利用了內核中內核態esp指針的高端是內核態堆棧,底端是task_struct結構的特性。我想大部分看過早期linux內核書籍的人都會對這種結構有印象。
這里只是一個現象,因為這里依然看不出來多核下的這段代碼在不同的CPU上如何獲得不同的當前變量值。
1、386體系結構下硬件任務管理支持
386體系結構加入了特權級功能,用戶態任務運行在低特權級別(3級)上,內核級操作系統運行在高權限級別上(0級),並且在特定場合下需要進行切換,例如中斷指令,硬件中斷,軟件除零異常、內存訪問異常等。此時就需要切換到高權限級別運行。
"運行"涉及到兩個基本的概念,一個是"堆棧",一個是"指令地址"。中斷的發生是不可預測的,當中斷發生之后,內核如何找到特定權限級別狀態下執行必須的堆棧和指令地址呢?
386系統為定義了一個專門的內核態寄存器(即用戶態沒有權限修改該寄存器內容)TR,該寄存器和我們常見的段寄存器(segment)例如CS、FS等段寄存器一樣,指向了一個GDT中一個描述符,這個描述符的屬性為Task Status Segment,和這個描述符並列的還有中斷門、陷阱門、系統調用門等,它們在GDT中通過特定的bit組合表示自己的類型。TR寄存器指向了GDT中一個特定的TSS結構,這個結構就是intel為進程准備的context信息。按照intel CPU的這種設計初衷,當內核需要進行任務切換的時候,在GDT中設置一個特定的任務門(Task Gate),通過段間跳轉指令(call,jmp,iret等)來索引這個段描述符,當CPU發現新的段描述符是一個task gate時,執行當前進程TSS的保存,將新task gate中指定的TSS段地址加載到TR寄存器中,並設置TSS中的Previous字段。這里有一個小的細節,在段間跳轉時,需要段和段偏移量個操作符,但是如果段描述符的類型是一個task gate,此時偏移量字段被忽略。intel指令集中對於call指令的描述中可以看到相關內容的說明:
Executing a task switch with the CALL instruction is similar to executing a call through a call gate. The target operand specifies the segment selector of the task gate for the new task activated by the switch (the offset in the target operand is ignored).
這個TSS結構的特定內容由intel開發規范規定,大致來看,文檔描述為這樣的一個結構。
內核中current實現 - Tsecer - Tsecer的回音島
結構中為0-2三種特權級都定義了各自的堆棧地址(SS和ESP絕對一個堆棧地址,此處依然使用段結構)。
當發生特權級切換時(例如中斷發生,調用 int 指令),此時硬件會根據TR-->>TSS結構中的信息自動刷新CPU寄存器的內容,這里最為重要的就是EIP和ESP寄存器了。
2、當中斷發生時CPU的執行流程
An interrupt gate or trap gate references an exception- or interrupt-handler procedure that runs in the context of the currently executing task (see Figure 6-3). The segment selector for the gate points to a segment descriptor for an executable code segment in either the GDT or the current LDT. The offset field of the gate descriptor points to the beginning of the exception- or interrupt-handling procedure.
When the processor performs a call to the exception- or interrupt-handler procedure:
? If the handler procedure is going to be executed at a numerically lower privilege level, a stack switch occurs.When the stack switch occurs: a. The segment selector and stack pointer for the stack to be used by the handler are obtained from the TSS for the currently executing task. On this new stack, the processor pushes the stack segment selector and stack pointer of the interrupted procedure. b. The processor then saves the current state of the EFLAGS, CS, and EIP registers on the new stack (seeFigures 6-4). c. If an exception causes an error code to be saved, it is pushed on the new stack after the EIP value. ? If the handler procedure is going to be executed at the same privilege level as the interrupted procedure: a. The processor saves the current state of the EFLAGS, CS, and EIP registers on the current stack (see Figures 6-4). b. If an exception causes an error code to be saved, it is pushed on the current stack after the EIP value.
內核中current實現 - Tsecer - Tsecer的回音島
當中斷或者切換發生時,目的EIP地址通過描述符中的段選擇符和段內偏移字段一起決定跳轉的目的地址,而當前任務TSS中的SS和ESP決定了切換后堆棧的位置。下面是一些門描述符的內部定義格式,其中的Segment Select選擇GDT中一個代碼段描述符,Offset指定基地址基礎上的32bit偏移量。
內核中current實現 - Tsecer - Tsecer的回音島
 
4、linux內核中的做法
linux內核中為每個CPU設置了一個私有的TSS結構(而不是intel設計CPU時的每個task一個TSS結構),在進程切換時更新這個TSS中的字段,以386實現為例
 627void __switch_to(struct task_struct *prev_p, struct task_struct *next_p)
 628{
 629        struct thread_struct *prev = &prev_p->thread,
 630                                 *next = &next_p->thread;
 631        struct tss_struct *tss = init_tss + smp_processor_id();
 632
 633        unlazy_fpu(prev_p);
 634
 635        /*
 636         * Reload esp0, LDT and the page table pointer:
 637         */
 638        tss->esp0 = next->esp0;
當進程切換之后,使用下一個進程的esp0寄存器來更新tss中的esp0值,而每個進程的esp0為自己的堆棧棧頂地址。
esp0為一個線程私有結構,其初始值在do_fork-->>copy_thread
 529int copy_thread(int nr, unsigned long clone_flags, unsigned long esp,
 530        unsigned long unused,
 531        struct task_struct * p, struct pt_regs * regs)
 532{
 533        struct pt_regs * childregs;
 534
 535        childregs = ((struct pt_regs *) (THREAD_SIZE + (unsigned long) p)) - 1;
 536        struct_cpy(childregs, regs);
 537        childregs->eax = 0;
 538        childregs->esp = esp;
 539
 540        p->thread.esp = (unsigned long) childregs; 內核態當前棧頂位置,整個pt_regs結構在進入內核中通過SAVE_ALL(linux-2.6.21\arch\i386\kernel\entry.S)保存。
 541        p->thread.esp0 = (unsigned long) (childregs+1); esp0為整個內核態堆棧(THREAD_SIZE字節)最頂端 542
 543        p->thread.eip = (unsigned long) ret_from_fork;
 544
 545        savesegment(fs,p->thread.fs);
 546        savesegment(gs,p->thread.gs);
 547
 548        unlazy_fpu(current);
 549        struct_cpy(&p->thread.i387, &current->thread.i387);
 550
 551        return 0;
 552}
總起來說,它實現的方法是在進程切換時,將當前進程(current)的棧頂位置保存在了TR寄存器指向的TSS段的esp0字段中,當中斷發生時,CPU保證進入內核后堆棧的位置在TSS中描述的位置,進而可以得到當前CPU的current的task_struct結構。
5、smp_processor_id的實現
在前一節中還有一個問題沒有說明清楚,在獲得當前CPU使用的tss字段時,並沒有通過TR寄存器指向的tss段來獲得,而是通過了smp_processor_id宏獲得CPU的編號,以該編號為下標從一個內存數組中獲得該CPU對應的TSS結構,那么一個CPU是如何獲得自己的CPU編號呢(所有的CPU執行相同的指令,但是返回值不同)?
#define smp_processor_id() (current->processor)
也就是每個進程的task_struct結構中保存了自己所在的處理器的編號,它這個編號又是從哪里來的呢?
同樣是在schedule函數中
 508asmlinkage void schedule(void)
 509{
 510        struct schedule_data * sched_data;
 511        struct task_struct *prev, *next, *p;
 512        struct list_head *tmp;
 513        int this_cpu, c;
 514
 515        if (!current->active_mm) BUG();
 516need_resched_back:
 517        prev = current;
 518        this_cpu = prev->processor;
……
 587#ifdef CONFIG_SMP
 588        next->has_cpu = 1;
 589        next->processor = this_cpu;
 590#endif
當一個進程獲得調度權時,它繼承前一個task_struct的processor字段。由於每個cpu上都有一個0號進程(或者說idle task),它們的初始值在每個CPU啟動之后由主CPU(BootStrap Processor)逐個賦值,從而一脈相承,連綿不絕。
四、2.6.0對多核中current的實現
從之前的實現可以看到一個問題,那就是每個task_struct結構都需要保留一個processor字段來表示自己在哪個CPU上,顯得有些浪費,這個字段也只是對於在運行的task才有意義。理論上說,一個cpu不依賴於task_struct結構就應該可以獲得自己所在CPU編號。
1、實現代碼
static __always_inline struct task_struct *get_current(void)
{
	return read_pda(pcurrent);
}
 
#define current get_current()
#define read_pda(field) pda_from_op("mov",field)
/* This variable is never instantiated.  It is only used as a stand-in
   for the real per-cpu PDA memory, so that gcc can understand what
   memory operations the inline asms() below are performing.  This
   eliminates the need to make the asms volatile or have memory
   clobbers, so gcc can readily analyse them. */
extern struct i386_pda _proxy_pda;
#define pda_from_op(op,field) \ ({ \ typeof(_proxy_pda.field) ret__; \ switch (sizeof(_proxy_pda.field)) { \ case 1: \ asm(op "b %%fs:%c1,%0" \ : "=r" (ret__) \ : "i" (pda_offset(field)), \ "m" (_proxy_pda.field)); \ break; \ case 2: \ asm(op "w %%fs:%c1,%0" \ : "=r" (ret__) \ : "i" (pda_offset(field)), \ "m" (_proxy_pda.field)); \ break; \ case 4: \ asm(op "l %%fs:%c1,%0" \ : "=r" (ret__) \ : "i" (pda_offset(field)), \ "m" (_proxy_pda.field)); \ break; \ default: __bad_pda_field(); \ } \ ret__; })
這里的宏展開之后只有一條指令,就是從fs段基礎位置便宜特定字段之后獲得一個變量,或者說,這里假設一個CPU的fs寄存器指向的是一個CPU私有的
struct i386_pda { struct i386_pda *_pda; /* pointer to self */ int cpu_number; struct task_struct *pcurrent; /* current process */ struct pt_regs *irq_regs; };
每個CPU自己當前運行的進程task_struct和自己的邏輯編號都在其中。和之前相比,這種引用翻了過來。前一個實現中是task_struct中保存CPU編號,現在是CPU信息中包含了current和cpu編號。
2、fs指向內容的初始化
cpu_init-->>cpu_set_gdt-->>set_kernel_fs
static inline void set_kernel_fs(void) { /* Set %fs for this CPU's PDA. Memory clobber is to create a barrier with respect to any PDA operations, so the compiler doesn't move any before here. */ asm volatile ("mov %0, %%fs" : : "r" (__KERNEL_PDA) : "memory"); }
cpu_init--->>init_gdt
pda = cpu_pda(cpu);
……
pack_descriptor((u32 *)&gdt[ GDT_ENTRY_PDA].a, (u32 *)&gdt[GDT_ENTRY_PDA].b, (unsigned long)pda, sizeof(*pda) - 1, 0x80 | DESCTYPE_S | 0x2, 0); /* present read-write data segment */ memset(pda, 0, sizeof(*pda)); pda->_pda = pda; pda->cpu_number = cpu; pda->pcurrent = idle;
也就是說,在CPU啟動之后,就為該CPU分配一個PDA結構,並讓該CPU的fs指向該結構的起始位置。在進入內核之后,SAVE_ALL寄存器會和更新內核代碼段一樣更新fs的值linux-2.6.21\arch\i386\kernel\entry.S
#define SAVE_ALL \
……
movl $(__USER_DS), %edx; \ movl %edx, %ds; \ movl %edx, %es; \ movl $(__KERNEL_PDA), %edx; \ movl %edx, %fs
五、多核CPU的啟動和編號分配
在intel手冊中說明了多核啟動有一定的協議MP initialization protocol,當系統中有多核存在時,只有一個主引導CPU(BSP BootStrap Processor)有效,其它的CPU處於Application Processor狀態。當引導完成之后,此時主CPU會為各個CPU分配編號並讓它們各自啟動。
在系統最開始啟動時,內核可以通過BIOS提供的信息來知道系統中共有多少個在線的CPU信息。內核中有大量acpi(Advanced Configuration and Power Interface (ACPI))相關的代碼,僅僅對於多核啟動來說,關鍵的代碼流程為
acpi_initialize_tables-->>acpi_os_get_root_pointer--->>acpi_find_rsdp
unsigned long __init acpi_find_rsdp(void) { unsigned long rsdp_phys = 0; if (efi_enabled) { if (efi.acpi20 != EFI_INVALID_TABLE_ADDR) return efi.acpi20; else if (efi.acpi != EFI_INVALID_TABLE_ADDR) return efi.acpi; } /* * Scan memory looking for the RSDP signature. First search EBDA (low * memory) paragraphs and then search upper memory (E0000-FFFFF). */ rsdp_phys = acpi_scan_rsdp(0, 0x400); if (!rsdp_phys) rsdp_phys = acpi_scan_rsdp(0xE0000, 0x20000); return rsdp_phys; }
static unsigned long __init acpi_scan_rsdp(unsigned long start, unsigned long length) { unsigned long offset = 0; unsigned long sig_len = sizeof("RSD PTR ") - 1; /* * Scan all 16-byte boundaries of the physical memory region for the * RSDP signature. */ for (offset = 0; offset < length; offset += 16) { if (strncmp((char *)(phys_to_virt(start) + offset), "RSD PTR ", sig_len)) continue; return (start + offset); } return 0; }
也就是從約定地址中搜索特定字段並進行校驗,如果配置了efi,則直接使用BIOS傳過來的結構即可。
六、根本的限制
其實從根本上看,如果每個CPU指向相同的代碼但是可以獲得不同的值,那么必須有一個CPU私有的內容來實現,即相同的代碼對不同的CPU來說是不同的。對於CPU來說,它私有的內容就是自己的寄存器組,每個CPU都有自己的寄存器組。對於386來說,最早使用的是TR寄存器,之后使用的是fs寄存器。
再看下其它的體系結構,PowerPC使用的是r1寄存器
static inline struct thread_info *current_thread_info(void) { register unsigned long sp asm("r1"); /* gcc4, at least, is smart enough to turn this into a single * rlwinm for ppc32 and clrrdi for ppc64 */ return (struct thread_info *)(sp & ~(THREAD_SIZE-1)); }
alpha體系結構使用的是$8寄存器
/* How to get the thread information struct from C. */ register struct thread_info *__current_thread_info __asm__("$8"); #define current_thread_info() __current_thread_info
這一點和很多編譯器支持的線程私有變量實現方式類似,如果有興趣的話可以看下gcc關於線程私有變量的實現,它本質上也是使用了不同線程使用不同的寄存器組這個事實來實現的該功能。
七、glibc及內核對於線程私有變量的實現
glibc-2.6\nptl\sysdeps\i386\tls.h
/* Code to initially initialize the thread pointer. This might need special attention since 'errno' is not yet available and if the operation can cause a failure 'errno' must not be touched. */ # define TLS_INIT_TP(thrdescr, secondcall) \ ({ void *_thrdescr = (thrdescr); \ tcbhead_t *_head = _thrdescr; \ union user_desc_init _segdescr; \ int _result; \ \ _head->tcb = _thrdescr; \ /* For now the thread descriptor is at the same address. */ \ _head->self = _thrdescr; \ /* New syscall handling support. */ \ INIT_SYSINFO; \ \ /* The 'entry_number' field. Let the kernel pick a value. */ \ if (secondcall) \ _segdescr.vals[0] = TLS_GET_GS () >> 3; \ else \ _segdescr.vals[0] = -1; \ /* The 'base_addr' field. Pointer to the TCB. */ \ _segdescr.vals[1] = (unsigned long int) _thrdescr; \ /* The 'limit' field. We use 4GB which is 0xfffff pages. */ \ _segdescr.vals[2] = 0xfffff; \ /* Collapsed value of the bitfield: \ .seg_32bit = 1 \ .contents = 0 \ .read_exec_only = 0 \ .limit_in_pages = 1 \ .seg_not_present = 0 \ .useable = 1 */ \ _segdescr.vals[3] = 0x51; \ \ /* Install the TLS. */ \ asm volatile (TLS_LOAD_EBX \ "int $0x80\n\t" \ TLS_LOAD_EBX \ : "=a" (_result), "=m" (_segdescr.desc.entry_number) \ : "0" (__NR_set_thread_area), \ TLS_EBX_ARG (&_segdescr.desc), "m" (_segdescr.desc)); \通過get_thread_area申請並安裝一個內核態的描述符,該描述符指向用戶態 _segdescr.desc地址,該描述符在的數值通過 _segdescr.desc.entry_number返回,因為用戶態不能操作內核的GDT表內容 \ if (_result == 0) \ /* We know the index in the GDT, now load the segment register. \ The use of the GDT is described by the value 3 in the lower \ three bits of the segment descriptor value. \ \ Note that we have to do this even if the numeric value of \ the descriptor does not change. Loading the segment register \ causes the segment information from the GDT to be loaded \ which is necessary since we have changed it. */ \ TLS_SET_GS (_segdescr.desc.entry_number * 8 + 3); \將內核返回的GDT索引賦值該gs寄存器。 \ _result == 0 ? NULL \ : "set_thread_area failed when setting up thread-local storage\n"; }) /* Return the address of the dtv for the current thread. */ # define THREAD_DTV() \ ({ struct pthread *__pd; \ THREAD_GETMEM (__pd, header.dtv); })


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM