操作系统(进程 线程 死锁 生产者消费者 读者写者 虚拟内存 页面调度 )

本文深入探讨了并发与并行的概念,详细解释了操作系统如何通过进程和线程实现并发,介绍了进程间通信的各种方式,包括无名管道、FIFO命名管道、消息队列、信号量、共享内存和剪切板通信。此外,还讨论了进程调度算法、进程同步机制、死锁问题及解决方案,以及内存管理中的虚拟内存技术和页面置换算法。
  • 并发:
    并发是指宏观上在一段时间内能同时运行多个程序,而并行则指同一时刻能运行多个指令。
    并行需要硬件支持,如多流水线、多核处理器或者分布式计算系统。
    操作系统通过引入进程和线程,使得程序能够并发运行。

  • 共享
    系统中的资源能够被多个进程共同使用。
    两种方式:互斥共享、同时共享
    互斥共享的资源被称为临界资源,在同一时间内只允许一个进程访问。

  • 虚拟
    把物理实体转换为逻辑实体。
    两种技术:时分复用、空分复用
    多个进程能在同一个处理器上并发执行使用了时分复用技术,让每个进程轮流占有处理器,每次只执行一小个时间片并快速切换。
    虚拟内存使用了空分复用技术,它将物理内存抽象为地址空间,每个进程都有各自的地址空间。地址空间的页被映射到物理内存,地址空间的页并不需要全部在物理内存中,当使用到一个没有在物理内存的页时,执行页面置换算法,将该页置换到内存中。

  • 操作系统基本功能
    进程管理
    进程管理、进程同步、进程通信、死锁处理、处理机调度
    内存管理
    内存分配、地址映射、内存保护与共享、虚拟内存
    文件管理
    文件存储空间管理、目录管理、文件读写管理和保护
    设备管理
    完成的用户的I/O请求、提高设备利用率

  • Linux的系统调用主要有以下

  • 中断分类
    外中断
    由 CPU 执行指令以外的事件引起,如 I/O 完成中断,表示设备输入/输出处理已经完成,处理器能够发送下一个输入/输出请求。此外还有时钟中断、控制台中断等
    异常
    由 CPU 执行指令的内部事件引起,如非法操作码、地址越界、算术溢出等。
    陷入
    在用户程序中使用系统调用

  • 进程
    是程序的实体,也是线程的容器,是系统资源分配和调度的基本单位。
    进程控制块 (Process Control Block, PCB) 描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对PCB 的操作。
    状态:就绪、运行、阻塞、释放(只有运行和就绪可以相互转化)

创建一个进程

CreateProcessA(
    _In_opt_ LPCSTR lpApplicationName,
    _Inout_opt_ LPSTR lpCommandLine,
    _In_opt_ LPSECURITY_ATTRIBUTES lpProcessAttributes,
    _In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
    _In_ BOOL bInheritHandles,
    _In_ DWORD dwCreationFlags,
    _In_opt_ LPVOID lpEnvironment,
    _In_opt_ LPCSTR lpCurrentDirectory,
    _In_ LPSTARTUPINFOA lpStartupInfo,
    _Out_ LPPROCESS_INFORMATION lpProcessInformation
    );
  • 进程间通信方式
    进程间通信方式一:无名管道
    1. 它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端。
    2. 它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)
    3. 它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。
    创建一个无名管道,实现父子进程之间的通信
int main(){
      SECURITY_ATTRIBUTES sa;//为了为了让子进程可以继承父进程的管道句柄,需要对SA结构体进行处理
      ZeroMemory(&sa, sizeof(sa));//初始化
      SECURITY_ATTRIBUTES sa2;
      ZeroMemory(&sa2, sizeof(sa2));
      sa.bInheritHandle = true;//子进程可以继承句柄
      //sa.nLength = sizeof(sa);//区别不大,更多是为了兼容
      sa2.bInheritHandle = true;
      //sa2.nLength = sizeof(sa2);
      HANDLE hw1;//传递消息
      HANDLE hr1;
      HANDLE hw2; //接受消息的管道
      HANDLE hr2;
      if (CreatePipe(&hr1, &hw1, &sa, 4096)){//该管道为了给子进程发送消息
            cout << "创建发送管道成功" << endl;
      }
      if (CreatePipe(&hr2, &hw2, &sa2, 4096)){//该管道为了接受子进程的消息
            cout << "创建读取管道成功" << endl;
      }
      STARTUPINFO st;//启动新窗口的结构体
      ZeroMemory(&st, sizeof(st));
      st.hStdInput = hr1;//子进程的标准输入变为管道1的读句柄
      st.hStdOutput = hw2;//子进程的标准输出变成管道2的写句柄
      st.hStdError = hw2;
      st.wShowWindow = 0;//没啥用,设置显示不显示窗口,都不会显示
      st.dwFlags = STARTF_USESTDHANDLES;//让新设置的句柄替换默认句柄
      PROCESS_INFORMATION pi;//接受新线程的一些参数
      WCHAR pc[] = TEXT("cmd.exe");
      CreateProcess(0, pc, 0, 0, 1, 0, 0, 0, &st, &pi);//可以继承父类句柄的新子进程
      Sleep(50);//给足子进程时间写入管道数据
      char buff[4096] = "dir\n";//初值没啥用,随便写的
      ULONG sendlen;//负责接受传入和接受的字节长度的变量
      ReadFile(hr2, buff, 4096, &sendlen, 0);//接受一段,正常情况下都会接到子进程打开时的画面
      cout << "收到子进程的欢迎画面:" << buff << endl;//规矩,输出子进程欢迎界面
      while (1){
            cin.getline(buff, 4096);//读取一行输入
            if (!strncmp(buff, "!", 2)){
                  sprintf(buff, "exit\r\n");//让cmd退出命令
                  WriteFile(hw1, buff, strlen(buff), &sendlen, 0);
                  cout << "退出终端" << endl;
                  break;
            }
            if (!strncmp(buff, "输出当前文件夹内容", 19)){
                  sprintf(buff, "dir\r\n");
                  WriteFile(hw1, buff, strlen(buff), &sendlen, 0);
            }
            else{
                  strcat(buff, "\r\n");
                  WriteFile(hw1, buff, strlen(buff), &sendlen, 0);
            }
            system("cls");//清屏
            cout << "发送了" << sendlen << "个字" << endl;//提示发送了多少字
            Sleep(50);//给足子进程充足时间
            ReadFile(hr2, buff, 4096, &sendlen, 0);//读文本到缓冲区
            cout << "收到了" << sendlen << "个字\n" << buff << endl;//收到的文本
      }
      return 0;
}

进程间通信方式二:FIFO 命名管道
1. FIFO可以在无关的进程之间交换数据,与无名管道不同。
2. FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。

1、创建命名管道:CreateNamedPipe
2、等待客户端连接:ConnectNamedPipe
3、读取客户端请求数据:ReadFile
4、向客户端回复数据:WriteFile
5、断开连接:DisconnectNamedPipe
6、关闭管道:CloseHandle

int choose;
      while (1)
      {
            cin >> choose;
            if (choose == 1){
                  //创建命名管道
                  HANDLE h =  CreateNamedPipe(TEXT("\\\\.\\pipe\\mypipe"), PIPE_ACCESS_DUPLEX,  PIPE_READMODE_MESSAGE | PIPE_TYPE_MESSAGE | PIPE_WAIT,  PIPE_UNLIMITED_INSTANCES, 0, 0, 0, 0);
                  if (h != 0){
                        cout << "等待其他进程连接" << endl;
                        //等待客户端连接:ConnectNamedPipe
                        if (ConnectNamedPipe(h, 0)){
                              cout << "有客户端连接" << endl;
                              char buff[512];
                              DWORD len;
                              //读取客户端请求数据:ReadFile
                              ReadFile(h, buff, 511, &len, 0);
                              buff[len] = 0;
                              cout << "接收到" << len << "个字符:\n" <<  buff << endl;
                        }
                        else{
                              cout << "错误" << endl;
                        }
                  }
                  else{
                        cout << "创建命名管道失败" << endl;
                  }
            }
            else if (choose == 2){
                  if (WaitNamedPipe(TEXT("\\\\.\\pipe\\mypipe"), 0)){
                        cout << "有资源" << endl;
                  }
                  else{
                        cout << "无资源" << endl;
                  }
                  HANDLE h = CreateFile(TEXT("\\\\.\\pipe\\mypipe"),  GENERIC_ALL, 0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
                  if (h != INVALID_HANDLE_VALUE){
                        char buff[512];
                        DWORD len;
                        cin >> buff;
                        //向客户端回复数据:WriteFile
                        WriteFile(h, buff, strnlen(buff, 512), &len, 0);
                        cout << "发送了" << len << "个字符" << endl;
                  }
            }
      }

进程间通信方式三:消息队列, 是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。
1. 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。
2. 消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。
3. 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。

进程间通信方式四:信号量 ,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
1. 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
2. 信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。
3. 每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。

进程间通信方式五:共享内存 ,两个或多个进程共享一个给定的存储区
1. 共享内存是最快的一种 IPC,因为进程是直接对内存进行存取。
2. 因为多个进程可以同时操作,所以需要进行同步。
3. 信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问。

进程间通信方式六:通过剪切板通信

OpenClipboard(NULL);//开启剪切板
      //得到剪切板数据
      HANDLE h = GetClipboardData(CF_TEXT);
      //上锁
      char* buff = (char*)GlobalLock(h);
      cout << buff << endl;
      //解锁
      GlobalUnlock(h);
      sprintf(buff, "hello Work");
      GlobalUnlock(h);
      EmptyClipboard();
      SetClipboardData(CF_TEXT, h);
  • 进程调度算法
  1. 批处理系统
    批处理系统没有太多的用户操作,在该系统中,调度算法目标是保证吞吐量和周转时间(从提交到终止的时间)。
    1.1先来先服务(FCFS)
    按照请求的顺序进行调度。有利于长作业,但不利于短作业
    1.2短作业优先
    按照作业的时间长短执行,长作业可能会饿死
    1.3最短剩余时间优先
    按估计剩余时间最短的顺序执行

2.交互式系统
2.1时间片轮转
将所有就绪进程按 FCFS 的原则排成一个队列,每次调度时,把 CPU 时间分配给队首进程,该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的末尾,同时继续把 CPU 时间分配给队首的进程。
影响时间片算法优劣的关键是时间片长度的确定。如果时间片太长使性能得不到保证,太短则会使进程切换频繁。
2.2优先级调度
为每个进程分配一个优先级,按优先级进行调度。
为了防止低优先级的进程永远等不到调度,可以随着时间的推移增加等待进程的优先级。
2.3多级反馈队列
一个进程需要执行 100 个时间片,如果采用时间片轮转调度算法,那么需要交换 100 次。
多级队列是为这种需要连续执行多个时间片的进程考虑,它设置了多个队列,每个队列时间片大小都不同,例如
1,2,4,8,…。进程在第一个队列没执行完,就会被移到下一个队列。这种方式下,之前的进程只需要交换 7 次。
每个队列优先权也不同,最上面的优先权最高。因此只有上一个队列没有进程在排队,才能调度当前队列上的进程。
可以将这种调度算法看成是时间片轮转调度算法和优先级调度算法的结合。

3.实时系统
要求一个请求能够在确定时间内得到响应。

  • 进程同步

1.临界资源
对临界资源进行访问的那段代码称为临界区。
为了互斥访问临界资源,每个进程在进入临界区之前,需要先进行检查。
代码实现一个临界区例子 创建一个临界区资源 当有进程调用的时候每次都需要先对它上锁 修改结束后在解锁

long long id; //模拟临界区资源

class MyClass
{
private:
	CRITICAL_SECTION as;
public:
	MyClass()
	{
		InitializeCriticalSection(&as);
	}
	~MyClass(){
		DeleteCriticalSection(&as);
	}
	void lock(){
		EnterCriticalSection(&as);
	}
	void unlock(){
		LeaveCriticalSection(&as);
	}
};
MyClass as;
DWORD WINAPI run(PVOID lpThreadParameter){
	for (int i = 0; i < 100; i++)
	{
		as.lock();
		++id;
		as.unlock();
	}
	return 0;
}
int main()
{
	while (1)
	{
		HANDLE harr[50];
		for (int i = 0; i < 50; i++){
			harr[i] = CreateThread(0, 0, run, (PVOID)i, CREATE_SUSPENDED, 0);
		}
		for (int i = 0; i < 50; i++)
		{
			ResumeThread(harr[i]);
		}
		WaitForMultipleObjects(50, harr, true, INFINITE);
		printf("%d\n", id);
		for (int i = 0; i < 50; i++)
		{
			CloseHandle(harr[i]);
		}
		system("pause");
	}
}
  • 互斥量 Mutex 互斥占有一个变量,一段时间内仅一个线程可以访问
    1. 创建互斥量 CreateMutex(0, initOwn, name)
    2. 上锁 等待指定对象处于信号状态或超时间隔过去WaitForSingleObject(obj,INFINITE)
    3. 解锁 释放指定互斥对象的所有权。 ReleaseMutex(obj)

3.读写锁
1. 读写锁把对共享资源的访问分为读者和写者,读者只对共享资源进行读访问,写者只对共享资源进行写操作。
2. 在互斥机制,读者和写者都需要独立独占互斥量以独占共享资源,
3. 在读写锁机制下,允许同时有多个读者访问共享资源,只有写者才需要独占资源。
4. 相比互斥机制,读写机制由于允许多个读者同时访问共享资源,进一步提高了多线程的并发度。
5. 初始化 InitializeSRWLock(&obj)
6. 写者锁 独占AcquireSRWLockExclusive
7. 释放写者锁 ReleaseSRWLockExclusive(&obj)
8. 创建读者锁 共享 AcquireSRWLockShared(&obj);
9. 释放读者锁ReleaseSRWLockShared(&obj);

-临界区实现的生产者消费者
生产者负责生产货物,前提是仓库有空位;消费者负责消费,前提是有货物。
因此在二者的线程对临界资源(仓库)执行操作的时候就涉及到同步的问题。

//临界区 简单封装
class MyClass
{
private:
	CRITICAL_SECTION as;
public:
	MyClass()
	{
		InitializeCriticalSection(&as);
	}
	~MyClass(){
		DeleteCriticalSection(&as);
	}
	void in(){
		EnterCriticalSection(&as);
	}
	void out(){
		LeaveCriticalSection(&as);
	}
};

#define N 50
vector<int> rq;
HANDLE goodsNumb;//货物数量的信号量
HANDLE vacancyNumb;//空位的数量的信号量
MyClass ca;
int producerTimer = 0;
void Producer(){
	while (1){
		WaitForSingleObject(vacancyNumb, INFINITE);
		ca.in();
		//printf("生产了货物:%d\n", rq.back());
		rq.push_back(producerTimer++);
		ca.out();
		ReleaseSemaphore(goodsNumb, 1, 0);
		//Sleep(1000);
	}
}
void Consumer(){
	while (1){
		WaitForSingleObject(goodsNumb, INFINITE);
		ca.in();
		printf("消费了货物:%d\n", rq.back());
		rq.pop_back();
		ca.out();
		ReleaseSemaphore(vacancyNumb, 1, 0);
		//Sleep(1000);
	}
}
int main(){
	rq.reserve(N);
	goodsNumb = CreateSemaphore(0, 0, N, 0); //货物起始数量为0 最大为N
	vacancyNumb = CreateSemaphore(0, N, N, 0); //空位起始位N 最大为N
	for (int i = 0; i < 3; i++){
		thread t(Producer);
		t.detach();
	}
	for (int i = 0; i < 5; i++){
		thread t(Consumer);
		t.detach();
	}
	system("pause");
}
  • 读者写者问题
    读者可以多个同时访问,写者只能单独访问。

    以下例子实现了读者写者在不同的“锁”下的性能
    注意:其中的不同的锁 实现与各自的封装

#include<KObject.h>
#include<iostream>
#include<vector>
#include<time.h>
#pragma comment(lib,"winmm.lib")

using namespace std;

//创建读者写者线程 比较互斥量 临界区和读写锁的性能
vector<int> b;
//KMutex  criticalArea  SRWLock
//KMutex ca; //互斥量
criticalArea ca; //临界区
//SRWLock ca; //读写锁

DWORD WINAPI write(void* a){
	for (int i = 0; i < 100000; i++){
		ca.lock();
		b[rand() % b.size()] = rand() % 99;
		ca.unlock();
	}
	return 0;
}

DWORD WINAPI read(void* a){
	for (int i = 0; i < 100000; i++){
		ca.lock();
		b[rand() % b.size()];
		ca.unlock();
	}
	return 0;
}

int main(){
	HANDLE harr[50];
	int i = 0;
	b.assign(10, 1);
	for (; i < 10; i++){
		harr[i] = CreateThread(0, 0, write, 0, CREATE_SUSPENDED, 0);
	}
	for (; i < 30; i++){
		harr[i] = CreateThread(0, 0, read, 0, CREATE_SUSPENDED, 0);
	}
	DWORD start = timeGetTime();
	for (int j = 0; j < 30; j++){
		ResumeThread(harr[j]);
	}
	WaitForMultipleObjects(30, harr, true, INFINITE);
	DWORD end = timeGetTime();

	cout << "花费时间" << end - start << endl;

	return 0;
}
  • 死锁产生的必要条件:
    1.互斥:每个资源要么已经分配给了一个进程;
    2.请求和保持:进程已经占有了一个资源但是执行接下来的操作需要申请其他进程正在占有的资源;
    3.不可抢占:已经被分配的资源不能够被强制剥夺;
    4.循环等待:有两个或者两个以上的进程组成一条环路,该环路中的每个进程都在等待下一个进程所占有的资源。

  • 死锁解决方案:
    1.鸵鸟策略:因为解决死锁问题的代价很高,因此鸵鸟策略这种不采取任务措施的方案会获得更高的性能。
    当发生死锁时不会对用户造成多大影响,或发生死锁的概率很低,可以采用鸵鸟策略。
    大多数操作系统,包括 Unix,Linux 和 Windows,处理死锁问题的办法仅仅是忽略它。
    2.死锁检测与死锁恢复
    3.死锁预防
    4.死锁避免

  • 内存管理
    为了在有限的内存上运行较大的进程,使用虚拟内存技术让物理内存扩充为更大的逻辑内存。
    为了管理内存操作系统将内存抽象为地址空间,每个进程拥有自己的地址空间,这个地址空间被划分成块,每一块称为一页。
    这些页被映射到物理内存,但不需要映射到连续的物理内存,也不需要所有页都必须在物理内
    存中。当程序引用到不在物理内存中的页时,由硬件执行必要的映射,将缺失的部分装入物理内存并重新执行失败的指令。

以上所说的“必要的映射”基于内存管理单元管理着地址空间和物理内存的转换。其中的页表(Page table)存储着页(程序地址空间)和页框(物理内存空间)的映射表。
一个虚拟地址分成两个部分,一部分存储页面号,一部分存储偏移量。

  • 页面置换算法
    在程序运行过程中,如果要访问的页面不在内存中,就发生缺页中断从而将该页调入内存中。此时如果内存已无空闲空间,系统必须从内存中调出一个页面到磁盘对换区中来腾出空间。
    页面置换算法的主要目标是使页面置换频率最低(也可以说缺页率最低)。

     1.最佳(OPT)所选择的被换出的页面将是最长时间内不再被访问,通常可以保证获得最低的缺页率。
     2.最近最久未使用(LRU)为了实现 LRU,需要在内存中维护一个所有页面的链表。当一个页面被访问时,将这个页面移到链表表头。这样就能
    

保证链表表尾的页面是最近最久未访问的。
3.最近未使用(NRU)当发生缺页中断时,NRU 算法随机地从类编号最小的非空类中挑选一个页面将它换出。
4.先进先出(FIFO)选择换出的页面是最先进入的页面。

  • 分段
    虚拟内存采用的是分页技术,也就是将地址空间划分成固定大小的页,每一页再与内存进行映射。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值