进程-Linux

本文详细介绍了Linux系统中的进程概念,包括轻量级进程(LWP)、线程与进程的区别、线程组的组织方式以及进程描述符的获取。通过进程描述符task_struct,讨论了如何在内核中标识和调度进程,特别是通过PID哈希表进行高效查找。此外,还涉及到了进程间的关系、等待队列的使用以及创建进程的过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

进程

定义:

进程是程序执行时的一个实例。

轻量级进程(LWP):

两个轻量级进程之间可以共享一些资源,诸如地址空间,同一打开文件集等来访问相同的应用程序结构集。只要其中一个修改共享资源,另一个就立即查看这种修改(同步情况下)。

线程 VS 轻量级进程:

轻量级进程能很好共享公共资源。同时,又不会在系统阻塞进程的时候,阻塞了线程,因为每个LWP都有内核独立调度。

线程组:

Linux 中实现线程组的方式是用多个轻量级进程构成的集合。线程组中有一个领头的线程叫做首领线程,线程组中的每个线程的PID号相同!也就是该组中第一个轻量级进程的PID。(因为每个线程都是一个LWP,所以有一个PID号)

标识一个进程 ——进程描述符

         程序能够独立调度的每个执行上下文都必须拥有自己的进程描述符。Linux 的进程描述符保存在 task_struct中。

获取进程描述符

1如果该进程是当前CPU正在执行的进程

  current宏得到。它等价于

ð   current_thread_info()->task.

ð  产生的汇编如下

movl $oxffffe000, %ecx

andl %ecx, %ecx

movl (%ecx), p

理论推导:

      每个进程都有一个thread_info的结构体成员,这个结构体和内核堆栈一起分配在一个联合体内, thread_union。定义如下

union thead_union {

struct thread_info thread_info;

unsigned long stack[2048];  //内核堆栈

};

此结构体的大小是 2048 * 4,正好是2个页框(一个页框4K)大小。

CPU 的堆栈寄存器esp指向当前进程的堆栈。忽略掉低13位(8K)就是thread_info结构体的基地址。通过thread_info的成员task可以指向他的进程描述符地址。

current_thread_info()   : 得到 thread_info的基地。

current_thread_info()->task : 得到当前进程描述符的地址。

2.寻找非当前运行进程

2.1 进程切换时,从TASK_RUNNING链表中寻找最合适的进程。

         早期Linux的TASK_RUNNING链表是一个单循环链表,为了找到最佳可运行进程必须扫描整个链表!这样做是开销是非常大的。

         Linux2.6实现的运行队列有所不同,他采取以空间换时间的策略,为每个优先级都分配一个链表头。这样就把长的链表切割成多个段链表。所有的这些链表都组装在一个结构体内,其结构如下:

struct prio_array_t{

         intnr_active;                  //链表中进程描述符的数量

              unsigned long bitmap[5];            //优先权位图:当且仅当某个优先权的进程链表不为

//空时设置相应的位标志为1        

struct list_head[140];             //140个优先权队列的头结点

}

注:1. Linux的进程优先级从 0——139 ,共140个优先级。

2. 存储进程描述符的数据结构为什么不选择小根堆

因为小根堆是数组操作,意味着这是静态的,因为Linux中最多有32767个(215-1)进程描述符,意味着你要一次申请 32767个 list_head。会造成极大的浪费。

3.Linux中最大的进程描述符数目为什么是32767

因为Linux用pidmap_array位图来表示那些进程PID号已经使用,它分配在一个单独的页框中,一个页框是4K,能够表示的最大位数是 4 * 1024 * 8(位) = 215 = 32768.为什么会减1,没有弄太清楚,估计是init进程(swapper进程)作为0号进程占了一位吧?!

2.2通过PID号和 类型寻找进程描述符

         首先通过PID号来查找一个进程描述符,不能像TASK_RUNNING链表那样分成140个链表来区分不同优先级类型的进程描述符。因为,PID != 优先级,不能通过PID给进程分类,因为PID是唯一的,范围是 0——32767.其次,查找速度要快而且有上界。

其实,在Linux中采用的是散列表来查找的。

Hash表的类型:

        

 
 

Hash表类型

 
 
 

字段名

 
 
 

说明

 
 
 

PIDTYPE_PID

 
 
 

pid

 
 
 

进程的PID

 
 
 

PIDTYPE_TGID

 
 
 

tgid

 
 
 

线程组领头进程的PID

 
 
 

PIDTYPE_PGID

 
 
 

pgrp

 
 
 

进程组领头进程的PID

 
 
 

PIDTYPE_SID

 
 
 

session

 
 
 

会话领头进程的PID

 

每个散列表应该取多大?这主要决定于你用多少个页框来存放一个散列表。

来看,Linux内核代码

         include/linux/sched.h

#define PIDHASH_SZ (4096 >;>; 2)

extern struct task_struct *pidhash[PIDHASH_SZ];

#define pid_hashfn(x) ((((x) >;>; ^ (x)) & (PIDHASH_SZ . 1))

哈希表大小为1024个表项。指针数组pidhash有1024个地址,一个地址一般是4B(32位系统)那么 1024 * 4B = 1页框。

 

pid_hashfn(x): 可以根据PID号X检索出索引表项号。

 

冲突处理:

         利用链表法来处理冲突。进程描述符中pids[1]结构中有一个成员叫pid_chain它指向下一个哈希链表成员。

 

 

常用处理散列表的函数和宏

1 do_each_task_pid(nr, type, task)

2 while_each_task_pid(nr , type, task)  循环作用在PID值等于nr的PID连表上。task参数指向当前当前被扫描的元素的进程描述符。

3 find_task_by_pid_type(type, nr)

attach_pid(task, type, nr) :把task指向的PID等于nr的进程描述符插入type类的散列表中。如果PID等于nr的进程描述符已经在散列表中,这个函数就把task插入已有的PID进程链表中。

detach_pid(task , type)

next_thread(task) :返回PIDTYPE_TGID类型的散列表链表中task只是的下一个轻量级进程的进程描述符。

进程间关系

         程序创建的进程具有父/子关系。如果一个进程创建多个子进程。则子进程之间具有兄弟关系。进程0,进程1是由内核创建;进程1(init)是所有进程的祖先。

有图可见,如果一个子进程没有兄弟节点,那么他的sibling.prev sibling.next指向父节点。最后一个孩子的sibling.next指向父亲。

父节点的 child.next 指向第一个孩子,child.prev指向最后一个孩子。

如何组织进程

         运行队列链表把处于TASK_RUNNING状态的所有进程组织在一起,当要把其他状态的进程分组时,不同的状态要求不同的处理,Linux选择了下列方式之一:

1.      没有处理TASK_STOPPED, EXIT_ZOMBIE, 或EXIT_DEAD状态的进程建立专门的链表。由于处于暂停,僵死,死亡状态的进程的访问比较简单。或者通过PID,或者通过特定父进程的子进程链表,所以不必对这三种状态进程分组。

2.      根据不同的特殊事件把处于TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE状态的进程细分为许多类。每一类都对应某个特殊事件。在这种情况下,进程状态提供的信息满足不了快速检索进程的需要,所以必须引入另外的进程链表。这些链表被称为等待队列。

等待队列

 

等待队列在内核中有很多用途,尤其用在中断处理,进程同步及定时。进程必须进厂等待某些事件的发生,例如,等待一个磁盘操作的终止,等待释放系统资源,或等待时间经过固定的间隔。等待队列实现了在事件上的条件等待:希望等待特定事件的进程把自己放进合适的等待队列,并放弃控制权。因此,等待队列表示一组睡眠过程。当某一条件为真时,由内核唤醒他们。(深入理解Linux内核 page.101)

等待队列列头

struct__wait_queue_head{

       spinlock_t lock;

       struct list_head task_list;

};

typedef struct__wait_queue_head wait_queue_head_T;

等待队列中的元素类型

struct__wait_queue {

       unsigned int flags; //为1时表示互斥进程

       struct task_struct *task;  //指向进程描述符

       wait_queue_func_t func;  //

       struct list_head  task_list; //指向等待相同时间的进程

};

typedefstruct  _wait_queue wait_queue_t;

由图可以看出,每当要使一个进程睡眠的时候,都会申请一个wait_queue_t。然后将进程描述符挂在上去。并添加到等待队列链表。

等待队列的操作:

包含在: linux/include/linux/wait.h

1.    定义新的等待队列头

DECLARE_WAIT_QUEUE_HEAD(name)

其中 __WAIT_QUEUE_HEAD_INITIALIZER定义如下

2.    初始化等待队列基元。

3.    添加/删除等待队列基元

从添加基元函数来看,只是吧 new的task_list地址付给了head的task_list,而不是wait_queue_t本身,而是其结构体内的一个成员。从而证明了上面等待队列图片的正确性。

4.    判断等待队列是否为空

5.    条件等待

添加时间段的条件等待

实现可阻塞的等待队列等待操作类似于以上函数。

参看  http://lxr.oss.org.cn/source/include/linux/wait.h?v=2.6.16#L87

250 行

 

创建进程

         do_fork()函数负责处理clone(), fork(), vfork()系统调用。其中,clone()创建一个轻量级进程。vfork()创建的进程会阻塞父进程,知道子进程执行完毕。

函数申明如下:

参数:

clone_flags : 各种信息,低字节(第一个字节)指定子进程结束时发送到父进程的信号代码,一般选择是GICHLD信号,剩余三个字节给一clone标志组用于编码。

stack_start :表示把用户对栈指针赋予子进程的esp寄存器。

Regs:指向通用寄存器的指针,通用寄存器的值是在从用户态切换到内核态时被保存到内核堆栈中的。ChinaUnix个人空间 xd*JK4DR$XL%X

Stack_size:未使用(总设置为0)5u MX,gF-U:mu*W0

Parent_tidptr : 表示父进程的用户态变量地址。}9w-F?9H2^A‑|,X0

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

 

 

 

 

 

附录 1 进程描述符结构体

struct task_struct {

volatile long state;  //说明了该进程是否可以执行,还是可中断等信息

unsigned long flags;  //Flage 是进程号,在调用fork()时给出
int sigpending;    //进程上是否有待处理的信号
mm_segment_t addr_limit; //进程地址空间,区分内核进程与普通进程在内存存放的位置不同

                        //0-0xBFFFFFFFfor user-thead
                        //0xC0000000 -0xFFFFFFFFfor kernel-thread

//调度标志,表示该进程是否需要重新调度,若非0,则当从内核态返回到用户态,会发生调度
volatile long need_resched;

int lock_depth;  //锁深度
long nice;       //进程的基本时间片

//进程的调度策略,有三种,实时进程:SCHED_FIFO,SCHED_RR,分时进程:SCHED_OTHER
unsigned long policy;
struct mm_struct *mm; //进程内存管理信息
int processor;
//若进程不在任何CPU上运行,cpus_runnable 的值是0,否则是1 这个值在运行队列被锁时更新
unsigned long cpus_runnable, cpus_allowed;
struct list_head run_list; //指向运行队列的指针
unsigned long sleep_time;  //进程的睡眠时间

//用于将系统中所有的进程连成一个双向循环链表,其根是init_task
struct task_struct *next_task, *prev_task;
struct mm_struct *active_mm;
struct list_head local_pages;       //指向本地页面      
unsigned int allocation_order, nr_local_pages;
struct linux_binfmt *binfmt;  //进程所运行的可执行文件的格式
int exit_code, exit_signal;
int pdeath_signal;     //父进程终止是向子进程发送的信号
unsigned long personality;
//Linux可以运行由其他UNIX操作系统生成的符合iBCS2标准的程序
int did_exec:1; 
pid_t pid;    //进程标识符,用来代表一个进程
pid_t pgrp;   //进程组标识,表示进程所属的进程组
pid_t tty_old_pgrp;  //进程控制终端所在的组标识
pid_t session;  //进程的会话标识
pid_t tgid;
int leader;     //表示进程是否为会话主管
struct task_struct *p_opptr,*p_pptr,*p_cptr,*p_ysptr,*p_osptr;
struct list_head thread_group;   //线程链表
struct task_struct *pidhash_next; //用于将进程链入HASH表
struct task_struct **pidhash_pprev;
wait_queue_head_t wait_chldexit;  //供wait4()使用
struct completion *vfork_done;  //供vfork()使用
unsigned long rt_priority; //实时优先级,用它计算实时进程调度时的weight值

 

//it_real_value,it_real_incr用于REAL定时器,单位为jiffies, 系统根据it_real_value

//设置定时器的第一个终止时间. 在定时器到期时,向进程发送SIGALRM信号,同时根据

//it_real_incr重置终止时间,it_prof_value,it_prof_incr用于Profile定时器,单位为jiffies。

//当进程运行时,不管在何种状态下,每个tick都使it_prof_value值减一,当减到0时,向进程发送

//信号SIGPROF,并根据it_prof_incr重置时间.
//it_virt_value,it_virt_value用于Virtual定时器,单位为jiffies。当进程运行时,不管在何种

//状态下,每个tick都使it_virt_value值减一当减到0时,向进程发送信号SIGVTALRM,根据

//it_virt_incr重置初值。

unsigned long it_real_value, it_prof_value,it_virt_value;
unsigned long it_real_incr, it_prof_incr, it_virt_value;
struct timer_list real_timer;   //指向实时定时器的指针
struct tms times;      //记录进程消耗的时间
unsigned long start_time;  //进程创建的时间

//记录进程在每个CPU上所消耗的用户态时间和核心态时间
long per_cpu_utime[NR_CPUS], per_cpu_stime[NR_CPUS]; 
//内存缺页和交换信息:

//min_flt, maj_flt累计进程的次缺页数(Copy on Write页和匿名页)和主缺页数(从映射文件或交换

//设备读入的页面数);nswap记录进程累计换出的页面数,即写到交换设备上的页面数。
//cmin_flt, cmaj_flt, cnswap记录本进程为祖先的所有子孙进程的累计次缺页数,主缺页数和换出页面数。

//在父进程回收终止的子进程时,父进程会将子进程的这些信息累计到自己结构的这些域中
unsigned long min_flt, maj_flt, nswap, cmin_flt, cmaj_flt,cnswap;
int swappable:1; //表示进程的虚拟地址空间是否允许换出
//进程认证信息
//uid,gid为运行该进程的用户的用户标识符和组标识符,通常是进程创建者的uid,gid

//euid,egid为有效uid,gid
//fsuid,fsgid为文件系统uid,gid,这两个ID号通常与有效uid,gid相等,在检查对于文件

//系统的访问权限时使用他们。
//suid,sgid为备份uid,gid
uid_t uid,euid,suid,fsuid;
gid_t gid,egid,sgid,fsgid;
int ngroups; //记录进程在多少个用户组中
gid_t groups[NGROUPS]; //记录进程所在的组

//进程的权能,分别是有效位集合,继承位集合,允许位集合
kernel_cap_t cap_effective, cap_inheritable, cap_permitted;

int keep_capabilities:1;
struct user_struct *user;
struct rlimit rlim[RLIM_NLIMITS];  //与进程相关的资源限制信息
unsigned short used_math;   //是否使用FPU
char comm[16];   //进程正在运行的可执行文件名
 //文件系统信息
int link_count, total_link_count;

//NULL if no tty 进程所在的控制终端,如果不需要控制终端,则该指针为空
struct tty_struct *tty;
unsigned int locks;
//进程间通信信息
struct sem_undo *semundo;  //进程在信号灯上的所有undo操作
struct sem_queue *semsleeping; //当进程因为信号灯操作而挂起时,他在该队列中记录等待的操作
//进程的CPU状态,切换时,要保存到停止进程的task_struct中
struct thread_struct thread;
  //文件系统信息 表示每个进程自己的当前的工作目录和它自己的跟目录

struct fs_struct *fs;
  //打开文件信息 进程当前打开的文件
struct files_struct *files;
  //信号处理函数
spinlock_t sigmask_lock;
struct signal_struct *sig; //信号处理函数
sigset_t blocked;  //进程当前要阻塞的信号,每个信号对应一位
struct sigpending pending;  //进程上是否有待处理的信号
unsigned long sas_ss_sp;
size_t sas_ss_size;
int (*notifier)(void *priv);
void *notifier_data;
sigset_t *notifier_mask;
u32 parent_exec_id;
u32 self_exec_id;

spinlock_t alloc_lock;
void *journal_info;
};

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值