第四周:创建进程fork & 进程同步
进程同步
进程同步概述
进程同步是操作系统中用于协调多个进程执行顺序的过程,以确保进程能够有效地共享资源并相互合作。进程同步的目的是实现程序的执行可再现性。
进程间的两种关系
- 直接相互制约关系:指多个进程为完成同一任务而需要进行协调,导致它们之间产生相互等待和信息交换的制约关系。
- 间接相互制约关系:通常是由于资源的有限性,进程之间为争夺资源(如CPU、内存等)而产生的相互制约关系。
保证进程同步的一般方法
-
互斥锁(Mutex):互斥锁是一种最常见的同步机制,用于保护共享资源,使得同一时间只有一个进程或线程可以访问该资源。当一个进程获取到互斥锁时,其他进程需要等待直到该进程释放锁。
-
信号量(Semaphore):信号量是一种计数器,用于控制对共享资源的访问。它可以用来实现进程的互斥和同步。信号量有两种类型:二进制信号量和计数信号量。二进制信号量只能取0或1,用于实现互斥。计数信号量可以取多个非负整数值,用于控制资源的数量。
-
条件变量(Condition Variable):条件变量用于在多个进程或线程之间进行通信和同步。它通常与互斥锁结合使用。进程可以在条件变量上等待某个条件满足,当条件满足时,其他进程可以通过发出信号来唤醒等待的进程。
-
读写锁(Read-Write Lock):读写锁允许多个进程同时读取共享资源,但只允许一个进程进行写操作。这样可以提高并发性能,因为多个进程可以同时读取资源而不会相互干扰,但写操作需要互斥保护。
-
事件(Event):事件是一种同步对象,用于在多个进程之间进行通信和同步。一个事件可以处于"已触发"或"未触发"的状态。进程可以等待事件的触发,当事件触发时,等待的进程将被唤醒。
记录型信号量、AND信号量以及信号量集
- 记录型信号量(Record Semaphores)是一种同步机制,用于在多线程编程中进行线程间的通信和同步。与传统的二进制信号量(Semaphore)不同,记录型信号量可以携带附加的信息。
特点:
- 记录型信号量可以存储额外的数据,例如计数器或状态信息。
- 它可以用于更复杂的同步场景,其中线程需要共享更多的信息。
- 记录型信号量支持更灵活的操作,例如增加或减少计数器的值。
- AND信号量是一种同步原语,用于多线程编程中的同步和互斥操作。AND信号量将多个线程的到达和离开操作组合在一起,以实现更复杂的同步需求。
特点:
- AND信号量可以用于线程间的同步操作,要求所有参与的线程都到达特定点或完成特定操作时才能继续执行。
- 当所有参与的线程都到达特定点时,AND信号量被释放,允许线程继续执行。
- 当所有参与的线程都完成特定操作时,AND信号量再次被释放,允许其他线程继续执行。
- 信号量集是一组相关的信号量的集合,用于实现更复杂的同步和互斥操作。它可以包含多个信号量,并提供对这些信号量的集合操作。
特点:
- 信号量集可以用于管理多个相关的信号量,使得对它们的操作更便捷和一致。
- 它提供了对信号量集合的原子操作,以确保线程安全性和同步性。
- 信号量集可以支持更高级的同步需求,例如多个资源的申请和释放,或者对多个互斥区域的管理。
三者运作方式
记录型信号量(Record Semaphores)是一种信号量的扩展形式,用于实现更复杂的同步机制。与传统的二进制信号量或计数信号量不同,记录型信号量可以存储更多的信息,并支持更丰富的同步操作。
记录型信号量可以用于实现进程同步的一般方法,包括:
-
互斥锁(Mutex):记录型信号量可以用作互斥锁,通过设置信号量的值为1或0来表示锁的状态。当一个进程获得互斥锁时,信号量的值为1,其他进程需要等待直到该进程释放锁,将信号量的值设置为0。
-
条件变量(Condition Variable):记录型信号量可以用作条件变量,用于等待和通知特定的条件。进程可以通过等待记录型信号量的值为特定值来等待条件的满足,而其他进程可以通过修改记录型信号量的值来通知等待的进程条件已满足。
AND信号量(AND Semaphores)是一种多个信号量的组合,其中所有的信号量都需要满足特定条件才能通过。它可以用于实现更为复杂的同步要求。例如,可以使用AND信号量来实现多个资源同时就绪时才允许进程执行的同步操作。
信号量集(Semaphore Sets)是一组关联的信号量,可以进行集合操作。通过信号量集,可以对多个信号量进行统一管理和操作。进程可以对整个信号量集进行等待、通知和修改操作,从而实现更灵活的进程同步。
第五周:进程控制和线程
什么是进程控制
进程控制的主要功能是对系统中所有进程实施有效的管理,它具有创建新进程,撤销已有进程,实现进程状态转换等功能。
简化理解:就是要实现进程状态转换。
进程控制通过什么实现
用“原语”实现(原语的执行不可分割)
为什么进程控制需要一气呵成?
如果步骤1,2被中断会导致数据和状态的不同。
eg:完成第一步后收到中断信号,此时state转变但是队列不变,导致信息不匹配。
进程创建、终止、阻塞、唤醒、挂起、激活,这些控制操作,操作系统到底做了什么?
- 创建
- 操作系统初始化,建立起一些常驻内存的系统进程
- 使用创建原语,用于创建非常驻内存系统进程和用户进程
创建一个空白PCB,分配一个唯一的进程标识符;
为新进程的程序和数据分配内存空间
初始化进程控制块:初始化标识符信息,填入处理机的状态信息(指令指针,栈指针)和控制信息(状态,优先级…);
设置相应的链接。如:把新进程加到就绪队列的链表中。
- 阻塞
主动调用阻塞原语(block)将自己阻塞。
保存CPU现场到PCB中
状态由运行态改为阻塞态
插入到事件阻塞队列中
进程调度程序选择一个就绪程序,修改状态,恢复CPU现场,投入运行。
- 唤醒
当被阻塞进程期待事件到来时,调用唤醒原语(wakeup)唤醒进程。
- 触发事件:
硬件中断,其它进程发信息- 阻塞进程从阻塞队列移出;
- 状态由阻塞态改为就绪态
- 插入到就绪队列中
- 挂起
进程将自己挂起或父进程将子进程挂起时,调用挂起原语(suspend)挂起
- 活动就绪态-》静止就绪态;
- 活动阻塞态-》静止阻塞态;
- 运行态-》静止就绪。并除法重新调度。
活动就绪态表示进程已经准备好并具备运行的条件,正在等待CPU的执行权。静止就绪态表示进程满足了运行条件,但由于某些原因暂时无法被调度执行,需要等待特定条件满足后才能转为活动就绪态。
- 中止
- 正常结束:完成时间片后结束。
- 异常结束:发生错误。内存越界,算术运算错等。
- 外界干预:操作系统干预,父进程请求,父进程中止。
进程中止时机发生终止进程的事件,OS调用撤销原语
- 激活
将进程的状态由挂起态改为就绪态。
将进程插入到就绪队列中,参与调度。
什么是线程?特征是?
线程是一个基本的CPU执行单元,也是程序执行流的最小单位。
线程的特征包括:
轻量性:相比于进程,线程的创建、切换和销毁的开销更小,因为线程共享进程的资源。线程的创建和切换比进程快速,因为它们不需要分配新的内存空间。
共享性:线程可以访问和共享进程的资源,包括内存、文件等。多个线程可以在同一个进程中并发执行,共同完成任务。
并发性:多个线程可以同时执行,实现并发处理。线程之间可以并行执行,提高系统的资源利用率和响应性。
独立性:线程拥有自己的程序计数器、栈和局部变量等,每个线程都以独立的方式执行任务。线程之间可以独立调度、同步和通信。
可并行性:多个线程可以在多核处理器上并行执行,充分利用多核计算资源。
共享内存:线程之间共享进程的内存空间,可以通过共享内存进行高效的数据交换和通信。
上下文切换:线程之间的切换需要保存和恢复上下文信息,包括程序计数器、寄存器等。
TCB的概念,以及包含了那些成员
TCB(Thread Control Block)是线程控制块的缩写,也称为线程控制结构或线程描述符。它是操作系统用来管理和控制线程的数据结构,用于存储和维护线程的状态和相关信息。
TCB包含了以下成员:
- 线程ID(Thread ID):用于唯一标识线程的标识符。
- 程序计数器(Program Counter):指向线程当前执行的指令地址,用于记录线程的执行位置。
- 寄存器集合(Register Set):保存线程的寄存器值,包括通用寄存器、堆栈指针、帧指针等。
- 线程状态(Thread State):记录线程的当前状态,如运行态、就绪态、阻塞态等。
- 优先级(Priority):用于确定线程的调度优先级,决定线程在竞争CPU资源时的优先级顺序。
- 堆栈指针(Stack Pointer):指向线程的堆栈顶部,用于保存线程的局部变量和函数调用信息。
- 线程私有数据(Thread-Specific Data):存储线程特定的数据,每个线程都有自己独立的数据副本。
- 资源列表(Resource List):记录线程所拥有的资源,如打开的文件、分配的内存等。
- 进程控制块指针(Process Control Block Pointer):指向所属进程的进程控制块(PCB),用于与进程的关联。
- 线程同步与通信信息:记录线程参与的同步和通信机制的状态和数据,如互斥锁、条件变量等。
TCB是操作系统用于管理线程的重要数据结构,它存储了线程的关键信息,包括状态、寄存器值、资源和调度信息等。通过操作TCB,操作系统可以实现线程的创建、销毁、调度和同步等操作,保证线程的正确执行和协调。
为什么OS要引入线程?
有的进程需要”同时“做许多事情,但是传统的进程只能串行地执行一系列程序。为此,引入”线程“来增加并发度
引入线程机制后的变化
线程模型有哪些?具体含义?
- 多对一模型
多对一模型将多个用户级线程映射到一个内核级线程. 线程管理由用户空间的线程库完成,当一个线程调用系统调用陷入内核,整个进程将被阻塞,一次只有一个线程可以访问内核.因此多个线程无法在多处理器上并行运行.
- 一对一模型
此模型提供更多的并发性,当一个线程陷入内核时其它线程还可以运行,也就是说多个线程可以在微处理器下并行执行
缺点:创建用户线程时需要相应的内核线程
- 多对多模型
此模型提供并发的最佳准确性
用户级线程和内核级线程的概念?如何实现?优缺缺点?
用户级线程和内核级线程是两种不同的线程实现方式,用于管理和调度线程的执行。
- 用户级线程(User-Level Thread):
- 概念:用户级线程是在用户空间中实现和管理的线程,操作系统对其不可见。用户级线程由用户级线程库进行创建、调度和同步。
- 实现:用户级线程通过线程库提供的函数调用来创建、销毁和切换线程。线程库负责管理线程的上下文、调度和同步等操作。
- 优点:
- 线程切换开销小:由于线程切换都在用户空间完成,无需切换到内核空间,因此线程切换的开销较小。
- 灵活性:线程库提供了更灵活的调度算法和同步机制,可以根据应用程序的需要实现自定义的调度策略。
- 缺点:
- 阻塞问题:如果一个用户级线程被阻塞(如等待I/O操作完成),它将导致整个进程的所有用户级线程被阻塞,因为操作系统无法感知到这种阻塞状态。
- 资源限制:用户级线程的数量和调度策略受限于操作系统对进程的资源分配,无法充分利用多核处理器的并行性。
- 内核级线程(Kernel-Level Thread):
- 概念:内核级线程是在内核空间中实现和管理的线程,由操作系统内核直接创建、调度和同步。
- 实现:内核级线程由操作系统内核提供的线程调度器进行管理,每个线程都由内核分配独立的资源(如内核栈)。
- 优点:
- 并发性:内核级线程能够充分利用多核处理器的并行性,通过操作系统的调度算法实现多线程的并发执行。
- 阻塞处理:如果一个内核级线程被阻塞,仅会影响到该线程本身,其他线程可以继续执行。
- 缺点:
- 线程切换开销大:由于线程切换需要切换到内核空间,涉及到上下文切换和内核数据结构的操作,因此线程切换的开销较大。
- 编程复杂性:与用户级线程相比,操作系统提供的线程调度和同步机制相对复杂,需要使用系统调用来操作线程。
选择用户级线程还是内核级线程取决于应用的需求和目标。用户级线程适用于对并发性要求不高、对调度和同步有自定义需求的应用;而内核级线程适用于需要充分利用多核处理器、依赖于操作系统调度和资源管理的应用。在实际应用中,也存在混合线程模型,以充分利用用户级线程和内核级线程的优势。
Linux如何创建线程?
#include <stdio.h>
#include <pthread.h>
// 线程函数
void* thread_function(void* arg) {
int thread_arg = *(int*)arg;
printf("Thread argument: %d\n", thread_arg);
printf("Hello from the thread!\n");
// 获取当前线程的线程ID
pthread_t thread_id = pthread_self();
// 获取线程TCB指针
pthread_key_t tcb_key;
void* tcb_ptr = NULL;
pthread_key_create(&tcb_key, NULL);
tcb_ptr = pthread_getspecific(tcb_key);
printf("Thread ID: %lu\n", (unsigned long)thread_id);
printf("Thread TCB: %p\n", tcb_ptr);
pthread_exit(NULL);
}
int main() {
pthread_t thread;
int arg = 123;
// 创建线程
int result = pthread_create(&thread, NULL, thread_function, &arg);
if (result != 0) {
perror("Thread creation failed");
return 1;
}
// 等待线程结束
result = pthread_join(thread, NULL);
if (result != 0) {
perror("Thread join failed");
return 1;
}
printf("Thread joined\n");
return 0;
}
第七周:进程通信
什么是进程通信
进程间通信(Inter-Process Communication,IPC)是指两个进程之间产生数据交互。
OS一般有哪些通信方式
- 共享存储
共享存储分为基于数据结构和基于存储区共享
原理:各个进程原本只能访问属于自己的地址空间,但是对于想要进行通信的进程可以开辟一个单独的共享存储区进行进程通信。当多个进程进行通信,为了防止写覆盖,进程间对于共享空间的访问应当互斥进行。
LINUX中实现方式
int shm_open(...); // 想要进行通信的进程通过方法调用,申请一片共享内存区
void * mmap(...); //双方程序都调用mmap方法,将通向内存区映射到进程自己的地址空间
- 消息传递
进程间的数据交换以格式化的信息为单位。进程通过操作系统提供的“发送/接受信息”两个原语进行数据交换。
信息传递分为直接通信方式和间接通信方式
格式化的信息
直接通信方式
过程:进程P要给进程Q发送一个信息,首先通过发送原语,send(Q,msg)指定目标进程和信息,然后这个信息会被复制到进程Q的进程控制(PCB)的消息队列上,然后进程Q通过接收原语,receive(P,&msg)将信息复制到进程Q的地址空间上。
指明进程发送
间接通信方式
过程:进程p首先将信息放到自己的地址空间完善信息,然后通过send原语将信息发送到A信箱,然后q进程通过receive原语将信息从A取出放到自己地址空间。
- 管道通信
- 管道只能采用半双工通信,如果要双向同时通信则需要设置两个管道。
- 各个管道要互斥的访问管道。
- 当管道写满时,写进程将被阻塞,直到读进程取走数据,才可以唤醒。
- 当管道读空时,读进程将被阻塞,直到写进程写入数据,才可以唤醒。
Linux下操作
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_DATA_LEN 256
#define DELAY_TIME 1
int main()
{
pid_t pid;
int pipe_fd[2];
char buf[MAX_DATA_LEN];
const char data[] = "Pipe Test Program";
int real_read, real_write;
memset((void*)buf, 0, sizeof(buf));
/* 创建管道 */
if (pipe(pipe_fd) < 0)
{
printf("pipe create error\n");
exit(1);
}
if ((pid = fork()) == 0)
{
/* 子进程关闭写描述符,并通过使子进程暂停3s等待父进程已关闭相应的读描述符 */
close(pipe_fd[1]);
sleep(DELAY_TIME * 3);
/* 子进程读取管道内容 */
if ((real_read = read(pipe_fd[0], buf, MAX_DATA_LEN)) > 0)
{
printf("%d bytes read from the pipe is '%s'\n", real_read, buf);
}
/* 关闭子进程读描述符 */
close(pipe_fd[0]);
exit(0);
}
else if (pid > 0)
{
/* 父进程关闭读描述符,并通过使父进程暂停1s等待子进程已关闭相应的写描述符 */
close(pipe_fd[0]);
sleep(DELAY_TIME);
if((real_write = write(pipe_fd[1], data, strlen(data))) != -1)
{
printf("Parent wrote %d bytes : '%s'\n", real_write, data);
}
close(pipe_fd[1]); /*关闭父进程写描述符*/
waitpid(pid, NULL, 0); /*收集子进程退出信息*/
exit(0);
}
return 0;
}
结果
实验三:同步机制之生产者-消费者问题
- 预习知识
复习信号量、临界区、临界资源、直接制约和间接制约关系概念,以及如何用信号量实现多进程(线程)同步控制的方法。熟悉
pthread_create()
、sem_wait()
、sem_post()
、pthread_mutex_init
、pthread_mutex_lock
、pthread_mutex_unlock
、pthread_join
函数的使用,复习C语言的相关内容。
下面是对
pthread_create()
、sem_wait()
、sem_post()
、pthread_mutex_init
、pthread_mutex_lock
、pthread_mutex_unlock
、pthread_join
函数的简要说明:
-
pthread_create():
pthread_create()
函数用于创建一个新的线程。它的原型如下:int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg); ``` 参数 `thread` 是一个指向线程标识符的指针,`attr` 是线程属性,`start_routine` 是线程开始执行的函数,`arg` 是传递给线程函数的参数。该函数成功时返回 0,失败时返回错误码。
-
sem_wait():
sem_wait()
函数用于对信号量进行 P 操作(等待操作)。它的原型如下:int sem_wait(sem_t *sem); ``` 参数 `sem` 是指向信号量的指针。如果信号量的值大于 0,则将其减 1;如果信号量的值为 0,则阻塞调用线程直到信号量的值大于 0。该函数成功时返回 0,失败时返回错误码。
-
sem_post():
sem_post()
函数用于对信号量进行 V 操作(释放操作)。它的原型如下:int sem_post(sem_t *sem); ``` 参数 `sem` 是指向信号量的指针。将信号量的值加 1。该函数成功时返回 0,失败时返回错误码。
-
pthread_mutex_init:
pthread_mutex_init
函数用于初始化互斥锁。它的原型如下:int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr); ``` 参数 `mutex` 是指向互斥锁的指针,`attr` 是互斥锁的属性。该函数成功时返回 0,失败时返回错误码。
-
pthread_mutex_lock:
pthread_mutex_lock
函数用于对互斥锁进行加锁操作。它的原型如下:int pthread_mutex_lock(pthread_mutex_t *mutex); ``` 参数 `mutex` 是指向互斥锁的指针。如果互斥锁当前没有被锁定,则锁定互斥锁。如果互斥锁已经被锁定,调用线程将被阻塞,直到互斥锁被解锁。该函数成功时返回 0,失败时返回错误码。
-
pthread_mutex_unlock:
pthread_mutex_unlock
函数用于对互斥锁进行解锁操作。它的原型如下:int pthread_mutex_unlock(pthread_mutex_t *mutex); ``` 参数 `mutex` 是指向互斥锁的指针。解锁互斥锁,允许其他线程获取该互斥锁。该函数成功时返回 0,失败时返回错误码。
-
pthread_join:
pthread_join
函数用于等待指定的线程结束。它的原型如下:int pthread_join(pthread_t thread, void **retval); ``` 参数 `thread` 是要等待的线程标识符,`retval` 是指向线程返回值的指针。调用线程将被阻塞,直到指定的线程结束。该函数成功时返回 0,失败时返回错误码。
这些函数是在 C 语言中用于多线程编程和同步控制的重要函数。使用它们可以实现线程的创建、互斥锁的操作、信号量的操作以及线程的等待和同步。
信号量(Semaphore)是一种同步原语,用于控制多进程(线程)对共享资源的访问。它可以用来解决多进程(线程)同步和互斥的问题。
临界区(Critical Section)是指一段代码,在同一时间只能被一个进程(线程)执行。在临界区内,对共享资源的访问需要进行同步控制,以避免并发访问导致的错误。
临界资源(Critical Resource)是指被多个进程(线程)共享的资源,如共享内存区、文件等。对于临界资源的访问需要进行同步控制,以确保多个进程(线程)之间的正确协作。
直接制约关系(Direct Constraint)指的是两个进程(线程)之间的依赖关系,其中一个进程(线程)的执行依赖于另一个进程(线程)的某个操作完成。
间接制约关系(Indirect Constraint)指的是两个进程(线程)之间通过共享资源产生的依赖关系,一个进程(线程)的执行依赖于另一个进程(线程)对共享资源的访问。
使用信号量可以实现多进程(线程)的同步控制。信号量可以看作是一个计数器,用于表示可用的资源数量。常用的信号量操作有两个:
sem_wait()
:如果信号量的值大于0,则将其减1;如果信号量的值为0,则阻塞该进程(线程)直到信号量的值大于0。sem_post()
:将信号量的值加1。
下面是一个使用信号量实现多进程(线程)同步控制的方法的示例代码:
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#define NUM_THREADS 5
sem_t semaphore; // 信号量
void* thread_func(void* thread_id) {
long tid = (long)thread_id;
// 进入临界区之前的操作
printf("Thread %ld is doing something before entering the critical section.\n", tid);
// 进入临界区
sem_wait(&semaphore);
// 临界区内的操作
printf("Thread %ld is in the critical section.\n", tid);
// 离开临界区
sem_post(&semaphore);
// 离开临界区之后的操作
printf("Thread %ld is doing something after leaving the critical section.\n", tid);
pthread_exit(NULL);
}
int main() {
pthread_t threads[NUM_THREADS];
int rc;
long t;
// 初始化信号量
sem_init(&semaphore, 0, 1);
for (t = 0; t < NUM_THREADS; t++) {
printf("Creating thread %ld\n", t);
rc = pthread_create(&threads[t], NULL, thread_func, (void*)t);
if (rc) {
printf("Error: return code from pthread_create() is %d\n", rc);
return -1;
}
}
// 等待所有线程结束
for (t = 0; t < NUM_THREADS; t++) {
pthread_join(threads[t], NULL);
}
// 销毁信号量
sem_destroy(&semaphore);
return 0;
}
在上面的示例代码中,使用了sem_wait()
和sem_post()
函数来实现对信号量的操作,控制多个线程对临界区的访问。pthread_create()
函数用于创建线程,pthread_join()
函数用于等待线程结束。sem_init()
函数用于初始化信号量,sem_destroy()
函数用于销毁信号量。
- 开始实验
(1)运行例子中的代码,请观察运行结果,并说明生产者和消费者在并发执行过程中,互斥信号量和同步信号量的作用。
(2)这是一个单生产者和单消费者的问题,改进程序,使其成为多生产者和多消费者问题。
(3)实验过程中遇到哪些问题,有哪些收获,越详细越具体越好。
//pv操作:生产者与消费者经典问题
//author:leaf
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#define M 32 /*缓冲数目*/
#define P(x) sem_wait(&x)
#define V(x) sem_post(&x)
int in = 0; /*生产者放置产品的位置*/
int out = 0; /*消费者取产品的位置*/
int buff[M] = {0}; /*缓冲初始化为0, 开始时没有产品*/
sem_t empty_sem; /*同步信号量,当满了时阻止生产者放产品*/
sem_t full_sem; /*同步信号量,当没产品时阻止消费者消费*/
pthread_mutex_t mutex; /*互斥信号量, 一次只有一个线程访问缓冲*/
/*
*output the buffer
*/
void print()
{
int i;
for(i = 0; i < M; i++)
printf("%d ", buff[i]);
printf("\n");
}
/*
*producer
*/
void *producer()
{
for(;;)
{
sleep(1);
P(empty_sem);
pthread_mutex_lock(&mutex);
in = in % M;
printf("(+)produce a product. buffer:");
buff[in] = 1;
print();
++in;
pthread_mutex_unlock(&mutex);
V(full_sem);
}
}
/*
*consumer
*/
void *consumer()
{
for(;;)
{
sleep(2);
P(full_sem);
pthread_mutex_lock(&mutex);
out = out % M;
printf("(-)consume a product. buffer:");
buff[out] = 0;
print();
++out;
pthread_mutex_unlock(&mutex);
V(empty_sem);
}
}
void sem_mutex_init()
{
/*
*semaphore initialize
*/
int init1 = sem_init(&empty_sem, 0, M);
int init2 = sem_init(&full_sem, 0, 0);
if( (init1 != 0) && (init2 != 0))
{
printf("sem init failed \n");
exit(1);
}
/*
*mutex initialize
*/
int init3 = pthread_mutex_init(&mutex, NULL);
if(init3 != 0)
{
printf("mutex init failed \n");
exit(1);
}
}
int main()
{
pthread_t id1;
pthread_t id2;
int i;
int ret;
sem_mutex_init();
/*create the producer thread*/
ret = pthread_create(&id1, NULL, producer, NULL);
if(ret != 0)
{
printf("producer creation failed \n");
exit(1);
}
/*create the consumer thread*/
ret = pthread_create(&id2, NULL, consumer, NULL);
if(ret != 0)
{
printf("consumer creation failed \n");
exit(1);
}
pthread_join(id1,NULL);
pthread_join(id2,NULL);
exit(0);
}
修改后流程图:
代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#define M 32 /*缓冲数目*/
#define N_PRODUCERS 2 /*生产者线程数目*/
#define N_CONSUMERS 2 /*消费者线程数目*/
#define P(x) sem_wait(&x)
#define V(x) sem_post(&x)
int in = 0; /*生产者放置产品的位置*/
int out = 0; /*消费者取产品的位置*/
int buff[M] = {0}; /*缓冲初始化为0, 开始时没有产品*/
sem_t empty_sem; /*同步信号量,当满了时阻止生产者放产品*/
sem_t full_sem; /*同步信号量,当没产品时阻止消费者消费*/
pthread_mutex_t mutex; /*互斥信号量, 一次只有一个线程访问缓冲*/
/*
*output the buffer
*/
void print()
{
int i;
for(i = 0; i < M; i++)
printf("%d ", buff[i]);
printf("\n");
}
/*
*producer
*/
void *producer()
{
for(;;)
{
sleep(1);
P(empty_sem);
pthread_mutex_lock(&mutex);
in = in % M;
printf("Producer %lu (+) produce a product. buffer:", pthread_self());
buff[in] = 1;
print();
++in;
pthread_mutex_unlock(&mutex);
V(full_sem);
}
}
/*
*consumer
*/
void *consumer()
{
for(;;)
{
sleep(2);
P(full_sem);
pthread_mutex_lock(&mutex);
out = out % M;
printf("Consumer %lu (-) consume a product. buffer:", pthread_self());
buff[out] = 0;
print();
++out;
pthread_mutex_unlock(&mutex);
V(empty_sem);
}
}
void sem_mutex_init()
{
/*
*semaphore initialize
*/
int init1 = sem_init(&empty_sem, 0, M);
int init2 = sem_init(&full_sem, 0, 0);
if( (init1 != 0) && (init2 != 0))
{
printf("sem init failed \n");
exit(1);
}
/*
*mutex initialize
*/
int init3 = pthread_mutex_init(&mutex, NULL);
if(init3 != 0)
{
printf("mutex init failed \n");
exit(1);
}
}
int main()
{
pthread_t producers[N_PRODUCERS];
pthread_t consumers[N_CONSUMERS];
int i;
int ret;
sem_mutex_init();
/*create the producer threads*/
for (i = 0; i < N_PRODUCERS; i++) {
ret = pthread_create(&producers[i], NULL, producer, NULL);
if(ret != 0)
{
printf("producer creation failed \n");
exit(1);
}
}
/*create the consumer threads*/
for (i = 0; i < N_CONSUMERS; i++) {
ret = pthread_create(&consumers[i], NULL, consumer, NULL);
if(ret != 0)
{
printf("consumer creation failed \n");
exit(1);
}
}
/* join the producer threads */
for (i = 0; i < N_PRODUCERS; i++) {
pthread_join(producers[i], NULL);
}
/* join the consumer threads */
for (i = 0; i < N_CONSUMERS; i++) {
pthread_join(consumers[i], NULL);
}
exit(0);
}
问题:
竞争条件:在并发执行的情况下,如果没有正确处理线程之间的竞争条件,可能导致数据不一致或者死锁等问题。
死锁:如果没有正确使用互斥锁和信号量,可能会导致线程之间的死锁,即所有线程都被阻塞,无法继续执行。
缓冲区溢出或下溢:如果没有正确控制生产者和消费者的速度或没有合理地处理缓冲区满或空的情况,可能会导致缓冲区溢出或下溢,造成数据丢失或错误。
收获:
理解并发编程:通过实验,可以加深对并发编程的理解,包括线程间的竞争条件、同步机制和互斥机制等概念。
学习信号量和互斥锁的使用:通过实验中对信号量和互斥锁的使用,可以学习如何使用这些同步机制来保护共享资源,避免竞争条件和死锁。
多生产者和多消费者问题的处理:通过改进程序,将单生产者和单消费者问题扩展为多生产者和多消费者问题,可以学习如何处理更复杂的并发场景,包括线程的创建和管理、同步机制的调整等。
调试并发程序:在实验过程中,可能会遇到各种并发问题,如死锁、数据竞争等。通过调试并发程序,可以学习如何识别和解决这些问题,并加深对并发调试技巧的理解。
第八周:处理机调度与调度算法
处理机调度模型
进程调度的时机
- 进程正常或异常结束
- 进程提出I/O请求
- 时间片到
- 高优先级进程抢占
- 信号
- 系统调用
如何分配CPU【进程切换】
- 保存进程A的CPU现场
- 修改被中断进程A的PCB,如状态改为阻塞
- 进程A的PCB链接到阻塞队列中
进程调度方式
抢占式
进程的CPU使用权被抢
- 优先权
- 短进程
- 时间片
非抢占式
进程主动放弃CPU
- 进程正常或异常完成
- 发生某事件不能运行
处理机算法评价准则
-
面向系统调度准则
-
面向用户调度准则
-
算法本身调度准则
常见的处理机调度算法
先来先服务(First-Come, First-Served,FCFS)调度算法:
FCFS调度算法按照作业或进程到达的先后顺序进行调度,采用非抢占式调度方式。它适用于批处理系统或对响应时间要求不高的环境。然而,它可能导致长作业优先(Longest Job First,LJF)效应,即长作业占用处理机时间较长,导致短作业的等待时间增加。
优点:
- 简单易实现,按照到达顺序进行调度。
- 适用于批处理系统或对响应时间要求不高的环境。
缺点:
- 可能导致长作业优先(LJF)效应,即长作业占用处理机时间较长,导致短作业的等待时间增加。
- 不考虑作业或进程的执行时间,可能导致平均等待时间较长。
最短作业优先(Shortest Job First,SJF)调度算法:
最短作业优先(SJF)调度算法根据作业或进程的执行时间进行调度,选择执行时间最短的作业或进程优先执行。它能够最大程度地减少平均等待时间,对短作业有较好的响应性能。然而,它需要预先知道作业或进程的执行时间,对实时系统或无法准确预测执行时间的情况不适用。此外,它可能导致长作业等待时间较长,容易出现饥饿现象。
优点:
- 能够最大程度地减少平均等待时间,对短作业有较好的响应性能。
- 采用非抢占式调度方式,简单易实现。
缺点:
- 需要预先知道作业或进程的执行时间,对实时系统或无法准确预测执行时间的情况不适用。
- 可能导致长作业等待时间较长,容易出现饥饿现象。
优先级调度算法:
优先级调度算法根据作业或进程的优先级进行调度,选择优先级最高的作业或进程优先执行。它可以灵活控制作业或进程的执行顺序,适用于根据实时性要求进行动态调整。然而,需要合理设置优先级,否则可能导致低优先级的作业或进程长时间等待。此外,可能出现优先级反转问题,即高优先级作业或进程被低优先级作业或进程长时间阻塞。
优点:
- 根据优先级进行调度,可以灵活控制作业或进程的执行顺序。
- 可采用抢占式调度方式,可以根据实时性要求进行动态调整。
缺点:
- 需要合理设置优先级,否则可能导致低优先级的作业或进程长时间等待。
- 可能出现优先级反转问题,即高优先级作业或进程被低优先级作业或进程长时间阻塞。
时间片轮转(Round Robin,RR)调度算法:
时间片轮转(RR)调度算法公平地分配处理机时间,保证每个作业或进程都能够及时执行。它适用于多任务环境,能够提供较好的响应性能。然而,时间片的长度需要合理设置,过长的时间片会导致响应时间变长,而过短的时间片会增加上下文切换的开销。此外,长作业可能仍然占用较多的时间片,导致短作业的等待时间增加。
优点:
- 公平地分配处理机时间,保证每个作业或进程都能够及时执行。
- 适用于多任务环境,能够提供较好的响应性能。
缺点:
- 时间片长度的设置需要合理,过长或过短都会影响性能。
- 长作业仍然可能占用较多的时间片,导致短作业的等待时间增加。
第九周:死锁
什么是死锁
死锁是指两个或多个进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,如果没有外力干涉,它们都无法继续执行。
死锁产生的原因
什么时候发生死锁
竞争资源:如果系统中有限的资源被多个进程或线程同时请求,而每个进程或线程都持有部分资源而等待其他进程释放其所占有的资源,那么就会出现死锁。例如,两个进程都获取了打开文件的描述符,并且每个进程都在等待另一个进程释放其文件,这就是一种死锁情况。
进程/线程推进顺序不当:如果进程或线程请求资源的顺序不一致,就可能发生死锁。例如,考虑两个进程A和B,A拥有资源1并请求资源2,B拥有资源2并请求资源1,如果它们都按照这个顺序请求资源,那么就会陷入死锁。
信号量使用不当:例如生产者-消费者问题中,如果使用实现互斥的p操作在同步的p操作之前就可能导致死锁。
处理死锁的策略
鸵鸟策略
鸵鸟策略是一种忽略死锁的策略,即忽略死锁的状态,并假设它不会发生。这种策略的问题在于,一旦死锁真正发生,系统可能无法恢复正常操作。
预防策略
预防策略是在发生死锁之前避免它发生的策略。这种策略通常包括限制系统资源的使用,例如限制同时打开的文件数量,或者要求进程在请求资源之前必须先释放其已经持有的所有资源。
避免策略(包含银行家算法)
避免策略是在发生死锁之前检测并避免它发生的策略。这种策略通常包括使用定时器来检测长时间等待资源的进程,并让这些进程释放它们已经持有的资源。
代码实现:
public class BankerAlgorithm {
private int[] maxResource; // 最大资源需求
private int[] allocResource; // 已分配资源
private int[] freeResource; // 可用资源数量
private int[] needResource; // 进程所需资源数量
private int numProcesses; // 进程数量
private int numResources; // 资源数量
public BankerAlgorithm(int[] maxResource, int[] allocResource, int[] freeResource, int[] needResource, int numProcesses, int numResources) {
this.maxResource = maxResource;
this.allocResource = allocResource;
this.freeResource = freeResource;
this.needResource = needResource;
this.numProcesses = numProcesses;
this.numResources = numResources;
}
public boolean isSafe() {
int[] available = new int[numResources]; // 可用的资源数量
for (int i = 0; i < numProcesses; i++) {
available[i] = freeResource[i];
}
boolean isSafe = true; // 是否安全
for (int i = 0; i < numProcesses; i++) {
if (needResource[i] <= available[i]) {
available[i] += allocResource[i];
} else {
isSafe = false;
break;
}
}
if (isSafe) {
for (int i = 0; i < numProcesses; i++) {
if (needResource[i] > available[i]) {
isSafe = false;
break;
}
}
}
return isSafe;
}
public static void main(String[] args) {
int[] maxResource = {10, 10, 10}; // 最大资源需求
int[] allocResource = {5, 3, 7}; // 已分配资源
int[] freeResource = {8, 5, 3}; // 可用资源数量
int[] needResource = {1, 1, 1}; // 进程所需资源数量
int numProcesses = 2; // 进程数量
int numResources = 3; // 资源数量
BankerAlgorithm bankerAlgorithm = new BankerAlgorithm(maxResource, allocResource, freeResource, needResource, numProcesses, numResources);
boolean isSafe = bankerAlgorithm.isSafe();
System.out.println("系统是否处于安全状态:" + isSafe);
}
}
检测和解除策略
检测和解除策略是在发生死锁之后检测并解除它发生的策略。这种策略通常包括定期检查系统状态以检测死锁,一旦检测到死锁,就采取措施(如终止和重启进程)来解除死锁。
总结:
第九周:内存概述+连续分配
内存管理的概念
内存空间的分配与回收
内存保护