每晚十五分钟,通读一个技术点,水滴石穿,一切只为渴求更优秀的你!
————零声大学
4.4task_struct结构在显存中的储存
task_struct结构在显存的储存与内核栈是分不开的,为此,首先讨论内核栈。
4.4.1进程内核栈
每位进程都有自己的内核栈。当进程从用户态步入内核态时,CPU就手动地设置该进程
的内核栈,也就是说,CPU从任务状态段TSS中放入内核栈表针esp(参见下一章的进程切换
一节)。
X86内核栈的分布如图4.2所示。
在Intel系统中,栈起始于末端,并朝这个显存区开始的方向下降。从用户态刚切换到
内核态之后,进程的内核栈总是空的,为此,esp寄存器直接指向这个显存区的顶端在图4.2
中,从用户态切换到内核态后,esp寄存器包含的地址为0xx018018fc00。进程描述符储存在从
0x015fa00开始的地址。只要把数据写进栈中,esp的值就递减。
在/include/linux/sched.h中定义了如下一个联合结构:
union task_union {
struct task_struct task;
unsigned long stack[2408];
};
从这个结构可以看出,内核栈占8KB的显存区。实际上,进程的task_struct结构所占
的显存是由内核动态分配的,更准确地说,内核根本不给task_struct分配显存,而仅仅给
内核栈分配8KB的显存,并把其中的一部份给task_struct使用。
task_struct结构大概占1K字节左右,其具体数字与内核版本有关,由于不同的版本其
域稍有不同。为此,内核栈的大小不能超过7KB,否则,内核栈会覆盖task_struct结构,
因而造成内核崩溃。不过,7KB大小对内核栈已足够。
把task_struct结构与内核栈置于一起具有以下益处:
•内核可以便捷而快速地找到这个结构,用伪代码描述如下:
task_struct=(structtask_struct*)STACK_POINTER&0xffffe000
•防止在创建进程时动态分配额外的显存。
•task_struct结构的起始地址总是开始于页大小(PAGE_SIZE)的边界。
4.4.2当前进程(current宏)
当一个进程在某个CPU上正在执行时,内核怎样获得指向它的task_struct的表针?上
面所提及的储存形式为达到这一目的提供了便捷。在linux/include/i386/current.h中
定义了current宏,这是一段与体系结构相关的代码:
tatic inline struct task_struct * get_current(void)
{
struct task_struct *current;
__asm__("andl %%esp,%0; ":"=r" (current) : "0" (~8191UL));
return current;
}
实际上,这段代码相当于如下一组汇编指令(设p是指向当前进程task_struc结构的
表针):
movl$0xffffe000,%ecx
andl%esp,%ecx
movl%ecx,p
换句话说,仅仅只需复查栈表针的值,而根本无需存取显存,内核就可以导入
task_struct结构的地址。
在本书的描述中,会时常出现current宏,在内核代码中也随处可见,可以把它看作全
局变量来用,比如,current->pid返回在CPU上正在执行的进程的标示符。
另外,在include/i386/processor.h中定义了两个函数free_task_struct()和
alloc_task_struct(),前一个函数释放8KB的task_union显存区,而后一个函数分配8KB
的task_union显存区。
4.5进程组织形式
在Linux中,可以把进程分为用户任务和内核线程,不管是哪一种进程,它们都有自己
的task_struct。在2.4版中,系统拥有的进程数可能达到数千乃至上万个,尤其对于企业
级应用(如数据库应用及网路服务器)更是这么。为了对系统中的好多进程及处于不同状态
的进程进行管理,Linux采用了如下几种组织形式。
4.5.1哈希表
哈希表是进行快速查找的一种有效的组织形式。Linux在进程中引入的哈希表称作
pidhash,在include/linux/sched.h中定义如下:
#definePIDHASH_SZ(4096>>2)
externstructtask_struct*pidhash[PIDHASH_SZ];
#definepid_hashfn(x)((((x)>>8)^(x))&(PIDHASH_SZ-1))
其中,PIDHASH_SZ为表中元素的个数,表中的元素是指向task_struct结构的表针。
pid_hashfn为哈希函数,把进程的PID转换为表的索引。通过这个函数,可以把进程的PID
均匀地散列在它们的域(0到PID_MAX-1)中。
在数据结构课程中我们早已了解到,哈希函数并不总能确保PID与表的索引一一对应,
两个不同的PID散列到相同的索引称为冲突。
Linux借助链地址法来处理冲突的PID:也就是说,每一表项是由冲突的PID组成的双
向数组,这些数组是由task_struct结构中的pidhash_next和pidhash_pprev域实现的,
同一数组中pid的大小由小到大排列。如图4.3所示。
哈希表pidhash中插入和删掉一个进程时可以调用hash_pid()和unhash_pid()
函数。对于一个给定的pid,可以通过find_task_by_pid()函数快速地找到对应的进程:
static inline struct task_struct *find_task_by_pid(int pid)
{
struct task_struct *p, **htable = &pidhash[pid_hashfn(pid)];
for(p = *htable; p && p->pid != pid; p = p->pidhash_next)
;
return p;
}
4.5.2单向循环数组
哈希表的主要作用是按照进程的pid可以快速地找到对应的进程,但它没有反映进程创
建的次序,也未能反映进程之间的亲属关系,因而引入单向循环数组。每位进程task_struct
结构中的prev_task和next_task域拿来实现这些数组,如图4.4所示。
宏SET_LINK拿来在该数组中插入一个元素:
#define SET_LINKS(p) do {
(p)->next_task = &init_task;
(p)->prev_task = init_task.prev_task;
init_task.prev_task->next_task = (p);
init_task.prev_task = (p);
(p)->p_ysptr = NULL;
if (((p)->p_osptr = (p)->p_pptr->p_cptr) != NULL)
(p)->p_osptr->p_ysptr = p;
(p)->p_pptr->p_cptr = p;
} while (0)
从这段代码可以看出,数组的头和尾都为init_task,它对应的是进程0(pid为0),
也就是所谓的空进程,它是所有进程的先祖。这个宏把进程之间的亲属关系也链接上去。另
外,还有一个宏for_each_task():
#definefor_each_task(p)
for(p=&init_task;(p=p->next_task)!=&init_task;)
这个宏是循环控制词句。注意init_task的作用,由于空进程是一个永远不存在的进程,
为此用它做数组的头和尾是安全的。
由于进程的单向循环数组是一个临界资源,因而在使用这个宏时一定要加锁,使用完后
换锁。
4.5.3运行队列
当内核要找寻一个新的进程在CPU上运行时,必须只考虑处于可运行状态的进程(即在
TASK_RUNNING状态的进程),由于扫描整个进程数组是相当低效的,所以引入了可运行状态
进程的单向循环数组,也叫运行队列(runqueue)。
运行队列容纳了系统中所有可以运行的进程,它是一个单向循环队列,其结构如图4.5
所示。
4.5.4进程的运行队列数组
该队列通过task_struct结构中的两个表针run_list数组来维持。队列的标志有两个:
一个是“空进程”idle_task,一个是队列的厚度。
有两个特殊的进程永远在运行队列中待着:当前进程和空进程。上面我们讨论过,当前
进程就是由cureent表针所指向的进程,也就是当前运行着的进程,并且请注意,current
表针在调度过程中(调度程序执行时)是没有意义的,为何如此说呢?调度前,当前进程
正在运行,当出现某种调度时机引起了进程调度,原本运行着的进程处于哪些状态是不可知
的,多数情况下处于等待状态,所以这时侯current是没有意义的,直至调度程序选取某个
进程投入运行后,current才真正指向了当前运行进程;空进程是个比较特殊的进程redhat linux 9.0,只有
系统中没有进程可运行时它就会被执行,Linux将它看作运行队列的头,当调度程序遍历运
行队列,是从idle_task开始、至idle_task结束的,在调度程序运行过程中,容许队列中
加入新出现的可运行进程,新出现的可运行进程插入到队尾linux内核4.17.2,这样的用处是不会影响到调度
程序所要遍历的队列成员,可见,idle_task是运行队列很重要的标志。
另一个重要标志是队列厚度,也就是系统中处于可运行状态(TASK_RUNNING)的进程数
目,用全局整型变量nr_running表示,在/kernel/fork.c中定义如下:
intnr_running=1;
若nr_running为0,就表示队列中只有空进程。在这儿要说明一下:若nr_running为
0,则系统中的当前进程和空进程就是同一个进程。并且Linux会充分借助CPU而尽量避开出
现这些情况。
4.5.5等待队列
在2.4版本中,引入了一种特殊的数组—通用单向数组,它是内核中实现其他数组的
基础,也是面向对象的思想在C语言中的应用。在等待队列的实现中多次涉及与此数组相关
的内容。
1.通用单向数组
在include/linux/list.h中定义了这些数组:
structlist_head{
structlist_head*next,*prev;
};
这是单向数组的一个基本框架,在其他使用数组的地方就可以使用它来定义任意一个双
向数组,比如:
struct foo_list {
int data;
struct list_head list;
};
对于list_head类型的数组,Linux定义了5个宏:
#define LIST_HEAD_INIT(name) { &(name), &(name) }
#define LIST_HEAD(name)
struct list_head name = LIST_HEAD_INIT(name)
#define INIT_LIST_HEAD(ptr) do {
(ptr)->next = (ptr); (ptr)->prev = (ptr);
} while (0)
#define list_entry(ptr, type, member)
((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member)))
#define list_for_each(pos, head)
for (pos = (head)->next; pos != (head); pos = pos->next)
前3个宏都是初始化一个空的数组,但用法不同,LIST_HEAD_INIT()在申明时使用,用
来初始化结构元素,第2个宏用在静态变量初始化的申明中,而第3个宏用在函数内部。
其中,最难理解的宏为list_entry(),在内核代码的好多处都用到这个宏,比如,在
调度程序中,从运行队列中选择一个最值得运行的进程,部份代码如下:
static LIST_HEAD(runqueue_head);
struct list_head *tmp;
struct task_struct *p;
list_for_each(tmp, &runqueue_head) {
p = list_entry(tmp, struct task_struct, run_list);
if (can_schedule(p)) {
int weight = goodness(p, this_cpu, prev->active_mm);
if (weight > c)
c = weight, next = p;
}
}
从这段代码可以剖析出list_entry(ptr,type,member)宏及参数的涵义:ptr是
指向list_head类型数组的表针,type为一个结构,而member为结构type中的一个域,
类型为list_head,这个宏返回指向type结构的表针。在内核代码中大量引用了这个宏,因
此,认清楚这个宏的涵义和用法极其重要。
另外,对list_head类型的链表进行删除和插入(头或尾)的宏为
list_del()/list_add()/list_add_tail(),在内核的其他函数中可以调用那些宏。比如,
从运行队列中删掉、增加及联通一个任务的代码如下:
static inline void del_from_runqueue(struct task_struct * p)
{
nr_running--;
list_del(&p->run_list);
p->run_list.next = NULL;
}
static inline void add_to_runqueue(struct task_struct * p)
{
list_add(&p->run_list, &runqueue_head);
nr_running++;
}
static inline void move_last_runqueue(struct task_struct * p)
{
list_del(&p->run_list);
list_add_tail(&p->run_list, &runqueue_head);
}
static inline void move_first_runqueue(struct task_struct * p)
{
list_del(&p->run_list);
list_add(&p->run_list, &runqueue_head);
}
2.等待队列
运行队列数组把处于TASK_RUNNING状态的所有进程组织在一起。当要把其他状态的进
程分组时,不同的状态要求不同的处理,Linux选择了下述形式之一。
•TASK_STOPPED或TASK_ZOMBIE状态的进程不链接在专门的数组中,也没必要把它们
分组,由于父进程可以通过进程的PID或进程间的亲属关系检索到子进程。
•把TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE状态的进程再分成好多类linux内核4.17.2,其每
一类对应一个特定的风波。在这些情况下,进程状态提供的信息满足不了快速检索进程,因
此,有必要引入另外的进程数组。这种数组叫等待队列。
等待队列在内核中有好多用途,尤其对中断处理、进程同步及定时好处更大。由于这种
内容在之后的章节中讨论,我们只在这儿说明,进程必须时常等待个别风波的发生,比如,
等待一个c盘操作的中止,等待释放系统资源或等待时间走过固定的间隔。等待队列实现在
风波上的条件等待,也就是说,希望等待特定风波的进程把自己放进合适的等待队列,并放
弃控制权。因而,等待队列表示一组睡眠的进程,当某一条件变为真时,由内核唤起它们。
等待队列由循环数组实现。在2.4版中,关于等待队列的定义如下(为了描述便捷,有所简
化):
struct __wait_queue {
unsigned int flags;
struct task_struct * task;
struct list_head task_list;
} ;
typedefstruct__wait_queuewait_queue_t;
另外,关于等待队列另一个重要的数据结构—等待队列首部的描述如下:
struct __wait_queue_head {
wq_lock_t lock;
struct list_head task_list;
};
typedefstruct__wait_queue_headwait_queue_head_t;
在这两个数据结构的定义中,都涉及到类型为list_head的数组,这与2.2版定义是不
同的,在2.2版中的定义为:
struct wait_queue {
struct task_struct * task;
struct wait_queue * next ;
} ;
typedef struct wait_queue wait_queue_t ;
typedef struct wait_queue *wait_queue_head_t ;
这儿要非常指出的是,2.4版中对等待队列的操作函数和宏比2.2版丰富了,而在你编
写设备驱动程序时会用到这种函数和宏,因而,要注意2.2到2.4函数的移植问题。下边给
出2.4版中的一些主要函数及其功能:
•init_waitqueue_head()—对等待队列首部进行初始化
•init_waitqueue_entry()-对要加入等待队列的元素进行初始化
•waitqueue_active()—判断等待队列中早已没有等待的进程
•add_wait_queue()—给等待队列中降低一个元素
•remove_wait_queue()—从等待队列中删掉一个元素
注意,在以上函数的实现中,都调用了对list_head类型链表的操作函数
(list_del()/list_add()/list_add_tail()),因而可以说,list_head类型相当于C++中
的基类型,这也是对2.2版的极大改进。
希望等待一个特定风波的进程能调用下述函数中的任一个:
sleep_on()函数对当前的进程起作用,我们把当前进程称作P:
sleep_on(wait_queue_head_t*q)
SLEEP_ON_VAR/*宏定义,拿来初始化要插入到等待队列中的元素*/
current->state=TASK_UNINTERRUPTIBLE;
SLEEP_ON_HEAD/*宏定义,把P插入到等待队列*/
schedule();
SLEEP_ON_TAIL/*宏定义把P从等待队列中删掉*/
这个函数把P的状态设置为TASK_UNINTERRUPTIBLE,并把P插入等待队列。之后,它调
用调度程序恢复另一个程序的执行。当P被唤起时,调度程序恢复sleep_on()函数的执行,
把P从等待队列中删掉。
•interruptible_sleep_on()与sleep_on()函数是一样的,但稍有不同,后者把
进程P的状态设置为TASK_INTERRUPTIBLE而不是TASK_UNINTERRUPTIBLE,因而,通过接受
一个讯号可以唤起P。
•sleep_on_timeout()和interruptible_sleep_on_timeout()与上面情况类似,
但它们容许调用者定义一个时间间隔,过了这个间隔之后,内核唤起进程。为了做到这点,
它们调用schedule_timeout()函数而不是schedule()函数。
借助wake_up或则wake_up_interruptible宏,让插入等待队列中的进程步入
TASK_RUNNING状态,这两个宏最终都调用了try_to_wake_up()函数:
static inline int try_to_wake_up(struct task_struct * p, int synchronous)
{
unsigned long flags;
int success = 0;
spin_lock_irqsave(&runqueue_lock, flags); /*加锁*/
p->state = TASK_RUNNING;
if (task_on_runqueue(p)) /*判断 p 是否已经在运行队列*/
goto out;
add_to_runqueue(p); /*不在,则把 p 插入到运行队列*/
if (!synchronous || !(p->cpus_allowed & (1 << smp_processor_id()))) /
reschedule_idle(p);
success = 1;
out:
spin_unlock_irqrestore(&runqueue_lock, flags); /*开锁*/
return success;
}
在这个函数中,p为要唤起的进程。假如p不在运行队列中,则把它装入运行队列。如
果重新调度正在进行的过程中,则调用reschedule_idle()函数,这个函数决定进程p是
否应当占据某一CPU上的当前进程(参见下一章)。
实际上,在内核的其他部份,最常用的还是wake_up或则wake_up_interruptible宏,
也就是说,假如你要在内核级进行编程,只需调用其中的一个宏。比如一个简单的实时时钟
(RTC)中断程序如下:
static DECLARE_WAIT_QUEUE_HEAD(rtc_wait); /*初始化等待队列首部*/
void rtc_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
spin_lock(&rtc_lock);
rtc_irq_data = CMOS_READ(RTC_INTR_FLAGS);
spin_unlock(&rtc_lock);
wake_up_interruptible(&rtc_wait);
}
这个中断处理程序通过从实时时钟的I/O端口(CMOS_READ宏形成一对outb/inb)读取
数据,之后唤起在rtc_wait等待队列上睡眠的任务。
4.6内核线程
内核线程(thread)或叫守护进程(daemon),在操作系统中抢占相当大的比列,当Linux
操作系统启动之后,尤其是Xwindow也启动之后,你可以用“ps”命令查看系统中的进程,
这时会发觉好多以“d”结尾的进程名,这种进程就是内核线程。
内核线程也可以叫内核任务,它们周期性地执行,比如,c盘高速缓存的刷新,网路连
接的维护,页面的换入换出等。在Linux中,内核线程与普通进程有一些本质的区别,从以
下几个方面可以看出两者之间的差别。
•内核线程执行的是内核中的函数,而普通进程只有通过系统调用能够执行内核中的函
数。
•内核线程只运行在内核态,而普通进程既可以运行在用户态,也可以运行在内核
态。
•由于内核线程指只运行在内核态,因而,它只能使用小于PAGE_OFFSET(3G)的地址
空间。另一方面,不管在用户态还是内核态,普通进程可以使用4GB的地址空间。
内核线程是由kernel_thread()函数在内核态下创建的,这个函数所包含的代码大部
分是内联式汇编语言,但在某种程度上等价于下边的代码:
int kernel_thread(int (*fn)(void *), void * arg,
unsigned long flags)
{
pid_t p ;
p = clone( 0, flags | CLONE_VM );
if ( p ) /* parent */
return p;
else { /* child */
fn(arg) ;
exit( ) ;
}
}
系统中大部份的内核线程是在系统的启动过程中构建的,其相关内容将在启动系统一章
进行介绍。
4.7进程的权能
Linux用“权能(capability)”表示一进程所具有的权利。一种权能仅仅是一个标志,
它表明是否容许进程执行一个特定的操作或一组特定的操作。这个模型不同于传统的“超级
用户对普通用户”模型,在后一种模型中,一个进程要么能做任何事情,要么哪些也不能做,
这取决于它的有效UID。也就是说,超级用户与普通用户的界定过分扼要。如表4.13给出了
在Linux内核中已定义的权能。
任何时侯,每位进程只须要有限种权能,这是其主要优势。因而,虽然一位有恶意的用
户使用有潜在错误程序,他也只能非法地执行有限个操作类型。
比如,假设一个有潜在错误的程序只有CAP_SYS_TIME权能。在这些情况下,借助其错
误的恶意用户只能在非法地改变实时时钟和系统时钟方面获得成功。他并不能执行其他任何
特权的操作。
4.8内核同步
内核中的好多操作在执行的过程中都不容许遭到打搅,最典型的事例就是对队列的操
作。假如两个进程都要将一个数据结构链入到同一个队列的尾部,要是在第1个进程完成了
一半的时侯发生了调度,让第2个进程插了进来,就可能导致混乱。类似的干扰可能来自某
个中断服务程序或bh函数。在多处理机SMP结构的系统中,这些干扰还有可能来自另一个处
理器。这些干扰本质上表项为对临界资源(如队列)的互斥使用。下边介绍几种防止这些干
扰的同步方法。
4.8.1讯号量
进程间对共享资源的互斥访问是通过“信号量”机制来实现的。讯号量机制是操作系统
教科书中比较重要的内容之一。Linux内核中提供了两个函数down()和up(),分别对应于
操作系统教科书中的P、V操作。
讯号量在内核中定义为semaphore数据结构,坐落include/i386/semaphore.h:
struct semaphore {
atomic_t count;
int sleepers;
wait_queue_head_t wait;
#if WAITQUEUE_DEBUG
long __magic;
#endif
};
其中的count域就是“信号量”中的那种“量”,它代表着可用资源的数目。假如该值
小于0,这么资源就是空闲的,也就是说,该资源可以使用。相反,假如count大于0雨林木风linux,这么
这个讯号量就是忙碌的,也就是说,这个受保护的资源现今不能使用。在后一种情况下,count
的绝对值表示了正在等待这个资源的进程数。该值为0表示有一个进程正在使用这个资源,
但没有其他进程在等待这个资源。
Wait域储存等待数组的地址,该数组中包含正在等待这个资源的所有睡眠的进程。其实,
假如count小于或等于0,则等待队列为空。为了明晰表示等待队列中正在等待的进程数,
引入了计数器sleepers。
down()和up()函数主要应用在文件系统和驱动程序中,把要保护的临界区置于这两个函
数中间,用法如下:
down();
临界区
up();
这两个函数是用嵌入式汇编实现的,十分麻烦,在此不予详尽介绍。
4.8.2原子操作
防止干扰的最简单方式就是保证操作的原子性,即操作必须在一条单独的指令内执行。
有两种类型的原子操作,废黜图操作和物理的加减操作。
1.位图操作
在内核的好多地方用到位图,比如显存管理中对空闲页的管理,位图还有一个广泛的用
途就是简单的加锁,比如提供对打开设备的互斥访问。关于位图的操作函数如下:
以下函数的参数中,addr指向位图。
•voidset_bit(intnr,volatilevoid*addr):设置位图的第nr位。
•voidclear_bit(intnr,volatilevoid*addr):清位图的第nr位。
•voidchange_bit(intnr,volatilevoid*addr):改变位图的第nr位。
•inttest_and_set_bit(intnr,volatilevoid*addr):设置第nr位,并返回该位
原先的值,且两个操作是原子操作,不可分割。
•inttest_and_clear_bit(intnr,volatilevoid*addr):清第nr为,并返回该位
原先的值,且两个操作是原子操作。
•inttest_and_change_bit(intnr,volatilevoid*addr):改变第nr位,并返回
该位原先的值,且这两个操作是原子操作。
这种操作借助了LOCK_PREFIX宏,对于SMP内核,该宏是总线锁指令的前缀,对于单CPU
这个宏不起任何作用。这就保证了在SMP环境下访问的原子性。
2.算术操作
有时侯位操作是不便捷的,取而代之的是须要执行算术操作,即加、减操作及加1、减
1操作。典型的事例是好多数据结构中的引用计数域count(如inode结构)。这种操作的原
子性是由atomic_t数据类型和表4.14中的函数保证的。atomic_t的类型在include/
i386/atomic.h,定义如下:
typedefstruct{volatileintcounter;}atomic_t;
4.8.3载流子锁、读写载流子锁和大读者载流子锁
在Linux开发的初期,开发者就面临这样的问题,即不同类型的上下文(用户进程对中
断)怎样访问共享的数据,以及怎样访问来自多个CPU同一上下文的不同实例。
在Linux内核中,临界区的代码或则是由进程上下文来执行,或则是由中断上下文来执
行。在单CPU上,可以用cli/sti指令来保护临界区的使用,比如:
unsigned long flags;
save_flags(flags);
cli();
/* critical code */
restore_flags(flags);
然而,在SMP上,这些方式显著是没有用的,由于同一段代码序列可能由另一个进程同
时执行,而cli()仅能单独地为每位CPU上的中断上下文提供对竞争资源的保护,它未能对运
行在不同CPU上的上下文提供对竞争资源的访问。为此,必须用到载流子锁。
所谓载流子锁,就是当一个进程发觉锁被另一个进程锁着时,它就不停地“旋转”,不断
执行一个指令的循环直至锁打开。载流子锁只对SMP有用,对单CPU没有意义。
有3种类型的载流子锁:基本的、读写以及大读者载流子锁。读写载流子锁适用于“多个读者
少数写者”的场合,比如,有多个读者仅有一个写者,或则没有读者只有一个写者。大读者
载流子锁是读写载流子锁的一种,但更照料读者。大读者载流子锁现今主要用在Sparc64和网路系
统中。
本章对进程进行了全面描述,但并不是对每位部份都进行了深入描述,由于在整个操作
系统中,进程处于核心位置,因而,内核的其他部份(如文件、内存、进程间的通讯等)都
与进程有密切的联系,相关内容只能在后续的章节中涉及到。通过本章的介绍,读者应当对
进程有一个全方位的认识:
(1)进程是由正文段(Text)、用户数据段(UserSegment)以及系统数据段(System
Segment)共同组成的一个执行环境。
(2)Linux中用task_struct结构来描述进程,也就是说,有关进程的所有信息都储存
在这个数据结构中,或则说,Linux中的进程与task_struct结构是同意词,在中文描述中,
有时把进程(Process)和线程(Thread)混在一起使用,但并不是说,进程与线程有同样的
涵义,只不过描述线程的数据结构也是task_struct。task_struct就是通常教科书上所讲的
进程控制块.。
(3)本章对task_struct结构中储存的信息进行了分类讨论,但并不要求在此能把握所
有的内容,相对独立的内容为进程的状态,在此再度给出概述。
•TASK_RUNNING:也就是一般所说的就绪(Ready)状态。
•TASK_INTERRUPTIBLE:等待一个讯号或一个资源(睡眠状态)。
•TASK_UNINTERRUPTIBLE:等待一个资源(睡眠状态),处于某个等待队列中。
•TASK_ZOMBIE:没有父进程的子进程。
•TASK_STOPPED:正在被调试的任务。
(4)task_struct结构与内核栈储存在一起,占8KB的空间。
(5)当前进程就是在某个CPU上正在运行的进程,Linux中用宏current来描述,也可
以把curennt当成一个全局变量来用。
(6)为了把内核中的所有进程组织上去,Linux提供了几种组织形式,其中哈希表和双
向循环数组方法是针对系统中的所有进程(包括内核线程),而运行队列和等待队列是把处
于同一状态的进程组织上去。
(7)Linux2.4中引入一种通用数组list_head,这是面向对象思想在C中的具体实现,
在内核中其他使用数组的地方都引用了这些基类型。
(8)进程的权能和内核的同步我们仅仅做了简单介绍,由于进程管理会涉及到那些内容,
但它们不是进程管理的核心内容,引入这种内容仅仅是为了让读者在阅读源代码时扫除一些
障碍。
(9)进程的创建及执行将在第六章的最后一节进行讨论。