进程(笔记)

进程、轻量级进程和线程

进程是程序执行的一个实例。从内核观点看,进程的目的就是担当分配系统资源的实体

尽管父子进程共享含有程序代码的页,但是各自有自有独立的数据拷贝,所以子进程对一个内存单元的修改对父进程是不可见的。

linux使用轻量级进程LWP对多线程应用程序提供更好的支持。

进程描述符

内核为了对每个进程所做的事情进行清楚的描述,用到了进程描述符,是task_struct类型的结构。内核还定义了task_t等同于task_struct

进程状态

进程描述符中的state字段描述了进程当前的状态, volatile long state;当前linux版本中,这些状态是互斥的,所以,只能设置一种状态。

可运行状态(TASK_RUNNING)

进程要么在CPU执行,要么准备执行。

可中断的等待状态(TASK_INTERRUPTIBLE)

进程被挂起,直到某个条件为真。可中断。

不可中断的等待状态(TASK_UNINTERRUPTIBLE)

很少用到,与可中断的等待状态类似,有个例外,把信号传递到睡眠进程不能改变它的状态。

暂停状态(TASK_STOPPED)

执行被暂停,当接收到SIGSTOP,SIGTSTP, SIGTTIN, SIGTTOU信号后,进入暂停。

跟踪状态(TASK_TRACED)

进程的执行被debugger程序暂停,当被另一个进程监控时,任何信号都可以把这个进程置于TASK_TRACED状态。

剩余两种状态,既可以放在state字段,也可以放在exit_state字段,只有当进程被终止时,状态才会变为这两种之一。

僵死状态(EXIT_ZOMBIE)

进程终止,但父进程没有调用wait4()或waitpid()返回有关死亡进程的信息。内核不会在wait()之前丢弃包含在死亡进程描述符中的数据。

僵死撤销状态(EXIT_DEAD)

最终状态,为了防止其他执行线程在同一个进程上也执行wait类系统调用,转为僵死撤销状态。

内核使用set_task_state和set_current_state宏,分别设置指定进程和当前执行进程的状态。里面通过set_mb确保编译程序或CPU控制单元不把赋值操作与其他指令混合。

标识一个进程

内核对进程的大部分引用都是通过进程描述符指针进行的。操作系统用户使用进程标识符PID来标识进程,存放在pid字段中。缺省情况下,PID的最大值是32767,PID_MAX_DEFAULT-1,管理员可以通过/proc/sys/kernel/pid_max改小,在64位体系结构中,最大可到4194303.

内核通过管理一个pidmap-array位图来表示当前已分配和闲置的PID号。

linux引入线程组的表示,一个线程组的所有线程使用和该线程组的领头线程相同的PID,存在tgid字段中。getpid返回的是当前进程的tgid值而不是pid

进程描述符处理

进程是动态实体。进程描述符是存放在动态内存中的。对每个进程,linux把两个不同的数据结构紧凑的存放在一个存储区域内:一个是内核态的进程堆栈,一个是小数据结构thread_info,线程描述符。当几乎没有可用的动态内存时,很难找到两个连续的页框,80x86体系结构在编译时,可以设置,使内核栈和线程描述符跨越一个单独的页框。

esp寄存器是CPU栈指针,一旦有数据写入堆栈,esp的值就递减,由于thread_info结构是52字节长,所以内核栈能扩展到8140字节。

C语言使用下列联合体表示一个进程的线程描述符和内核栈:

union thread_union {                                                             
    struct thread_info thread_info;                                              
    unsigned long stack[THREAD_SIZE/sizeof(long)];                               
};
内核使用alloc_thread_info和free_thread_info宏分配和释放存储这个内存区。

标识当前进程

这样thread_info和栈指针的紧密结合的好处是:很容易从esp的值获得当前CPU上运行的thread_info结构的地址,如果thread_union长度是8k,内核屏蔽掉esp的低13位就可以获得thread_info的基地址,如果是4k, 则屏蔽掉低12位。current_thread_info函数完成这个实现。

static inline struct thread_info *current_thread_info(void)                      
{                                                                                
    struct thread_info *ti;                                                      
    __asm__("andl %%esp,%0; ":"=r" (ti) : "0" (~(THREAD_SIZE - 1)));             
    return ti;                                                                   
} 
产生如下的汇编指令:

movl $0xffffe000,%ecx
andl %esp, %ecx
movl %ecx, p
进程最常用的是进程描述符,为了获得当前CPU的进程描述符,内核调用current宏,该宏定义为get_current函数:

static inline struct task_struct * get_current(void)                             
{                                                                                
    return current_thread_info()->task;                                          
}
因为task字段在thread_info结构中的偏移量为0,p就包含在CPU上运行进程的描述符指针。

用栈存放进程描述符的另一个优点体现在多处理器系统上。

双向链表

对于链表,linux内核定义了list_head数据结构,字段next和prev分别表示通用双向链表向前和向后的指针元素。 值得关注的是,list_head字段的指针中存放的是另一个list_head字段的地址,而不是含有list_head结构的整个数据结构。

struct list_head {                                                               
    struct list_head *next, *prev;                                               
};
新链表用LIST_HEAD(list_name)宏创建,这个变量作为新链表的占位符,是一个哑元素。如下:

#define LIST_HEAD_INIT(name) { &(name), &(name) }                                
                                                                                 
#define LIST_HEAD(name) \                                                        
    struct list_head name = LIST_HEAD_INIT(name)
linux2.6内核支持另一种双向链表,和list_head有着明显的区别,不是循环链表, 主要用于散列表。对散列表最重要的是空间而不是固定的时间内找到表中的最后一个元素。表头存放在hlist_head数据结构中,

struct hlist_head {                                                              
    struct hlist_node *first;                                                    
};
每个元素都是hlist_node类型的数据结构,

struct hlist_node {                                                              
    struct hlist_node *next, **pprev;                                            
}; 
next指针指向下一个元素,pprev指向前一个元素的next字段。因为不是循环的。所以第一个元素的pprev和最后一个元素的next字段都置为NULL。对这个链表操作的函数有hlist_add_head(), hlist_del(), hlist_empty(), hlist_entry(), hlist_for_each_entry()。

进程链表
双向链表的例子---进程链表。进程链表的头是init_task描述符,它是所谓的0进程或swapper进程的进程描述符。

struct task_struct init_task = INIT_TASK(init_task);
SET_LINKS和REMOVE_LINKS宏用于从进程链表插入和删除一个进程描述符,考虑了进程间父子关系。

#define REMOVE_LINKS(p) do {                    \                                
    if (thread_group_leader(p))             \                                    
        list_del_init(&(p)->tasks);         \                                    
    remove_parent(p);                   \                                        
    } while (0)                                                                  
                                                                                 
#define SET_LINKS(p) do {                   \                                    
    if (thread_group_leader(p))             \                                    
        list_add_tail(&(p)->tasks,&init_task.tasks);    \                        
    add_parent(p, (p)->parent);             \                                    
    } while (0)
for_each_process宏是扫描整个进程链表。

#define for_each_process(p) \                                                    
    for (p = &init_task ; (p = next_task(p)) != &init_task ; )
TASK_RUNNING状态的进程链表

linux2.6实现的运行队列有所不同,目的让调度程序能在固定的时间内选出“最佳”可运行进程,与队列中可运行的进程数无关。提高调度程序运行速度的诀窍是建立多个可运行进程链表,根据不同的进程优先权,建立不同的优先权运行队列,范围0到139.此外,多处理器系统中,每个CPU都有自己的运行队列。所有这些链表由一个单独的prio_array_t数据结构来实现。

struct prio_array {                                                                  
    unsigned int nr_active; /* 链表中进程描述符的数量 */                                                          
    unsigned long bitmap[BITMAP_SIZE]; /* 优先权位图,当且仅当某个优先权的进程链表不为空时设置相应的位标志。 */                                           
    struct list_head queue[MAX_PRIO]; /* 140个优先权队列的头结点 */                                               
};
enqueue_task(p,array)把进程描述符插入某个运行队列的链表。

static void enqueue_task(struct task_struct *p, prio_array_t *array)             
{                                                                                
    sched_info_queued(p);                                                        
    list_add_tail(&p->run_list, array->queue + p->prio);                         
    __set_bit(p->prio, array->bitmap);                                           
    array->nr_active++;                                                          
    p->array = array;                                                            
}
dequeue_task(p,array)从运行队列的链表中删除一个进程的描述符。

static void dequeue_task(struct task_struct *p, prio_array_t *array)             
{                                                                                
    array->nr_active--;                                                          
    list_del(&p->run_list);                                                        
    if (list_empty(array->queue + p->prio))                                      
        __clear_bit(p->prio, array->bitmap);                                     
}
进程间的关系

进程0和进程1是内核创建的,进程1(init)是所有 进程的祖先。

进程描述符中表示进程亲属关系的字段的描述:

real_parent:指向创建了P的进程的描述符,如果P的父进程不存在,就指向进程1.

parent:指向P的当前父进程,通常与real_parent一致,偶尔也不同。

children:链表的头部,所有元素都是P的子进程。

sibling:指向兄弟进程链表中的下一个元素或前一个元素,这些兄弟进程的父进程都是P。

进程之间还存在其他关系,如下字段描述非亲属关系:

group_leader:P所在进程组的领头进程的描述符指针。

signal->pgrp:P所在进程租的领头进程的PID。

tgid:P所在线程组的领头进程的PID

signal->session:P的登录会话领头进程的PID。

ptrace_children:链表的头,包含所有被debugger程序跟踪的P的子进程。

ptrace_list:指向跟踪进程其实际父进程链表的前一个和下一个元素。

pidhash表及链表

内核必须能从进程的PID导出对应的进程描述符指针。比如kill参数需要PID到处其对应的进程描述符。为了加速查找,引入了4个散列表,因为进程描述符包含了不同类型PID的字段。

hash表的类型字段名进程的PID
PIDTYPE_PIDpid进程的PID
PIDTYPE_TGIDtgid线程组领头进程的PID
PIDTYPE_PGIDpgrp进程组领头进程的PID
PIDTYPE_SIDsession会话领头进程的PID

内核初始化期间动态的为4个散列表分配空间,并存入pid_hash数组。

用pid_hashfn宏把PID转化为表索引,定义为:

#define pid_hashfn(nr) hash_long((unsigned long)nr, pidhash_shift)
static inline unsigned long hash_long(unsigned long val, unsigned int bits)      
{                                                                                
    unsigned long hash = val;                                                    
                                                                                 
#if BITS_PER_LONG == 64                                                          
    /*  Sigh, gcc can't optimise this alone like it does for 32 bits. */         
    unsigned long n = hash;                                                      
    n <<= 18;                                                                    
    hash -= n;                                                                   
    n <<= 33;                                                                    
    hash -= n;                                                                   
    n <<= 3;                                                                     
    hash += n;                                                                   
    n <<= 3;                                                                     
    hash -= n;                                                                   
    n <<= 4;                                                                     
    hash += n;                                                                   
    n <<= 2;                                                                         
    hash += n;                                                                   
#else                                                                            
    /* On some cpus multiply is faster, on others gcc will do shifts */          
    hash *= GOLDEN_RATIO_PRIME;                                                  
#endif                                                                           
                                                                                 
    /* High bits are more random, so use them. */                                
    return hash >> (BITS_PER_LONG - bits);                                       
}
变量pidhash_shift用来存放表索引的长度。 两个不同的PID散列到相同的表索引称为冲突(colliding)。

等待队列的操作

也可以用DEFINE_WAIT宏声明一个wait_queue_t类型的新变量。

#define DEFINE_WAIT(name)                       \                                   
    wait_queue_t name = {                       \                                   
        .task       = current,              \                                       
        .func       = autoremove_wake_function,     \                               
        .task_list  = { .next = &(name).task_list,  \                               
                    .prev = &(name).task_list,  \                                   
                },                  \                                               
    }

最后通过init_waitqueue_func_entry()函数来自定义唤醒函数。

static inline void init_waitqueue_func_entry(wait_queue_t *q,                       
                    wait_queue_func_t func)                                      
{                                                                                   
    q->flags = 0;                                                                   
    q->task = NULL;                                                                 
    q->func = func;                                                                 
}
一旦定义了一个元素,必须把他插入等待队列。add_wait_queue()函数把一个非互斥的进程插入等待队列链表的第一个位置。add_wait_queue_exclusive()函数把一个互斥的进程插入等待队列链表的最后一个位置。remove_wait_queue从等待队列中删除一个进程。waitqueue_active()函数检查一个给定的等待队列是否为空。

要等待特定条件的进程可以调用如下函数:

  • sleep_on
    void fastcall __sched sleep_on(wait_queue_head_t *q)                             
    {                                                                                
        SLEEP_ON_VAR                                                                 
                                                                                     
        current->state = TASK_UNINTERRUPTIBLE;                                       
                                                                                     
        SLEEP_ON_HEAD                                                                
        schedule();                                                                  
        SLEEP_ON_TAIL                                                                
    }
    #define SLEEP_ON_HEAD                   \                                        
        spin_lock_irqsave(&q->lock,flags);      \                                    
        __add_wait_queue(q, &wait);         \                                        
        spin_unlock(&q->lock);                                                       
                                                                                     
    #define SLEEP_ON_TAIL                   \                                        
        spin_lock_irq(&q->lock);            \                                        
        __remove_wait_queue(q, &wait);          \                                    
        spin_unlock_irqrestore(&q->lock, flags);

  • interruptible_sleep_on与sleep_on一样,只是设置状态为TASK_INTERUPTIBLE,接收一个信号就可以唤醒当前进程。
  • sleep_on_timeout和interuptible_sleep_on_timeout()与前面类似,允许调用定义在一个时间间隔之后,进程被内核唤醒。所以这里调用的是schedule_timeout。
  • prepare_to_wait, prepare_to_wait_exclusive(), finish_wait()提供另一种途径,使当前进程在一个等待队列中睡眠。
    void fastcall                                                                    
    prepare_to_wait(wait_queue_head_t *q, wait_queue_t *wait, int state)             
    {                                                                                
        unsigned long flags;                                                         
                                                                                     
        wait->flags &= ~WQ_FLAG_EXCLUSIVE;                                           
        spin_lock_irqsave(&q->lock, flags);                                          
        if (list_empty(&wait->task_list))                                            
            __add_wait_queue(q, wait);                                               
        /*                                                                           
         * don't alter the task state if this is just going to                       
         * queue an async wait queue callback                                        
         */                                                                          
        if (is_sync_wait(wait))                                                      
            set_current_state(state);                                                
        spin_unlock_irqrestore(&q->lock, flags);                                     
    }
    用第三个参数设置进程的状态。进程一旦被唤醒,执行finish_wait,把状态设置为TASK_RUNNING,从等待队列中删除。
    void fastcall finish_wait(wait_queue_head_t *q, wait_queue_t *wait)              
    {                                                                                
        unsigned long flags;                                                         
                                                                                     
        __set_current_state(TASK_RUNNING);                                                                                                               
        if (!list_empty_careful(&wait->task_list)) {                                 
            spin_lock_irqsave(&q->lock, flags);                                      
            list_del_init(&wait->task_list);                                         
            spin_unlock_irqrestore(&q->lock, flags);                                 
        }                                                                            
    }

  • wait_event和wait_event_interuptible宏使他们的调用进程在等待队列上睡眠。都是调用的__wait_event函数,如下:
    #define __wait_event(wq, condition)                     \                        
    do {                                    \                                        
        DEFINE_WAIT(__wait);                        \                                
                                        \                                            
        for (;;) {                          \                                        
            prepare_to_wait(&wq, &__wait, TASK_UNINTERRUPTIBLE);    \                
            if (condition)                      \                                    
                break;                      \                                        
            schedule();                     \                                        
        }                               \                                            
        finish_wait(&wq, &__wait);                  \                                
    } while (0)

内核通过下面任何一个宏唤醒等待队列中的进程并设置为TASK_RUNNING状态。wake_up, wake_up_nr, wake_up_all, wake_up_interruptible, wake_up_interruptible_nr, wake_up_interruptible_all, wake_up_interruptible_sync, wake_up_locked。

进程资源限制

每一组进程都有一组相关的资源限制,存放在current->signal->rlim字段,该字段类型为rlimit结构的数组。

struct rlimit {                                                                     
    unsigned long   rlim_cur;                                                       
    unsigned long   rlim_max;                                                       
};
RLIMIT_AS:进程地址空间的最大数。

RLIMIT_CORE:内核信息转储文件的大小。进程异常终止时,在当前目录下创建内存信息转储文件之前要检查这个值,如果为0,则不创建。

RLIMIT_CPU:进程使用CPU的最长时间。

RLIMIT_DATA:堆大小的最大值,在扩充进程的堆之前,要检查这个值。

RLIMIT_FSIZE:文件大小的最大值。

RLIMIT_LOCKS:文件锁的最大值。

RLIMIT_MEMLOCK:非交换内存的最大值。当进程通过mlock和mlockall()系统调用锁定一个页框时,检查这个值。

RLIMIT_MSQUEUE:POSIX消息队列中的最大字节数。

RLIMIT_NOFILE:打开文件描述符的最大数。

RLIMIT_NPROC:用户能拥有的的进程最大数。

RLIMIT_RSS:进程所拥有的页框最大数。

RLIMIT_SIGPENDING:进程挂起信号的最大数。

RLIMIT_STACK:栈大小的最大值。

利用getrlimit和setrlimit系统调用,可以取得和设置rlim_cur。只有root用户才可以改变rlim_max值,或改变rlim_cur大于rlim_max的值。

进程切换

内核挂起正在CPU运行的进程,并恢复以前挂起的某个进程,称为进程切换(process switch)、任务切换(task switch)、上下文切换(context switch)。

硬件上下文

进程恢复执行前必须装入寄存器的一组数据称为硬件上下文。在linux中,进程硬件上下文的一部分放在TSS段,剩余部分放在内核堆栈中。因为进程切换经常发生,因此减少保存和减少硬件上下文所花费的时间是非常重要的。

linux2.6使用软件执行进程切换,因为:

  • 通过一组mov指令逐步进行切换。这样能控制所装入数据的合法性。旧的far jmp不能进行这类检查。
  • 新旧方法时间大致相同,尽管当前切换代码有该机的余地,但不能对硬件优化。
进程切换只发生在内核态。执行进程切换之前,用户态进程使用的所有寄存器内容都已保存在内核态堆栈上。

任务状态段

80x86体系结构包括了一个特殊的段类型,任务状态段(Task State Segment,TSS)linux强制为系统每个CPU创建一个TSS,有两个理由:

  • 当80x86的一个CPU从用户态切换到内核态时,就从TSS中获取内核态堆栈的地址
  • 用户态进程试图通过in或out指令访问一个I/O端口时,CPU需要访问存放在TSS中的I/O许可位全图。主要检查:



  1. 检查eflags寄存器中的2位IOPL字段。如果字段为3,控制单元执行I/O指令。
  2. 访问tr寄存器以确定当前的TSS和相应的I/O许可位全图。
  3. 检查I/O指令中指定的I/O端口在I/O许可位全图中对应的位。



tss_struct结构描述了TSS格式。init_tss数组为系统每个不同的CPU存放一个TSS。TSS反映了CPU上当前进程的特权级。

thread字段

每个进程描述符包含一个类型那个为thread_struct的thread字段,只要进程被切换出去,内核就把硬件上下文保存在这个结构中。这个结构体包含大部分CPU寄存器,但不包括诸如eax、ebx等通用寄存器,他们的值保存在内核堆栈中。

执行进程切换

进程切换可能只发生在特殊的点,schedule()函数。本质上说,每个进程切换由两步组成:

  1. 切换页全局目录以安装一个新的地址空间。
  2. 切换内核态堆栈和硬件上下文。
switch_to宏

进程切换的第二部由这个宏执行。这是内核中与硬件关系最密切的例程之一。

#define switch_to(prev,next,last) do {					\
	unsigned long esi,edi;						\
	asm volatile("pushfl\n\t"					\
		     "pushl %%ebp\n\t"					\
		     "movl %%esp,%0\n\t"	/* save ESP */		\
		     "movl %5,%%esp\n\t"	/* restore ESP */	\
		     "movl $1f,%1\n\t"		/* save EIP */		\
		     "pushl %6\n\t"		/* restore EIP */	\
		     "jmp __switch_to\n"				\
		     "1:\t"						\
		     "popl %%ebp\n\t"					\
		     "popfl"						\
		     :"=m" (prev->thread.esp),"=m" (prev->thread.eip),	\
		      "=a" (last),"=S" (esi),"=D" (edi)			\
		     :"m" (next->thread.esp),"m" (next->thread.eip),	\
		      "2" (prev), "d" (next));				\
} while (0)

prev,next分别表示被替换进程和新进程描述符的地址在内存中的位置。

在任何进程切换中,涉及到三个进程而不是两个。

__switch_to函数

此函数大多数开始于switch_to宏的进程切换。

struct task_struct fastcall * __switch_to(struct task_struct *prev_p, struct task_struct *next_p)
{
	struct thread_struct *prev = &prev_p->thread,
				 *next = &next_p->thread;
        /* 获得本地CPU的下标,执行代码的CPU,从当前进程的thread_info结构的cpu字段获得下标并将它保存在cpu局部变量 */
	int cpu = smp_processor_id();
	struct tss_struct *tss = &per_cpu(init_tss, cpu);

	/* never put a printk in __switch_to... printk() calls wake_up*() indirectly */
        /* 有选择的保存prev_p进程的FPU、MMX及XMM寄存器的内容 */
	__unlazy_fpu(prev_p);

	/*
	 * Reload esp0, LDT and the page table pointer:
         * 把next->thread.esp0装入对应于本地CPU的TSS的esp0字段;
	 */
	load_esp0(tss, next);

	/*
	 * Load the per-thread Thread-Local Storage descriptor.
         * 把next_p进程使用的线程局部存储(TLS)段装入本地CPU的全局描述符表。
	 */
	load_TLS(next, cpu);

	/*
	 * Save away %fs and %gs. No need to save %es and %ds, as
	 * those are always kernel segments while inside the kernel.
         * 把fs和gs段寄存器的内容分别存放在prev->thread的fs和gs中
         * 对应的汇编语言指令是
	 */
	asm volatile("movl %%fs,%0":"=m" (*(int *)&prev->fs));
	asm volatile("movl %%gs,%0":"=m" (*(int *)&prev->gs));

	/*
	 * Restore %fs and %gs if needed.
         * 如果fs或gs段寄存器已经被prev_p或next_p进程中的任意一个使用
         * 则将next_p进程的thread_struct描述符中保存的值装入这些寄存器
	 */
	if (unlikely(prev->fs | prev->gs | next->fs | next->gs)) {
		loadsegment(fs, next->fs);
		loadsegment(gs, next->gs);
	}

	/*
	 * Now maybe reload the debug registers
	 */
	if (unlikely(next->debugreg[7])) {
		loaddebug(next, 0);
		loaddebug(next, 1);
		loaddebug(next, 2);
		loaddebug(next, 3);
		/* no 4 and 5 */
		loaddebug(next, 6);
		loaddebug(next, 7);
	}
        /* 如果有必要,更新TSS中的I/O位图 */
	if (unlikely(prev->io_bitmap_ptr || next->io_bitmap_ptr))
		handle_io_bitmap(next, tss);
        /* prev_p参数在edi中被拷贝到eax,缺省情况下任何C函数的返回值被传递给eax寄存器 */
	return prev_p;
}

这个函数不同于其他函数,从eax和edx取参数prev_p,next_p,而不是从栈中取参数。所以在include/asm-i386/system.h中声明如下:

extern struct task_struct * FASTCALL(__switch_to(struct task_struct *prev, struct task_struct *next));
#define FASTCALL(x)	x __attribute__((regparm(3)))

这里内核利用__attribute__和regparm关键字,强迫函数从寄存器取它的参数。这两个是C语言的非标准的扩展名,由gcc编译程序实现。

保存和加载FPU、MMX及XMM寄存器

从intel 80486DX开始, 算术浮点单元(floating-point unit, FPU)被集成到CPU中。最近的Pentium模型中,Intel在它的微处理器中引入了新的汇编指令集,MMX,用来加速多媒体应用程序的执行,作用于FPU的浮点寄存器。

union i387_union {
	struct i387_fsave_struct	fsave;
	struct i387_fxsave_struct	fxsave;
	struct i387_soft_struct soft;
};
这个数据结构装入了处理FPU、MMX和XMM寄存器。进程描述符包含两个附加的标志:

  • 包含在thread_info描述符的status字段中的TS_USEDFPU标志。表示进程在当前执行的过程中,是否使用过FPU、MMX和XMM寄存器。
  • 包含在task_struct描述符的flags字段中的PF_USED_MATH标志。表示thread.i387字段的内容是否有意义。
保存FPU寄存器

__switch_to函数把替换进程prev的描述符参数传递给我__unlazy_fpu宏,

#define __unlazy_fpu( tsk ) do { \
	if ((tsk)->thread_info->status & TS_USEDFPU) \
		save_init_fpu( tsk ); \
} while (0)
检查TS_USEDFPU标志,如果被设置,说明prev在这次执行中使用过FPU、MMX、SSE或SSE2指令,必须保存相关上下文。

装载FPU寄存器

创建进程

传统的unix系统以统一方式对待所有进程,子进程复制父进程所有的资源,这种方法效率低。现在unix内核通过引入三种不同的机制解决:

  • 写时复制技术,允许父子进程读相同的物理页,只要其中一个试图写一个物理页,内核就把这个页的内容拷贝到新的物理页,并把这个新的物理页分配给正在写的进程。
  • 轻量级进程允许父子进程共享没进程在内核的很多数据结构。
  • vfork()系统调用创建的进程能共享其父进程的内存地址空间

clone、fork和vfrok系统调用

linux中,轻量级进程由名为clone的函数创建,这个函数的参数:

fn:指定一个由新进程执行的函数。

arg:指向传递给fn函数的数据。

flags:

child_stack:表示把用户态堆栈指针赋给子进程esp寄存器。

tls:表示线程局部存储段(TLS)数据结构的地址。

ptid:表示父进程的用户态变量地址。

ctid:表示新轻量级进程的用户态变量地址。

fork系统调用在linux中是clone实现的,其中clone的flags参数指定为SIGCHLD信号及所有清0的clone标志,vfork也是clone实现的,其中flags标志为SIGCHLD信号和CLONE_VM及CLONE_VFORK标志。

do_fork函数

long do_fork(unsigned long clone_flags, //与clone的flags相同
	      unsigned long stack_start, //与clone的child_stack相同
	      struct pt_regs *regs, //指向通用寄存器的指针,通用寄存器的值是在从用户态切换到内核态时被保存到内核态堆栈中的
	      unsigned long stack_size, //未使用
	      int __user *parent_tidptr,// 最后两个与clone的ptid和ctid相对应
	      int __user *child_tidptr)
{
	struct task_struct *p;
	int trace = 0;
	long pid = alloc_pidmap(); //通过查找pidmap_array位图,为子进程分配新的PID

	if (pid < 0)
		return -EAGAIN;
	if (unlikely(current->ptrace)) { //检查父进程的ptrace字段,如果不等于0,说明由另一个进程正在跟踪父进程
		trace = fork_traceflag (clone_flags);
		if (trace)
			clone_flags |= CLONE_PTRACE;
	}
        /* 调用copy_process复制进程描述符,如果所有必须的资源都是可用的,该函数返回刚创建的task_struct描述符的地址 */
	p = copy_process(clone_flags, stack_start, regs, stack_size, parent_tidptr, child_tidptr, pid);
	/*
	 * Do this prior waking up the new thread - the thread pointer
	 * might get invalid after that point, if the thread exits quickly.
	 */
	if (!IS_ERR(p)) {
		struct completion vfork;

		if (clone_flags & CLONE_VFORK) {
			p->vfork_done = &vfork;
			init_completion(&vfork);
		}
                /* 如果设置了CLONE_STOPPED标志或者必须跟踪子进程, 那么子进程状态被设置为TASK_STOPPED,并为子进程增加挂起的SIGSTOP信号 */
		if ((p->ptrace & PT_PTRACED) || (clone_flags & CLONE_STOPPED)) {
			/*
			 * We'll start up with an immediate SIGSTOP.
			 */
			sigaddset(&p->pending.signal, SIGSTOP);
			set_tsk_thread_flag(p, TIF_SIGPENDING);
		}
                /*如果没有设置CLONE_STOPPED标志, 则调用wake_up_new_task函数*/
		if (!(clone_flags & CLONE_STOPPED))
			wake_up_new_task(p, clone_flags);
		else
			p->state = TASK_STOPPED;
                /*如果父进程被跟踪,则把子进程PID存入current的ptrace_message字段并调用ptrace_notify,停止当前进程,并向其父进程发送SIGCHLD信号 */
		if (unlikely (trace)) {
			current->ptrace_message = pid;
			ptrace_notify ((trace << 8) | SIGTRAP);
		}
                /* 如果设置了CLONE_VFORK标志,则把父进程插入等待队列,并挂其父进程直到子进程释放自己的内存地址空间 */
		if (clone_flags & CLONE_VFORK) {
			wait_for_completion(&vfork);
			if (unlikely (current->ptrace & PT_TRACE_VFORK_DONE))
				ptrace_notify ((PTRACE_EVENT_VFORK_DONE << 8) | SIGTRAP);
		}
	} else {
		free_pidmap(pid);
		pid = PTR_ERR(p);
	}
	return pid;
}

copy_process函数

这个函数创建进程描述符以及子进程所需要的所有其他数据结构。参数与do_fork函数相同,外加一个PID。步骤如下:

  1. 检查clone_flags所传递标志的一致性,尤其是,CLONE_NEWNS和CLONE_FS标志都设置,返回错误码;CLONE_THREAD标志被设置,但CLONE_SIGHAND标志被清0;CLONE_SIGHAND标志被设置,但CLONE_VM被清0。
  2. 通过调用security_task_create以及后面的security_task_alloc执行所有附加的安全检查。这是linux2.6提供扩展安全性的钩子函数
  3. 调用dup_task_struct为子进程获取进程描述符。
  4. 检查当前限制的进程数是否小于用户所拥有的进程数。
  5. 递增user_struct结构的使用计数器和用户拥有的进程的计数器。
  6. 检查进程中的数量是否超过max_threads变量的值。
  7. 如果实现新进程的执行域和可执行格式的内核函数都包含在内核模块中,则递增他们的使用计数器。
  8. 设置与进程状态相关的关键字段:大内核锁计数器task->lock_depth初始化为-1;tsk->dis_exec初始化为0;更新父进程复制到tsk->flags字段中的一些标志。
  9. 把新进程的PID存入tsk->pid字段。
  10. 如果clone_flags设置CONE_PARENT_SETTID,就把子进程的PID复制到参数parent_tidptr指向的用户态变量中。
  11. 初始化子进程描述符中的list_head数据结构和自旋锁。
  12. 调用copy_semundo,copy_files,copy_fs,copy_sighand,copy_signal,copy_mm和copy_namespace来创建新的数据结构,并把父进程相关的值复制到新的数据结构。
  13. 调用copy_thread
  14. 如果clone_flags参数的值被置为CLONE_CHILD_SETTID或CLONE_CHILD_CLEARTID,就把child_tidptr参数的值分别复制到tsk->set_chid_tid或tsk->clear_child_tid字段。
  15. 清除子进程thread_info结构的TIF_SYSCALL_TRACE标志。
  16. 用clone_flags参数低位的信号数字编码初始化tsk->exit_signal字段。
  17. 调用sched_fork()完成对新进程数据结构的初始化
  18. 把新进程的thread_info结构的cpu字段设置为smp_processor_id所返回的CPU号。
  19. 初始化表示亲子关系的字段。
  20. 如果不需要跟踪子进程,就把tsk->ptrace字段置为0.
  21. 执行SET_LINKS宏,把新进程描述符插入进程链表
  22. 如果子进程必须被跟踪,就把current->parent复制给tsk->parent,并把子进程插入调试程序的跟踪链表中。
  23. 调用attach_pid把新进程的PID插入pidhash散列表
  24. 如果子进程是线程组的领头进程,把tsk->tgid初值置为tsk->pid;把tsk->group_lead初值置为tsk;调用三次attach_pid,把子进程插入PIDTYPE_TGID,PIDTYPE_PGID,PIDTYPE_SID类型的PID散列表。
  25. 否则,把tsk->tgid的值置为tsk->current->tgid,tsk->group_leader置为current->group_leader,调用attach_pid,把子进程插入PIDTYPE_TGID类型的散列表。
  26. 递增nr_threads变量的值
  27. 递增total_forks变量以记录被创建进程的数量
  28. 返回子进程描述符指针tsk。

内核线程

因为一些系统进程只运行在内核态,所以现代操作系统把他们的函数委托给内核线程,内核线程在以下几个方面不同于普通进程

  • 内核线程只运行在内核态,普通进程可以运行在内核态和用户态。
  • 内核线程只运行在内核态,只使用PAGE_OFFSET的线性地址。
创建一个内核线程

kernel_thread函数创建一个新的内核线程。

int kernel_thread(int (*fn)(void *), void * arg, unsigned long flags)
{
	struct pt_regs regs;

	memset(®s, 0, sizeof(regs));

	regs.ebx = (unsigned long) fn;
	regs.edx = (unsigned long) arg;

	regs.xds = __USER_DS;
	regs.xes = __USER_DS;
	regs.orig_eax = -1;
	regs.eip = (unsigned long) kernel_thread_helper;
	regs.xcs = __KERNEL_CS;
	regs.eflags = X86_EFLAGS_IF | X86_EFLAGS_SF | X86_EFLAGS_PF | 0x2;

	/* Ok, create the new process.. */
	return do_fork(flags | CLONE_VM | CLONE_UNTRACED, 0, ®s, 0, NULL, NULL);
}
do_fork调用时CLONE_VM标志避免复制调用程序的页表,由于新内核线程无论如何都不会访问访问用户态的地址空间,所以复制会造成时间和空间的浪费。CLONE_UNTRACED标志保证不会有任何进程跟踪新内核线程,即使调用程序被跟踪。

进程0

所有进程的祖先叫做0,idle进程,或因为历史原因叫做swapper进程。它是linux的初始化阶段从无到有创建的内核线程。这个祖先进程使用静态分配的数据结构,所有其他的进程的数据结构都是动态分配的

struct task_struct init_task = INIT_TASK(init_task);
init_task由INIT_TASK宏完成对他的初始化。

init_thread_union变量中的thread_info描述符和内核堆栈,由INIT_THREAD_INFO宏初始化。

union thread_union init_thread_union 
	__attribute__((__section__(".data.init_task"))) =
		{ INIT_THREAD_INFO(init_task) };
进程描述符指向的下列表:init_mm,init_fs,init_files,init_signals,init_sighand,分别由INIT_MM,INIT_FS,INIT_FILES,INIT_SIGNALS,INIT_SIGHAND初始化。

static struct fs_struct init_fs = INIT_FS;
static struct files_struct init_files = INIT_FILES;
static struct signal_struct init_signals = INIT_SIGNALS(init_signals);
static struct sighand_struct init_sighand = INIT_SIGHAND(init_sighand);
struct mm_struct init_mm = INIT_MM(init_mm);
start_kernel()函数初始化内核需要的所有数据结构,激活中断,创建进程1的内核线程(一般叫做init进程)。

在最后调用rest_init函数里开始init进程。

static void noinline rest_init(void)
	__releases(kernel_lock)
{
	kernel_thread(init, NULL, CLONE_FS | CLONE_SIGHAND);
	numa_default_policy();
	unlock_kernel();
	preempt_enable_no_resched();
	cpu_idle();
} 
创建init进程后,进程0执行cpu_idle函数,该函数本质上是在开中断的情况下重复执行hlt汇编语言指令。只有没有其他进程处于TASK_RUNNING状态,调度程序才选择进程0.

在多处理器系统中,每个CPU都有一个进程0,计算机的BIOS就启动某一个CPU同时禁用其他CPU,运行在CPU0上的swapper进程初始化内核数据结构,然后激活其他的CPU,并通过copy_process函数创建另外的swapper进程。把0传递给新创建的swapper进程作为他们新的PID。

进程1

init一次完成内核初始化,init()调用execve系统调用装入可执行程序init,结果init内核线程变为一个普通进程。

系统关闭之前,init一直存活,它创建和监控在操作系统外层执行的所有进程的活动。

其他内核线程

keventd:也称为事件,执行keventd_wq工作队列。

kapmd:处理与高级电源管理相关的事情

kswapd:执行内存回收

pdflush:刷新“脏”缓冲区中的内容到磁盘以回收内存。

kblockd:周期性激活块设备驱动程序

ksoftirqd:执行tasklet,每个CPU都有这样的例程。

撤销进程

进程终止

有两个终止用户态应用的系统调用:

  • exti_group系统调用,终止整个线程组,do_group_exit时实现这个的内核函数。
  • exit()系统调用,终止某一个线程,do_exit时实现这个系统调用的内核函数。
NORET_TYPE void
do_group_exit(int exit_code)
{
	BUG_ON(exit_code & 0x80); /* core dumps don't get here */

	if (current->signal->flags & SIGNAL_GROUP_EXIT)//检查这个标志,不为0的话,说明内核已经开始为线程执行退出的过程,存放作为退出码,跳到do_exit
		exit_code = current->signal->group_exit_code;
	else if (!thread_group_empty(current)) {
		struct signal_struct *const sig = current->signal;
		struct sighand_struct *const sighand = current->sighand;
		read_lock(&tasklist_lock);
		spin_lock_irq(&sighand->siglock);
		if (sig->flags & SIGNAL_GROUP_EXIT)
			/* Another thread got here before we took the lock.  */
			exit_code = sig->group_exit_code;
		else {
			sig->flags = SIGNAL_GROUP_EXIT;
			sig->group_exit_code = exit_code;
			zap_other_threads(current);//杀死current线程组中的其他进程,向表中所有不同于current的进程发送SIGKILL信号
		}
		spin_unlock_irq(&sighand->siglock);
		read_unlock(&tasklist_lock);
	}

	do_exit(exit_code);
	/* NOTREACHED */
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值