Linux进程基本知识

来源:互联网 发布:网络嗅探器 sniffer 编辑:程序博客网 时间:2024/05/21 00:20
1.基本概念
进程是资源管理的最小单位,而线程是程序执行的最小单位。在操作系统设计上,从进程演化出线程的主要目的:更好的支持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   tgidPIDTYPE_PGID   pgrpPIDTYPE_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函数调用的函数。







0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 小孩家里说话外面不说话怎么办 2个月小宝宝便秘怎么办 3个月小宝宝便秘怎么办 微信客户不说话怎么办 一岁半宝宝便秘肛裂怎么办 网恋现在都不说话了怎么办 宝宝便秘四天了怎么办 月子里小孩吐奶怎么办 月子里的小孩吐奶怎么办 婴儿吃多了吐奶怎么办 20个月孩子便秘怎么办 一岁宝宝肛裂怎么办 婴儿吃饱了吐奶怎么办 23天新生儿吐奶怎么办 婴儿吐奶舌苔白怎么办 宝宝吐奶酸臭味怎么办? 1周岁吐奶有酸味怎么办 十多天的宝宝吐奶怎么办 未满月婴儿吐奶怎么办 2个月宝宝溢奶怎么办 四岁宝宝说话结巴怎么办 小孩说话结巴打顿怎么办 2岁宝宝突然说话结巴怎么办 2岁宝宝突然结巴怎么办 幼儿舌头起泡牙龈出血怎么办 小孩长得太快怎么办 脑出血压着神经不会说话怎么办 四岁宝宝说话有点口吃怎么办 三岁宝宝有点口吃怎么办 3岁宝宝有点口吃怎么办 三岁宝宝说话有点口吃怎么办 六岁说话重复第一个字怎么办 宝贝烧到39.5度怎么办 宝贝39度不退烧怎么办 两岁多小儿突然变得口吃怎么办 百度两周岁宝宝口吃怎么办 2岁宝宝偶尔结巴怎么办 两岁宝宝说话磕巴怎么办 宝宝两岁结巴了怎么办 人多说话就紧张怎么办 小孩拉尿不叫人怎么办