Linux进程基本知识
2014-12-07 21:39
302 查看
1.基本概念
进程是资源管理的最小单位,而线程是程序执行的最小单位。在操作系统设计上,从进程演化出线程的主要目的:更好的支持SMP以及减小(进程/线程)上下文切换的开销。
针对线程模型的两大意义,分别开发出了核心级线程(For SMP)和用户级线程(For 上线文切换)两类线程模型,其分类标准主要是线程的调度者是核内还是核外。很多系统都着重于开发混合模型,而Linux没有这种打算。
Linux内核只提供了轻量进程的支持(严格来讲,Linux中没有线程这一概念),尽管其限制了更高效的线程模型的实现,但Linux侧重于优化进程调度开销,一定程度上弥补了这一缺陷。目前最流行的线程机制LinuxThreads所采用的就是线程-进程"一对一"模型,调度交给核心,而在用户级实现一个包括信号处理在内的线程管理机制。
pthread线程的创建依赖于clone函数,而clone、fork和vfork最终都依赖于do_fork函数。
2. 进程描述符
进程描述符:一个task_struct类型结构,它的字段包含了与进程相关的所有信息。
2.1 进程状态
state 字段:描述进程当前状态。
其由一组标志组成,每个标志描述一种可能的状态,这些状态是互斥的,设定一个则清楚其它标志。
其中TASK_ZOMBIE 和 TASK_DEAD 也可以放在exit_state字段中,只有当进程被终止时,才会设置这两种状态。
赋值与获取进程状态标志的方式:
2.2 进程标识
pid 字段:进程标志符,用于标识进程。
每个可被独立调度的执行上下文都拥有自己的进程描述符,即使共享内核大部分数据的轻量级进程,也有自己的task_struct结构,操作系统使用进程标志符标记一个task_struct结构,即每个进程标志符与一个进程或轻量级进程对应。
实际上pid_t 是一个32位整数,因此其默认最大值为32767(32位系统,64位4194303)。每次创建新进程,系统都会使用前一个创建进程的pid+1作为新进程的进程标志符号,直到达到PID_MAX_DEFAULT-1 后循环使用已闲置的小pid号,可修改/proc/sys/kernel/pid_max文件的值,设定更小的最大文件标识号。
为循环使用PID编号,内核管理一个pidmap_array位图(32位系统,存放于一个单独的页中,一个页框包含32768位; 64位可能有多个页)表示已分配和闲置的PID。
Linux中,一个线程实际上是一个轻量级线程,然而在我们的认知中,同一个进程中的线程应具有相同的PID,这一点让人非常费解。Linux中引入线程组的表示,即一个线程组中的所有线程使用与该线程组的领头线程相同的PID,也就是该组中的一个轻量级线程的PID,其被存入进程标识符的tgid字段中。领头线程(有且只有)的pid和tgid字段相同。当我们执行getpid命令时,实际上我们返回的是tgid字段。
进程标志符被存放在动态内存,而非永久分配给内核的内存区。内核可通过调用current宏获取当前运行在CPU上的进程的进程描述符指针,如current->pid返回在CPU上运行的进程的PID。对于多处理器系统,current被定义成一个数组,每个元素对应于一个可用CPU。
2.3 进程链表
tasks字段:该类型的prev和next字段分别指向前面和后面的task_struct元素。
2.4 TASK_RUNNING状态进程链表
run_list字段:若进程的优先级为k,这run_list讲进程链入优先级为k的可运行进程链表中。
enqueue_task(p, array) 将进程描述符插入某个运行队列的链表。
2.5 进程关系
以下字段用于表示进程亲缘关系:
以下字段用于表示非亲缘进程的一些关系:
2.6 pidhash表及链表
为保证可通过进程标志符到处进程描述符指针内核引入了4个散列表,内核初始化期间动态的为4个散列表分配空间,并将地址存入pid_hash数组中。
Linux采用链表法解决冲突的问题。PID散列表允许为散列表中的任何PID字段定义进程链表,因此在TGID散列表中,具有相同tgid的进程被连接成一个二级链表。
2.7 等待队列
等待队列:双向链表实现,头为一个wait_queue_head_t的数据结构,保存等待被唤醒的进程。
等待链表中的元素类型如下:
除了上面说的运行队列,实际上内核为除TASK_STOP、EXIT_ZOMBIE和EXIT_DEAD之外的状态都维护了不同的队列。等待队列的同步是通过队列头中的lcok自旋锁实现的。
2.8 进程资源限制
每个进程都有一组相关的资源限制,对当前进程的资源限制存放在current->signal->rlim字段中
该字段为以下结构:
3. 创建进程
传统Unix进程以统一的方式创建子进程:子进程复制父进程所拥有的资源。
但很多时候,子进程会立刻调用exec,进而又不得不删除复制过来的资源。为解决这个问题,引入了一下三种不同的机制:
写时复制技术(copy-on-right);轻量级进程允许复制进程共享内核中很多数据结构;vfork创建的进程可共享父进程的内存空间。
3.1 clone函数
轻量级进程(即线程)由名为clone的函数创建。
fn 指定一个由新进程执行的函数;
arg 传递给fn的数据;
flags 各种各样的信息,低字节指定子进程结束时发送给父进程的信号代码,通常悬在SIGCHLD信号,剩余3个字节指定克隆标记;
child_stack 表示把用户态堆栈指针赋给子进程的esp寄存器,调用进程应重视为子进程分配新的堆栈。
具体的clone标志如下(待添加):
实际上,上面的clone函数是C语言库中定义的一个封装函数,它负责建立新轻量级进程的堆栈,并调用系统调用clone。实现clone的系统调用sys_clone服务例程并没有fn和arg。封装函数将fn指针放在子进程堆栈的某个位置,该位置就是该封装函数自身返回地址存放的位置,arg指针正好放在子进程堆栈的fn的下面。这样封装函数结束时,CPU从堆栈取回地址,然后执行fn(arg)。
vfork:flags指定为SIGCHLD信号和CLONE_VM和 CLONE_VFORK,child_stack等于父进程当前的栈指针;
fork:flags指定为SIGCHLD信号和所有清零的clone标志,child_stack等于父进程当前的栈指针。
以下为调用关系的伪码:
3.3 内核线程
内核线程与普通线程有以下区别:
内核线程只运行在内核态,而普通进程即可以运行在内核态又可以运行在用户态
只是用大于PAGE_OFFSET的线性地址空间,而普通进程可以使用4GB的线性地址空间
kernel_thread()函数可用于创建一个新的内核线程,它接收的参数与clone基本相同,除去child_stack。该函数的本质是以下面的方式调用do_fork函数:
3.4 进程终止
Linux中有两个终止用户态应用的系统调用:
exit_group系统调用,终止整个线程组,内部调用do_group_exit函数,C语言库函数exit调用的函数;
exit系统调用,她终止某一个线程,内部调用do_exit,pthread_exit函数调用的函数。
进程是资源管理的最小单位,而线程是程序执行的最小单位。在操作系统设计上,从进程演化出线程的主要目的:更好的支持SMP以及减小(进程/线程)上下文切换的开销。
针对线程模型的两大意义,分别开发出了核心级线程(For SMP)和用户级线程(For 上线文切换)两类线程模型,其分类标准主要是线程的调度者是核内还是核外。很多系统都着重于开发混合模型,而Linux没有这种打算。
Linux内核只提供了轻量进程的支持(严格来讲,Linux中没有线程这一概念),尽管其限制了更高效的线程模型的实现,但Linux侧重于优化进程调度开销,一定程度上弥补了这一缺陷。目前最流行的线程机制LinuxThreads所采用的就是线程-进程"一对一"模型,调度交给核心,而在用户级实现一个包括信号处理在内的线程管理机制。
pthread线程的创建依赖于clone函数,而clone、fork和vfork最终都依赖于do_fork函数。
2. 进程描述符
进程描述符:一个task_struct类型结构,它的字段包含了与进程相关的所有信息。
2.1 进程状态
state 字段:描述进程当前状态。
volatile long state; int exit_state;
其由一组标志组成,每个标志描述一种可能的状态,这些状态是互斥的,设定一个则清楚其它标志。
TASK_RUNNING //可运行状态。进程要么在CPU上执行,要么准备执行 TASK_INTERRUPTIBLE //可中断的等待状态。进程被挂起,直到某个条件为真后被唤醒(进程状态变为TASK_RUNNING) TASK_UNINTERRUPTIBLE //不可中断的等待状态。与上面不同,信号无法唤醒该状态的进程 TASK_STOPPED //暂停状态。进程的执行被暂停,收到SIGSTOP、SIGTSTO、SIGTTIN或SIGTTOT后进入该状态 TASK_TRACED //跟踪状态。进程由debugger程序暂停 EXIT_ZOMBIE //僵尸状态。进程被终止,但资源未释放(父进程未wait 或 waitpid) EXIT_DEAD //僵尸撤销状态。最终状态,父进程刚发出wait或waitpid调用,因而进程由系统删除,防止其他进程调用wait(竞争状态)
其中TASK_ZOMBIE 和 TASK_DEAD 也可以放在exit_state字段中,只有当进程被终止时,才会设置这两种状态。
赋值与获取进程状态标志的方式:
struct task_struct p; p->state = TASK_RUNNING // 简单赋值 set_current_state(TASK_RUNNING) //宏定义。设置当前进程状态 set_task_state(p,TASK_RUNNING) //宏定义。设置指定进程的状态
2.2 进程标识
pid 字段:进程标志符,用于标识进程。
pid_t pid; pid_t tgid
每个可被独立调度的执行上下文都拥有自己的进程描述符,即使共享内核大部分数据的轻量级进程,也有自己的task_struct结构,操作系统使用进程标志符标记一个task_struct结构,即每个进程标志符与一个进程或轻量级进程对应。
实际上pid_t 是一个32位整数,因此其默认最大值为32767(32位系统,64位4194303)。每次创建新进程,系统都会使用前一个创建进程的pid+1作为新进程的进程标志符号,直到达到PID_MAX_DEFAULT-1 后循环使用已闲置的小pid号,可修改/proc/sys/kernel/pid_max文件的值,设定更小的最大文件标识号。
为循环使用PID编号,内核管理一个pidmap_array位图(32位系统,存放于一个单独的页中,一个页框包含32768位; 64位可能有多个页)表示已分配和闲置的PID。
Linux中,一个线程实际上是一个轻量级线程,然而在我们的认知中,同一个进程中的线程应具有相同的PID,这一点让人非常费解。Linux中引入线程组的表示,即一个线程组中的所有线程使用与该线程组的领头线程相同的PID,也就是该组中的一个轻量级线程的PID,其被存入进程标识符的tgid字段中。领头线程(有且只有)的pid和tgid字段相同。当我们执行getpid命令时,实际上我们返回的是tgid字段。
进程标志符被存放在动态内存,而非永久分配给内核的内存区。内核可通过调用current宏获取当前运行在CPU上的进程的进程描述符指针,如current->pid返回在CPU上运行的进程的PID。对于多处理器系统,current被定义成一个数组,每个元素对应于一个可用CPU。
2.3 进程链表
tasks字段:该类型的prev和next字段分别指向前面和后面的task_struct元素。
list_head *tasks;通过该结构内核把素偶有描述符链接起来,形成一个双向链表,进程链表(循环链表)。进程链表的头是init_task描述符,它是0进程的进程描述符,其prev字段指向最后插入的进程描述符的tasks字段。以下宏考虑了进程间的父子关系:
SET_LINKS //插入一个进程描述符 REMOVE_LINKS //删除一个进程描述符 for_each_process //遍历进程链表
2.4 TASK_RUNNING状态进程链表
run_list字段:若进程的优先级为k,这run_list讲进程链入优先级为k的可运行进程链表中。
list_head *run_list;内核使用以下结构维护所有优先级的可运行进程链表:
struct prio_array_t{ int nr_active; unsigned long bitmap[5]; struct list_head queue[140]; }
enqueue_task(p, array) 将进程描述符插入某个运行队列的链表。
2.5 进程关系
以下字段用于表示进程亲缘关系:
struct task_struct *real_parent; //创建P的进程的描述符。若父进程不存在,则为init进程的描述符 struct tast_struct *parent; //P进程的当前父进程的描述符。一般与上一个相同,除dubugger时 struct list_head children; // 只进程链表的表头。 struct list_head sibling; //指向前一个或后一个兄弟进程。他们的父亲都是P
以下字段用于表示非亲缘进程的一些关系:
struct task_struct *group_leader; //进程组长的描述符指针。 pid_t signal->pgrp ; // 进程主张的PID。 pid_t signal->session; // 会话组长的PID。
2.6 pidhash表及链表
为保证可通过进程标志符到处进程描述符指针内核引入了4个散列表,内核初始化期间动态的为4个散列表分配空间,并将地址存入pid_hash数组中。
PIDTYPE_PID pid //Hash表名 字段 PIDTYPE_TGID tgid PIDTYPE_PGID pgrp PIDTYPE_SID sesssion
Linux采用链表法解决冲突的问题。PID散列表允许为散列表中的任何PID字段定义进程链表,因此在TGID散列表中,具有相同tgid的进程被连接成一个二级链表。
2.7 等待队列
等待队列:双向链表实现,头为一个wait_queue_head_t的数据结构,保存等待被唤醒的进程。
struct __wait_queue_head{ spinlock_t lock; //用于队列同步 struct list_head task_list;//等待链表头 }
等待链表中的元素类型如下:
struct __wait_queue{ unsigned int flags; struct task_struct *task; wait_queue_func_t func; struct list_head task_list; } typedef struct __wait_queue wait_queue_t;
除了上面说的运行队列,实际上内核为除TASK_STOP、EXIT_ZOMBIE和EXIT_DEAD之外的状态都维护了不同的队列。等待队列的同步是通过队列头中的lcok自旋锁实现的。
2.8 进程资源限制
每个进程都有一组相关的资源限制,对当前进程的资源限制存放在current->signal->rlim字段中
该字段为以下结构:
struct rlimit{ unsigned long rlim_cur; unsigned long rlim_max; }通过不同的字段名,可以访问不同的资源限制,如
current->signal->rlim[RLIMIT_CPU]; //取得CPU资源限制还可以通过getrlimit 和setrlimit系统调用访问和设置系统资源。
3. 创建进程
传统Unix进程以统一的方式创建子进程:子进程复制父进程所拥有的资源。
但很多时候,子进程会立刻调用exec,进而又不得不删除复制过来的资源。为解决这个问题,引入了一下三种不同的机制:
写时复制技术(copy-on-right);轻量级进程允许复制进程共享内核中很多数据结构;vfork创建的进程可共享父进程的内存空间。
3.1 clone函数
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg);
轻量级进程(即线程)由名为clone的函数创建。
fn 指定一个由新进程执行的函数;
arg 传递给fn的数据;
flags 各种各样的信息,低字节指定子进程结束时发送给父进程的信号代码,通常悬在SIGCHLD信号,剩余3个字节指定克隆标记;
child_stack 表示把用户态堆栈指针赋给子进程的esp寄存器,调用进程应重视为子进程分配新的堆栈。
具体的clone标志如下(待添加):
实际上,上面的clone函数是C语言库中定义的一个封装函数,它负责建立新轻量级进程的堆栈,并调用系统调用clone。实现clone的系统调用sys_clone服务例程并没有fn和arg。封装函数将fn指针放在子进程堆栈的某个位置,该位置就是该封装函数自身返回地址存放的位置,arg指针正好放在子进程堆栈的fn的下面。这样封装函数结束时,CPU从堆栈取回地址,然后执行fn(arg)。
vfork:flags指定为SIGCHLD信号和CLONE_VM和 CLONE_VFORK,child_stack等于父进程当前的栈指针;
fork:flags指定为SIGCHLD信号和所有清零的clone标志,child_stack等于父进程当前的栈指针。
以下为调用关系的伪码:
clone(): fork(): vforf(){ clone(){ do_fork(){ copy_process(); } }; }
3.3 内核线程
内核线程与普通线程有以下区别:
内核线程只运行在内核态,而普通进程即可以运行在内核态又可以运行在用户态
只是用大于PAGE_OFFSET的线性地址空间,而普通进程可以使用4GB的线性地址空间
kernel_thread()函数可用于创建一个新的内核线程,它接收的参数与clone基本相同,除去child_stack。该函数的本质是以下面的方式调用do_fork函数:
do_fork(flags|CLONE_VM|CLONE_UNTRACED, 0 , pregs, 0, NULL, NULL);
3.4 进程终止
Linux中有两个终止用户态应用的系统调用:
exit_group系统调用,终止整个线程组,内部调用do_group_exit函数,C语言库函数exit调用的函数;
exit系统调用,她终止某一个线程,内部调用do_exit,pthread_exit函数调用的函数。
相关文章推荐
- Linux进程学习(一)之Linux进程的基本知识和实现
- linux操作系统知识分享(进程简介,SSH使用原理图,配置环境变量,系统基本信息查询,系统管理,war包部署相关命令)
- Linux进程基本知识
- Linux进程的基本知识
- 辛星浅析Linux中的进程基本知识以及fork的简单理解
- Linux基本知识(6)——进程及信号
- 一片较好的文章帮你理解Linux 下图形界面(X)的基本知识
- 交叉编译场景分析(arm-linux)(一)--基本知识
- 嵌入式linux最基本知识框架
- Linux 使用基本知识:文件名
- linux基本知识
- 交叉编译场景分析(arm-linux)(一)--基本知识
- 交叉编译场景分析(arm-linux)(一)--基本知识
- linux图形界面编程基本知识
- linux的一点基本知识
- linux图形界面编程基本知识(zz)
- 关于linux图形界面的基本知识
- 关于Linux图形界面的基本知识
- 关于linux图形界面的基本知识
- linux图形界面编程基本知识