Linux进程基本知识

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   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函数调用的函数。







  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值