(1)在linux中一个进程是处于执行中的程序,它等于:执行代码+资源(打开文件、挂起信号、内核内部数据、处理器状态、地址空间及一个或者多个线程、数据);(2)内核调度的对象是线程,不是进程,内核对两者不区分,但是两者都通过同一结构体来实现(task_struct);(3)线程和进程去别在于前者可以共享虚拟内存,但拥有各自的虚拟处理器。
一、进程描述符+任务结构(1)进程描述符
内核把进程
存放在叫做任务队列(task list)的
双向循环链表中。链表中的每一项都是类型
task_struct,称为
进程描述符的结构,该结构定义在
中,称为进程描述符的结构,该结构在32位机器上大约有1.7K字节。2.6内核通过slab分配器动态生成task_struct,各个进程的task_struct存放在他们内核栈的尾端。
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;
int lock_depth; /* BKL lock depth */
#ifdef CONFIG_SMP #ifdef __ARCH_WANT_UNLOCKED_CTXSW int oncpu; ...... };
|
内核通过唯一的标识值来标识(PID)每个进程,PID的最大值默认设置为32767(short int短整型的最大值)。如果需要更多进程,系统管理员可以通过修改/proc/sys/kernel/pid_max来提高上限,这里还有一个current指针来指向当前正在执行的进程描述符(相当于C++中的this指针)。
注:内核通过task_struct来存放进程,用PID来标识。
(2)实际表示
为了方便于栈指针的查找,在内核中使用了struct thread_info的结构体,该结构体在,如下:
struct thread_info { struct task_struct *task; /* main task structure */ struct exec_domain *exec_domain; /* execution domain */ unsigned long flags; /* low level flags */ unsigned long status; /* thread-synchronous flags */ __u32 cpu; /* current CPU */ int preempt_count; /* 0 => preemptable, <0 => BUG */ mm_segment_t addr_limit;
|
(3)进程可以分为一下五种状态:
- TASK_RUNNING(运行) — 进程是可执行的;它或者正在执行,或者在运行队列中等待执行。
- TASK_INTERRUPTIBLE(可中断) — 进程正在睡眠(也就是说它被阻塞),等待某些条件的达成。一旦这些条件达成,内核就会把进程状态设置为运行。处于此状态的进程也会因为收到信号而被提前唤醒并投入运行。
- TASK_UNINTERRUPTIBLE(不可中断) — 除了不会因为接收到信号而被唤醒从而投入运行,这个状态与可中断状态相同。这个状态通常在进程必须在等待时不受干扰或等待事件很快发生时出现。
- TASK_ZOMBIE(僵死) — 该进程已经结束了,但是其父进程还没有调用wait4()系统调用。为了父进程能够获知它的消息,子进程的进程描述符仍然被保留着。
- TASK_STOP PED(停止) — 进程停止执行。通常这种状态发生在接收到SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU等信号的时候。此外,在调试期间接收到任何信号,都会使进程进入这种状态。
同时,进程的状态可以通过如下函数来设置:
set_task_state(task,state);
等价于
task->state=state;
(4)进程家族树
进程的第一个进程是init进程,通过它作为父亲向下的每个进程有0个或多个子进程,这样就构成了一个进程家族树,我们可以在用户空间通过ps命令来查看,而内核中通过以下方式访问子进程:
struct task_struct *task ; struct list_head *list ; list_for_each (list , ¤t->children ) { task = list_entry (list , struct task_struct , slibing ) ; }
|
获取父进程:
struct task_struct *my_parent = current->parent ;
|
二、进程的创建
进程的创建其实从总的来说和简单,就是通过fork()和exec()函数来实现,前者实现与父进程的拷贝,后者来执行,就这么简单,但是从细节来分析就相当的麻烦了。
(1)写时拷贝(COW)
写时拷贝其实很好理解,就是在进程创建是父子进程共享源于父进程的一些数据,为了让系统减少繁忙,系统可以推迟甚至免除拷贝数据,而等到子进程需要写时才将数据拷贝给它,如果只读的话就没有必要了。
(2)fork() and vfork()
fork()过程:
- 调用dup_task_struct()创建内核栈、thread_info和task_struct
- 检查进程数目有没有超出资源限制
- 使父子进程区别开,task_sturct中的成员设置为0或者初始值
- 子进程状态设置为TASK_INTERRUPTIBLE(可中断)
- copy_proccess()函数调用copy_flags()更新task_struct中的flags,PF—SUPERPRIV清零,表明exec()函数还没有被调用的PF_FORKNOEXEC标志被清零。
- 调用get_pid()获取有效的PID
- 根据clone()的参数标志,copy_process()拷贝或共享父进程的资源
- 父子进程平分生于的时间片
- copy_process()返回一个子进程的指针
- 执行exec()
vfork()系统调用和fork()的功能相同,除了不拷贝父进程的页表项。子进程作为父进程的一个单独的线程在它的地址空间里运行,父进程被阻塞,直到子进程退出或执行exec()。子进程不能向地址空间写入。但由于其设计并不优良所以最好让它逐渐淡出。
三、线程的实现
前面提到:内核调度的对象是线程,不是进程,内核对两者不区分,但是两者都通过同一结构体来实现(task_struct),即是linux把线程当作进程来实现,这些实现只需要在clone()参数传递作变化:
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0 ) ;
这些标志定义在中,细节如下:
- CLONE_CLEARTID — 清除TID
- CLONE_DETACHED — 父进程不需要子进程退出时发送SIGCHLD
- CLONE_FILES — 父子进程共享打开的文件
- CLONE_FS — 父子进程共享文件系统信息
- CLONE_IDLETASK — 将PID设置为0(只供idle进程使用)
- CLONE_NEWNS — 为子进程创建新的命名空间
- CLONE_PARENT — 指定子进程与父进程拥有同一个父进程
- CLONE_PTRACE — 继续调试子进程
- CLONE_SETTID — 将TID回写至用户空间
- CLONE_SETTLS — 为子进程创建新的TLS
- CLONE_SIGHAND — 父子进程共享信号处理函数
- CLONE_SYSVSEM — 父子进程共享System V SEM_UNDO语义
- CLONE_THREAD — 父子进程放入相同的线程组
- CLONE_VFORK — 调用vfork(),所以父进程准备睡眠等待子进程将其唤醒
- CLONE_VM — 父子进程共享地址空间
- CLONE_KERNEL — 定义了内核线程常用到的参数标志:CLONE_FS、CLONE_FILES、CLONE_SIGHAND。
四、进程的终结
我们编程的时候只需要调用exit()或者return来结束,不过实际在内核的结束的确是麻烦,不过总的还是分为释放进程资源和进程描述符。
释放进程资源:
- 将task_struct中的flags设置为PR_EXITING
- 调用del_timer_sync()删除内核定时器,确保没有定时器在排队
- 调用_exti_mm()放弃占用的mm_struct,如果没有其他的进程使用就彻底释放
- 调用exit_sem(),如果有IPC信号就离开队列
- 调用_exit_files(),_exit_fs(),exit_namespace(),exit_sighand()
,释放文件描述符、文件系统数据、进程名空间、信号处理函数
- 把task_strcut的exit_code设置为exit()提供的代码中,供父进程检索
- 调用exit_nofify()向父进程发送信号,把子进程设置为TASK_ZOMBIE
- do_exit()调用schedule()切换到其他进程
这里子进程为TASK_ZOMBIE,但是进程描述符还在系统中保留,这里必须删除
释放进程描述符:
- 调用free_uid()减少进程拥有者的计数
- release_task()调用unhash_process()从pidhash上删除该进程。同时从task_list中删除
- 这个删除进程的子进程父进程重设
- release_task()调用put_task_struc()释放内核栈和thread_info,task_struct占用的slab高速缓存
阅读(985) | 评论(0) | 转发(0) |