您的当前位置:首页正文

操作系统学习笔记【P18】—— 信号量的代码实现

2024-11-30 来源:个人技术集锦


阅读建议:反复看目录,厘清程序功能被分为哪几个部分。

从纸上到实际程序

sys_sem_open的实现

以下代码基于Linux0.11

sem.c //进入内核 
typedef struct {//一个结构体数组,用来记录所有的信号量
	char name [20];//信号量名称 如empty
	int value;	   //信号量的值
	task_struct * queue;  //此信号量的相关 被阻塞的进程队列
}semtable[20];
	
sys_sem_open (char *name){
	在semtable中寻找name对上的;
	没找到则创建;
	返回对应的下标;
}

sys_sem_wait的实现

sys_sem_wait(int sd){
	//(配套实验环境里只有单CPU,可通过这种方式实现互斥访问)
	cli(); //通过关中断 实现对信号量的原子操作
	//根据前面传入的下标,找到对应信号量	
	if(semtable[sd].value-- < 0) {//执行P操作的-1 然后判断是否小于0
		设置自己为阻塞;//修改进程PCB中的current的值,可参考进程调度代码里,是如何修改进程状态的
		将自己加入semtable[sd].queue中;
		schedule(); //切换其他进程 
	}
	sti(); 
}

Linux0.11内部 进程同步实例 —— 读磁盘块

同步案例:进程启动磁盘读以后睡眠,等待磁盘读完由磁盘中断将其唤醒。
到磁盘上读一个磁盘块的过程:在内存中申请一段BUFFER,通过DMA技术将磁盘中的内容读到BUFFER里,CPU直接从BUFFER里读内容。所以这个buffer只能被此进程读,是互斥访问的,需要锁住。

1.启动磁盘读 执行过程分析

启动磁盘读的程序流:用户态read——内核态sys_read——bread() 【下面仅解释bread()代码】

struct buffer_head {//忽略了buffer_head 结构的某些字段
    struct buffer_head *b_next;     /* 用于快速查找数据块缓存 */
    unsigned long b_blocknr;        /* 数据块号 */
    unsigned short b_size;          /* 数据块大小 */
    unsigned short b_list;          /* List that this buffer appears */
    kdev_t b_dev;                   /* 数据块所属设备 */
 
    atomic_t b_count;               /* 引用计数器 */
    kdev_t b_rdev;                  /* 数据块所属真正设备 */
    ...
};

bread (int dev ,int block) {
	//到磁盘上读一个磁盘块的过程:在内存中申请一段BUFFER,通过DMA技术将磁盘中的内容读到BUFFER里,CPU直接从BUFFER里读内容
	struct buffer_head * bh;//申请一段空闲缓冲区
	ll_rw_block(READ,bh);//请求执行操作: 在磁盘里读bh记录的磁盘块 到 缓冲区
	//读磁盘时,CPU会阻塞在缓冲区这里,等待将磁盘内容读到缓冲区
	wait_on_buffer(bh);
}

2.buffer互斥访问 实现技术分析

在本节第一段就说过,为什么要对缓冲区加锁:一个进程请求 读磁盘到缓冲区的过程中 其他进程不能对此缓冲区进行操作 需要对缓冲区加锁。主要由lock_buffer()实现。

//b_lock是bh中的定义的一个信号量 各程序要根据b_lock的值判断能否操作缓冲区
lock_buffer(buffer_head*bh){
	cli ();
	while (bh->b_lock)//如果等于1 就是被其他进程锁上了,本进程需要睡眠
		sleep_on(&bh->b_wait);
	bh->b_lock = 1;//1表示上锁 上锁就表示没读完 读完后中断后会修改其值进行解锁
	sti(); 
}

从代码里看到这个b_lock信号量的实现就很特殊,b_lock的值一直为1,没有往下减。那这是怎么实现信号量机制的呢?
首先,这个实现机制跟正常的流程是一样的:
1、用开关中断指令保证互斥访问,(正常的是:原子指令实现的信号量mutex 来实现互斥访问)
2、while根据信号量的值判断是否要让进程睡眠(sleep_on),不睡眠则修改信号的值,(正常的是:原子操作PV对信号量+/-,然后判断是否让进程睡眠)
具体怎么运作的:要从sleep_on说起:

sleep_on主要功能是 将当前进程睡眠(阻塞)【就是PV操作里,if(…)将进程加入阻塞队列】,然后切换到下一个进程
以下代码也是分为 功能明显的三节:
1将当前进程放入阻塞队列
2设置进程状态为阻塞态并切到别的进程
3切回本进程时,唤醒队列里的下一个进程

void sleep_on (struct task_struct **p){
	struct task _struct *tmp ;
	tmp = *p;
	*p = current;
	//以上三行 “将当前进程放入阻塞队列”
	
	current->state = TASK_UNINTERRUPTIBLE; //设置进程状态为阻塞态
	schedule ();//切到别的进程
	
	//以下两句,唤醒队列里的所有进程,队首进程由读磁盘完成后的磁盘中断唤醒,队首的sleep_on唤醒前一个,前一个继续往后唤醒
	//如果有下一个进程就唤醒这个进程(state=0表示唤醒进程) 这个被唤醒的进程从哪里执行? 它schedule()的后一行,就是被唤醒的进程,又会去唤醒它的下一个进程,这样一个唤醒一个就唤醒了所有
	if (tmp) 
		tmp->state=0;  
	//会把队列里所有的进程都唤醒 为什么要唤醒所有呢?都唤醒后,选择优先级最高的一个执行,此进程会将b_lock又设为1 (由进程调度的代码来决定谁执行,这里老师没讲具体代码)。所以必须用while包住sleep_on,其他优先级低的进程sleep_on执行完后又判断while的条件,然后再次睡眠。
	//这种实现机制,信号量也不会为负,因为会把阻塞进程都唤醒,不需要记住有多少个
	
}
2.1 下面解释 最开始的三行代码是怎么实现 加入队列的 (最隐蔽的队列)

类似链表的头插法
1、新建tmp 保存当前队头
2、队头*p 指向 将要插入的 current
3、current的指针 指向 之前用tmp保存起来的队头 这样新加的元素就和之前队列连在一起了
很巧妙的是,第3步没有用指针,而是通过函数栈帧实现。tmp是sleep_on函数里新建的局部变量,存在当前进程的sleep_on函数的栈帧里。当前进程在schedule()被切出去,切回来后执行if (tmp),这个tmp就是切出去之前保存在栈帧里的tmp,指向的是之前的队头。

2.2 下面解释 最后的二行代码是怎么实现 唤醒阻塞队列里所有的进程

为什么唤醒阻塞队列里所有的进程呢?
if机制是唤醒队首第一个,这样的话,相当于谁来得晚谁就优先级高了,现实中的优先级可能会有其他情况。
while机制将进程都唤醒后,由进程调度的代码来选择优先级最高的一个执行。
这种实现机制,信号量也不会为负,因为会把阻塞进程都唤醒,所有不需要记住有多少个。

总述: 队首进程由读磁盘完成后的磁盘中断唤醒。(下一小节讲)
队首进程里的sleep_on唤醒前一个,前一个继续往前唤醒,这样一个唤醒一个就唤醒了所有。
具体怎么唤醒的:
队首进程被唤醒后从哪里执行? 它 schedule()的后一行if (tmp),
tmp是什么呢? 在sleep_on最开始的三行代码里,解释了tmp是指向的是之前的队头,也就是当前进程被阻塞前的已经被阻塞的一个进程。
如果tmp存在就tmp->state=0;(state=0表示唤醒进程) 就是唤醒这个进程,
这个被唤醒的进程又会从它 schedule()的后一行if (tmp)开始执行,继续去唤醒之前被阻塞的进程。
这样一个唤醒一个就唤醒了所有。

3.读磁盘完成后 磁盘中断唤醒进程 过程分析

这也是在回答 上个小节提到的阻塞进程队列的队首是怎么唤醒的?
由磁盘中断引起四个函数的调用执行,最后的wake_up()函数将队首唤醒。(state=0表示唤醒进程)
磁盘中断唤醒进程的程序流:read_intr() ——end_request() —— unlock_buffer() —— wake_up()

//磁盘中断
static void read_intr(void){
	...
	end_request(1);
}

end_request(int uptodate){
	...
	unlock_buffer(CURRENT->bh);
}

unlock_buffer(struct buffer_head * bh){
	bh->b_lock=0;
	wake_up(&bh->b_wait);
}

wake_up(struct task_struct**p){ 
	if (p && *p) {
		(**p).state=0; //唤醒队首
		*p=NULL;
	}
}
显示全文