深入分析Linux内核源代码4-进程描述(2)

每天十五分钟,熟读一个技术点,水滴石穿,一切只为渴望更优秀的你!

————零声学院

4.4 task_struct 结构在内存中的存放

task_struct 结构在内存的存放与内核栈是分不开的,因此,首先讨论内核栈。

4.4.1 进程内核栈
每个进程都有自己的内核栈。当进程从用户态进入内核态时,CPU 就自动地设置该进程
的内核栈,也就是说,CPU 从任务状态段 TSS 中装入内核栈指针 esp(参见下一章的进程切换
一节)。
X86 内核栈的分布如图 4.2 所示。
在 Intel 系统中,栈起始于末端,并朝这个内存区开始的方向增长。从用户态刚切换到
内核态以后,进程的内核栈总是空的,因此,esp 寄存器直接指向这个内存区的顶端在图 4.2
中,从用户态切换到内核态后,esp 寄存器包含的地址为 0x018fc00。进程描述符存放在从
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 = (struct task_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 中定义如下:
#define PIDHASH_SZ (4096 >> 2)
extern struct task_struct *pidhash[PIDHASH_SZ];
#define pid_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():
#define for_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 才真正指向了当前运行进程;空进程是个比较特殊的进程,只有
系统中没有进程可运行时它才会被执行,Linux 将它看作运行队列的头,当调度程序遍历运
行队列,是从 idle_task 开始、至 idle_task 结束的,在调度程序运行过程中,允许队列中
加入新出现的可运行进程,新出现的可运行进程插入到队尾,这样的好处是不会影响到调度
程序所要遍历的队列成员,可见,idle_task 是运行队列很重要的标志。
另一个重要标志是队列长度,也就是系统中处于可运行状态(TASK_RUNNING)的进程数
目,用全局整型变量 nr_running 表示,在/kernel/fork.c 中定义如下:
int nr_running=1;
若 nr_running 为 0,就表示队列中只有空进程。在这里要说明一下:若 nr_running 为
0,则系统中的当前进程和空进程就是同一个进程。但是 Linux 会充分利用 CPU 而尽量避免出
现这种情况。

4.5.5 等待队列
在 2.4 版本中,引入了一种特殊的链表—通用双向链表,它是内核中实现其他链表的
基础,也是面向对象的思想在 C 语言中的应用。在等待队列的实现中多次涉及与此链表相关
的内容。
1.通用双向链表
在 include/linux/list.h 中定义了这种链表:

struct list_head { 
 struct list_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 状态的进程再分成很多类,其每
一类对应一个特定的事件。在这种情况下,进程状态提供的信息满足不了快速检索进程,因
此,有必要引入另外的进程链表。这些链表叫等待队列。
等待队列在内核中有很多用途,尤其对中断处理、进程同步及定时用处更大。因为这些
内容在以后的章节中讨论,我们只在这里说明,进程必须经常等待某些事件的发生,例如,
等待一个磁盘操作的终止,等待释放系统资源或等待时间走过固定的间隔。等待队列实现在
事件上的条件等待,也就是说,希望等待特定事件的进程把自己放进合适的等待队列,并放
弃控制权。因此,等待队列表示一组睡眠的进程,当某一条件变为真时,由内核唤醒它们。
等待队列由循环链表实现。在 2.4 版中,关于等待队列的定义如下(为了描述方便,有所简
化):

struct __wait_queue { 
 unsigned int flags; 
 struct task_struct * task; 
 struct list_head task_list; 
} ; 

typedef struct __wait_queue wait_queue_t ;
另外,关于等待队列另一个重要的数据结构—等待队列首部的描述如下:

struct __wait_queue_head { 
 wq_lock_t lock; 
 struct list_head task_list; 
}; 

typedef struct __wait_queue_head wait_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”结尾的进程名,这些进程就是内核线程。
内核线程也可以叫内核任务,它们周期性地执行,例如,磁盘高速缓存的刷新,网络连
接的维护,页面的换入换出等。在 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,那么
这个信号量就是繁忙的,也就是说,这个受保护的资源现在不能使用。在后一种情况下,count
的绝对值表示了正在等待这个资源的进程数。该值为 0 表示有一个进程正在使用这个资源,
但没有其他进程在等待这个资源。
Wait 域存放等待链表的地址,该链表中包含正在等待这个资源的所有睡眠的进程。当然,
如果 count 大于或等于 0,则等待队列为空。为了明确表示等待队列中正在等待的进程数,
引入了计数器 sleepers。
down()和 up()函数主要应用在文件系统和驱动程序中,把要保护的临界区放在这两个函
数中间,用法如下:
down();
临界区
up();
这两个函数是用嵌入式汇编实现的,非常麻烦,在此不予详细介绍。

4.8.2 原子操作
避免干扰的最简单方法就是保证操作的原子性,即操作必须在一条单独的指令内执行。
有两种类型的原子操作,即位图操作和数学的加减操作。
1.位图操作
在内核的很多地方用到位图,例如内存管理中对空闲页的管理,位图还有一个广泛的用
途就是简单的加锁,例如提供对打开设备的互斥访问。关于位图的操作函数如下:
以下函数的参数中,addr 指向位图。
• void set_bit(int nr, volatile void *addr):设置位图的第 nr 位。
• void clear_bit(int nr, volatile void *addr): 清位图的第 nr 位。
• void change_bit(int nr, volatile void *addr): 改变位图的第 nr 位。
• int test_and_set_bit(int nr, volatile void *addr): 设置第 nr 位,并返回该位
原来的值,且两个操作是原子操作,不可分割。
• int test_and_clear_bit(int nr, volatile void *addr): 清第 nr 为,并返回该位
原来的值,且两个操作是原子操作。
• int test_and_change_bit(int nr, volatile void *addr):改变第 nr 位,并返回
该位原来的值,且这两个操作是原子操作。
这些操作利用了 LOCK_PREFIX 宏,对于 SMP 内核,该宏是总线锁指令的前缀,对于单 CPU
这个宏不起任何作用。这就保证了在 SMP 环境下访问的原子性。
2.算术操作
有时候位操作是不方便的,取而代之的是需要执行算术操作,即加、减操作及加 1、减
1 操作。典型的例子是很多数据结构中的引用计数域 count(如 inode 结构)。这些操作的原
子性是由 atomic_t 数据类型和表 4.14 中的函数保证的。atomic_t 的类型在 include/
i386/atomic.h,定义如下:
typedef struct { volatile int counter; } 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)、用户数据段(User Segment)以及系统数据段(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)Linux 2.4 中引入一种通用链表 list_head,这是面向对象思想在 C 中的具体实现,
在内核中其他使用链表的地方都引用了这种基类型。
(8)进程的权能和内核的同步我们仅仅做了简单介绍,因为进程管理会涉及到这些内容,
但它们不是进程管理的核心内容,引入这些内容仅仅是为了让读者在阅读源代码时扫除一些
障碍。
(9)进程的创建及执行将在第六章的最后一节进行讨论。

每日分享15分钟技术摘要选读,关注一波,一起保持学习动力!

在这里插入图片描述

第一章 走进linux 1.1 GNU与Linux的成长 1.2 Linux的开发模式和运作机制 1.3走进Linux内核 1.4 分析Linux内核的意义 1.5 Linux内核结构 1.6 Linux内核源代码 1.7 Linux内核源代码分析工具 第二章 Linux运行的硬件基础 2.1 i386的寄存器 2.2 内存地址 2.3 段机制和描述符 2.4 分页机制 2.5 Linux中的分页机制 2.6 Linux中的汇编语言 第三章中断机制 3.1 中断基本知识 3.2中断描述符表的初始化 3.3异常处理 3.4 中断处理 3.5中断的后半部分处理机制 第四章 进程描述 4.1 进程和程序(Process and Program) 4.2 Linux中的进程概述 4.3 task_struct结构描述 4.4 task_struct结构在内存中的存放 4.5 进程组织的方式 4.6 内核线程 4.7 进程的权能 4.8 内核同步 第五章进程调度 5.1 Linux时间系统 5.2 时钟中断 5.3 Linux的调度程序-Schedule( ) 5.4 进程切换 第六章 Linux内存管理 6.1 Linux的内存管理概述 6.2 Linux内存管理的初始化 6.3 内存的分配和回收 6.4 地址映射机制 6.5 请页机制 6.6 交换机制 6.7 缓存和刷新机制 6.8 进程的创建和执行 第七章 进程间通信 7.1 管道 7.2 信号(signal) 7.3 System V 的IPC机制 第八章 虚拟文件系统 8.1 概述 8.2 VFS中的数据结构 8.3 高速缓存 8.4 文件系统的注册、安装与拆卸 8.5 限额机制 8.6 具体文件系统举例 8.7 文件系统的系统调用 8 .8 Linux2.4文件系统的移植问题 第九章 Ext2文件系统 9.1 基本概念 9.2 Ext2的磁盘布局和数据结构 9.3 文件的访问权限和安全 9.4 链接文件 9.5 分配策略 第十章 模块机制 10.1 概述 10.2 实现机制 10.3 模块的装入和卸载 10.4 内核版本 10.5 编写内核模块 第十一章 设备驱动程序 11.1 概述 11.2 设备驱动基础 11.3 块设备驱动程序 11.4 字符设备驱动程序 第十二章 网络 12.1 概述 12.2 网络协议 12.3 套接字(socket) 12.4 套接字缓冲区(sk_buff) 12.5 网络设备接口 第十三章 启动系统 13.1 初始化流程 13.2 初始化的任务 13.3 Linux 的Boot Loarder 13.4 进入操作系统 13.5 main.c中的初始化 13.6 建立init进程 附录: 1 Linux 2.4内核API 2.1 驱动程序的基本函数 2.2 双向循环链表的操作 2.3 基本C库函数 2.4 Linux内存管理中Slab缓冲区 2.5 Linux中的VFS 2.6 Linux的连网 2.7 网络设备支持 2.8 模块支持 2.9 硬件接口 2.10 块设备 2.11 USB 设备
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值