admin 管理员组

文章数量: 887021

文章目录

  • 传送门
  • 实验报告与源码下载
  • 前言
  • 进程控制API
    • Linux
      • getpid/getppid
      • fork/vfork
      • exit/_exit
      • exec函数族
      • wait/waitpid
      • pause/sleep
    • Windows
      • GetCurrentProcessId
      • CreateProcess
      • GetModuleFileName
      • Sleep
      • WaitForSingleObject/WaitForMultipleObjects
  • 进程间通信API(IPC-API)
    • Linux
      • 共享内存区
        • shmget/shmat/shmdt/shmctl
        • 例子:基本用法
        • 例子:多进程fork+共享
      • 信号量
        • semget/semop/semctl
        • PV封装
        • 例子:多进程文件操作
    • Windows
      • 互斥体
      • 信号量
      • 文件映射
  • 储存器管理API
    • 详见代码阶段
  • 文件操作API
    • 详见代码阶段
  • 实验一:Linux内核编译
    • 注意事项
    • 实验环境
    • 实验步骤
      • 使用镜像源下载linux源码
      • 解压linux-5.4.69.tar.gz
      • 进入解压后的文件
      • 复制本机的配置文件
      • 编译前的环境准备
      • 基于文本选单的配置界面,默认即可
      • 内核全开,编译与模块编译及安装,大概需要30-60min
      • 修改引导菜单
    • 参考博客
  • 实验二:生产者消费者进程
    • Linux版本
      • 头文件
      • 主程序代码
      • 函数框架
      • fork结构
      • 结果
    • Windows版本
      • 头文件
      • main.c
      • producer.c
      • consumer.c
      • 结果
  • 实验三:内存监控
    • 运行结果
    • 主函数
    • 辅助函数
      • 显示权限信息:printProtection
      • 显示帮助:showHelp
    • 核心函数
      • 显示系统信息:displaySystem
      • 显示内存信息:displayMemory
      • 获取活跃进程:getAllProcess
      • 获取进程具体信息:getProcessDetail
  • 实验四:文件复制
    • Windows
      • 运行结果
      • 主函数
      • 命令解析:Parse
      • 同步属性:SyncInfo
      • 文件复制:CopyFile
      • 文件夹复制:CopyDir
    • Linux
      • 运行结果
      • 实现思路
      • 代码

传送门

由于操作系统知识太多,再加上我总结的比较细,所以一篇放不下,拆分成了多篇文章。

操作系统笔记——概述、进程、并发控制
操作系统笔记——储存器管理、文件系统、设备管理
操作系统笔记——Linux系统实例分析、Windows系统实例分析
北理工操作系统实验合集 | API解读与例子
北京理工大学操作系统复习——习题+知识点
资料包百度云下载,含2022年真题一套,提取码cyyy

实验报告与源码下载

百度云提取码:cyyy

前言

我感觉做这方面的实验挺费力的,因为可以参考的资料是真的少,于是我一怒之下就开始写这篇文章了。

本文主要分两大部分,一部分是API讲解,其实我觉得这才是精华,恕我直言,老师上课给的ppt真差点意思,代码都不能用的,这算什么api讲解?另一部分是实验的分析与代码,这一部分我会参考一个github仓库,这个仓库写的不错。但是我还是不建议直接抄,看一看我写的api解读,也不会花太多时间:

代码参考

这篇文章的代码都是我亲手编译运行过的,完全是可以跑的(你跑不了可能是命令的问题,再不济就是一点点小bug)。

进程控制API

Linux

  1. 创建:fork/vfork
  2. 终止:exit/_exit
  3. 获取进程标识符:getpid/getppid(获取parent的pid)
  4. 调用程序:exec
  5. 进程等待:wait/waitpid
  6. 暂停:pause/sleep

getpid/getppid

主进程是程序本身,又称作父进程。父进程可以创建进程,称作子进程。每一个进程都有一个id,通过函数可以查询当前id和父进程id,为什么没有子进程id?这是因为子进程可以创建多个,目前返回值还没有实现一次返回多个的机制。

#include <unistd.h>
pid_t getpid(void);
pid_t getppid(void); //父进程

fork/vfork

#include <sys/types.h>
#include <unistd.h>
pid_t fork (void);

fork是双返回值的,在子进程中返回0(不是子进程pid),父进程中返回子进程的pid(不是父进程pid)。

父子进程实际上是写在一份代码中的,通过if else区分父子进程,可以实现一份代码两个作用。fork是单调用双返回函数,父进程的返回值是子进程PID,子进程返回值为0,这样就既能做到通信,又能实现区分。要想判断当前进程是父进程还是子进程,检验一下PID就行。

先来明确一些fork过程中的pid都是些什么:

#include<unistd.h>
#include<stdio.h>
#include<sys/types.h>

int main(void)
{
	printf("main pid=%d\n",getpid());
	pid_t pid;
	if((pid=fork())<0)
	{
		printf("error\n");
		exit(0);	
	}	
	else if(pid==0)
	{
		printf("child forkpid=%d getpid=%d\n",pid,getpid());
	}
	else
	{
		printf("father forkpid=%d getpid=%d\n",pid,getpid());
	}
} 

可以看到主进程pid为23389,子进程pid为23390。父进程的fork返回值为23390,即子进程pid,子进程fork返回值为0,表示当前进程为子进程。

给一个复杂一点的代码如下:

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int glob = 3;//全局变量 
int main(void)
{
	pid_t pid; //pid_t类型 
	int loc = 3; //局部变量 
	printf("before fork, glob=%d, loc=%d.\n\n", glob, loc);
	if((pid=fork())<0) //fork,赋值pid,检验是否成功 
	{
	  printf("fork() failed.\n");
	  exit(0);
	}
	else if(pid==0) //子进程代码段 
	{
	  glob++;
	  loc--;
	  printf("child process changes glob and loc\n");
	  printf("glob=%d, loc=%d\n", glob, loc);
	}
	else //父进程代码段 
	{
	  printf("parent process doesn’t change glob and loc\n");
	  printf("glob=%d, loc=%d\n", glob, loc);
	}
	printf("\nafter fork()\n");

	//return 0; 
	exit(0);
}

由此可见,fork的执行机制:将fork后的代码复制一份出来,重复创建两个进程,一个为父(pid>0),一个为子(pid=0),进程都拥有全部资源,且资源隔离。

探讨一下代码复制机制。我在fork前和fork后都加了printf,发现fork前代码只执行一次,fork后所有代码复制两份,执行两次,说明fork只复制后面的代码,前面的仅是资源共享,代码不共享。

注意,fork控制的关键在于exit的调用,灵活使用exit阻断进程可以有效控制fork区间,为fork后到exit前的区域

而vfork则是两个进程资源共享,而且会阻塞父进程,先把子进程执行完,再回来执行父进程。所以可以说vfork是串行,fork是并行。

#include <sys/types.h>
#include <stdio.h>
pid_t vfork(void);

把fork改vfork后,代码结果如下:

如何调用多个子进程呢?这里先给出一个简单的嵌套调用案例:

#include<sys/shm.h>
#include<time.h> 
#include<stdio.h>
#define SHMKEY 100
int main(void)
{
	int *pint,shmid;
	pid_t pid1,pid2;
	time_t now;
	
	shmid=shmget(SHMKEY,1024,0666|IPC_CREAT);
	
	if((pid1=fork())>0)//主进程 
	{
		if((pid2=fork())>0)//主进程,二次fork 
		{
			printf("pid=%d\n",getpid());
			exit(0);
		}
		else if(pid2==0)//子进程2 
		{
			printf("pid2=%d\n",getpid());
			exit(0);
		}
		else
		{		
			printf("fork error\n");
			exit(0);
		}

	}
	else if(pid1==0)//子进程1
	{
		printf("pid1=%d\n",getpid());
		exit(0);
	}
	else
	{
		printf("fork error\n");
		exit(0);
	}
}


进一步,如果想要创建任意进程,就需要编写递归函数或者循环函数了:

TODO

exit/_exit

exit先在用户态下,把IO关闭,清空缓冲,之后切到核心态进行系统调用去终止进程。
_exit直接跳过用户态处理,强制终止进程。

exit和return在单进程程序中都可以作为main函数结尾,但是如果在多进程情况下(前面的代码),将最后的exit替换成return,在使用vfork的情况下会报错,具体原因是因为,return影响进程栈,exit是直接退出,如果是vfork,栈是共享的,子进程先return把栈关闭了,那主进程再return,就会出错,甚至栈内的数的调用也会出bug。

参考

如果把上面的代码变成vfork+return,就会报错,意料之中:

首先是,主进程的loc会出问题,其次是竟然又会再执行一次主进程,还会出现段错误,总之问题很多。

exec函数族

exec()函数族。这是一系列函数。

进程调用函数运行一个外部的可执行程序。调用后,原进程代码段、数据段与堆栈段被新程序所替代,新程序从它的main( )开始执行。进程号保持不变,因为是被替代了,而不是新建了进程。此时,原程序exec后面的代码不会被执行(各个内存段都被替代了,自然不会保留源程序,唯一留下的,就是pid)。

给出两个调用例子(execl函数):

#include<unistd.h>
#include<stdio.h>

int main(void)
{
	printf("when exec pid=%d\n",getpid());	
} 
#include<unistd.h>
#include<stdio.h>

int main(void)
{
	printf("before exec pid=%d\n",getpid());
	execl("./exe",0);
	printf("after exec pid=%d\n",getpid());//这一行不执行
}

看下面的执行结果,用main去调用exe,pid是不变的,但是exec后面的代码没有执行(after那句)

exec族具有统一的特征,那具体内部之间还有什么区别呢?

第一个区别在于是否要加路径,或者说路径是否在path中。一般来说,要么用相对路径,要么就用已经加了环境变量的,保证程序鲁棒性。

第二个区别是,命令行参数是采用可变参数+NULL结尾的方式指定,还是以char* argv[]的方式传入(不需要用NULL表明参数列表结束)。

第三个区别在于,是否可以指定新环境,新环境以argv形式传入。

wait/waitpid

wait等待任意一个子进程终止,返回值为子进程pid,同时子进程终止码由一个int指针从参数中返回。

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *statloc);

下面程序展示了返回值和statloc,但是这个statloc比较奇特,如果把exit(1)对应256的statloc,exit(2)对应512的statloc。即exit中数*256。通常都是wait(0),不用这个statloc。

#include<unistd.h>
#include<stdio.h>
#include<sys/types.h>
#include<wait.h> //wait

int main(void)
{
	pid_t pid;
	if((pid=fork())<0)
	{
		printf("error\n");
	}
	else if(pid==0) //子进程 
	{
		printf("child pid=%d\n",getpid());
		exit(1);
	}
	else //父进程 
	{
		printf("father pid=%d\n",getpid());
		int statloc;
		printf("child pid=%d\n",wait(&statloc));
		printf("statloc=%d\n",statloc);
		exit(0);
	} 
}


exit(2)

waitpid,通过pid参数实现更灵活的控制,选择性等待某个子进程,至于子进程pid从何而来,你的fork是有返回值的,保存即可。

#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t, int *statloc, int options);
  1. 父进程可以使用pid指定等待的子进程,pid > 0:pid完全匹配,pid = 0:匹配与当前进程是同一个进程组的任何终止子进程;pid = -1:匹配任何终止的子进程;pid < -1:匹配任何进程组标识等于pid绝对值的任何终止子进程
  2. 可在option中设置WNOHANG,如果没有任何子进程终止,则立即返回0,如不使用option,参数为0。
  3. wait(statloc) = waitpid(-1, statloc, 0)

pause/sleep

pause基本不用,sleep粗略,秒单位,usleep特地使用unsigned long参数,就是为了支持毫秒睡眠。

下面给出简单的sleep代码,子进程先输出5次,主进程wait后也输出5次。wait放在循环内外都无所谓,因为子进程只会exit一次,exit后wait函数如果检测不到子进程,也不会阻塞。

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>  /* 简单的进程同步: 父进程等待子进程输出后再输出*/
main()
{
	int p;
	while((p=fork())==-1);
	if(p==0)
	{/*子进程块*/
	 	int i;
		for(i=0;i<5;i++)
		{
		   	printf("I am child.\n");
  			sleep(1);
		}
		exit(0);
	}
	else
	{/*父进程块*/
	  	int i;
	  	//wait(0);
		for(i=0;i<5;i++)
		{
	  		wait(0); //等待子进程结束
	  		printf("I am parent.\n");
	  		sleep(1);
		}
	}
}

Windows

感觉windows的api包含的信息很多,大概是和其实窗口有关,写起来是比较麻烦的。不过好在其很多概念封装的比较好,函数反而更少。

首先要明白,一定要导入一个<windows.h>库,这一个库基本就全部搞定了。

GetCurrentProcessId

#include<windows.h>
DWORD GetCurrentProcessId(void);

返回一个32bit的DWORD(双字),作为进程id,可以通过%d输出,毕竟双字本身也是int

CreateProcess

主要参数有4个。

  1. 第一个参数:可执行文件路径.。字符串,绝对路径或者相对路径
  2. 第二个参数:命令行参数。字符串
  3. 倒数第二参数:STARTUPINFO结构体指针,储存进程相关的各种信息,比如窗口大小等等,初始化的时候只需要初始化第一个参数即可。比如STARTUPINFO si={sizeof(si)};
  4. 倒数第一参数:PROCESS_INFORMATION结构体指针。包含进程标识的4个成员信息。

可以看到,STARUPINFO东西很多,但是我们刚开始只需要初始化cb即可。

typedef struct _STARTUPINFO 
{ 
	DWORD cb;			 	 //包含STARTUPINFO结构中的字节数.如果Microsoft将来扩展该结构,它可用作版本控制手段.应用程序必须将cb初始化为sizeof(STARTUPINFO) 
    PSTR lpReserved;		 //保留。必须初始化为NULL
    PSTR lpDesktop;			 //用于标识启动应用程序所在的桌面的名字。如果该桌面存在,新进程便与指定的桌面相关联。如果桌面不存在,便创建一个带有默认属性的桌面,并使用为新进程指定的名字。如果lpDesktop是NULL(这是最常见的情况 ),那么该进程将与当前桌面相关联 
    PSTR lpTitle;			 //用于设定控制台窗口的名称。如果lpTitle是NULL,则可执行文件的名字将用作窗口名.This parameter must be NULL for GUI or console processes that do not create a new console window.
    DWORD dwX;				 //用于设定应用程序窗口相对屏幕左上角位置的x 坐标(以像素为单位)。 
    DWORD dwY;				 //对于GUI processes用CW_USEDEFAULT作为CreateWindow的x、y参数,创建它的第一个重叠窗口。若是创建控制台窗口的应用程序,这些成员用于指明相对控制台窗口的左上角的位置
    DWORD dwXSize;			 //用于设定应用程序窗口的宽度(以像素为单位)
    DWORD dwYSize;			 //子进程将CW_USEDEFAULT 用作CreateWindow 的nWidth、nHeight参数来创建它的第一个重叠窗口。若是创建控制台窗口的应用程序,这些成员将用于指明控制台窗口的宽度 
    DWORD dwXCountChars;	 //用于设定子应用程序的控制台窗口的宽度(屏幕显示的字节列)和高度(字节行)(以字符为单位) 
    DWORD dwYCountChars; 
    DWORD dwFillAttribute;   //用于设定子应用程序的控制台窗口使用的文本和背景颜色 
    DWORD dwFlags;           //请参见下一段和表4-7 的说明 
    WORD wShowWindow;        //用于设定如果子应用程序初次调用的ShowWindow 将SW_*作为nCmdShow 参数传递时,该应用程序的第一个重叠窗口应该如何出现。本成员可以是通常用于ShowWindow 函数的任何一个SW_*标识符,除了SW_SHOWDEFAULT. 
    WORD cbReserved2;        //保留。必须被初始化为0 
    PBYTE lpReserved2;       //保留。必须被初始化为NULL
    HANDLE hStdInput;        //用于设定供控制台输入和输出用的缓存的句柄。按照默认设置,hStdInput 用于标识键盘缓存,hStdOutput 和hStdError用于标识控制台窗口的缓存 
    HANDLE hStdOutput; 
    HANDLE hStdError; 
} STARTUPINFO, *LPSTARTUPINFO;

PROCESS_INFORMATION就简单很多

typedef struct _PROCESS_INFORMATION{
	HANDLE  hProcess;      //新进程句柄
	HANDLE  hThread;      //新线程句柄
	DWORD  dwProcessId; //新进程标识符
	DWORD  dwThreadId; //新线程标识符
}PROCESS_INFORMATION;

下面给出一个例子,分析一下参数如何组合:

//child.c,子进程
#include<stdio.h>
#include<windows.h>

int main(int argc,char* argv[])
{
	DWORD PID;//双字,32位 
	PID= GetCurrentProcessId();
	printf("我是子进程,id=%d\n",PID);
	for(int i=0;i<argc;i++)
	{
		printf("命令行参数%d:%s\n",i+1,argv[i]);
	}
	
	exit(0);
}
//main.c
#include<windows.h>
#include<stdio.h>

int main()
{
	STARTUPINFO si={sizeof(si)};//初始化si
	PROCESS_INFORMATION pi;
	
	//方法一
	char cmd[]="child 1 2 3" ;//子进程exe,用child.exe也一样,这很符合命令行特点
	CreateProcess(NULL,cmd,NULL,NULL,
		FALSE,0,NULL,NULL,&si,&pi);
		

	/*方法二
	char path[]="./child.exe" ;//子进程exe
	CreateProcess(path,"nihao I am cyy",NULL,NULL,
		FALSE,0,NULL,NULL,&si,&pi);
		*/
	printf("主进程id=%d,其子进程id=%d\n",GetCurrentProcessId(),pi.dwProcessId);
	
	return 0; 
}

将子进程编译后,再编译运行主进程,方法一的截图如下:
可以看到,主进程和子进程是并行的,因为子进程在不停打断主进程的输出。
方法一不需要指定程序路径,命令行就像我们平时运行程序的方法一样,为程序名+其他命令行参数。

方法二将程序和命令行分开指定。

如果都两个方法同时使用,一次开两个子进程,截图如下:
可见并行的执行顺序并不能确定,这次是子进程比主进程先执行完。

GetModuleFileName

DWORD GetModuleFileName(
		HMODULE hModule,
		LPTSTR lpFilename, DWORD nSize);

检索含有给定模块的可执行文件路径名,一般情况第一个参数都是NULL,默认当前程序。第二个参数传入一个char指针,路径通过char返回,第三个参数使用nSize指定区域长度(我猜的,我感觉buf开多大就指定nSize为多大即可)

//child.c
#include<stdio.h>
#include<windows.h>

int main(int argc,char* argv[])
{
	DWORD PID;//双字,32位 
	PID= GetCurrentProcessId();
	char buf[128];
	GetModuleFileName(NULL,buf,128);
	printf("我是子进程,id=%d\n,路径为%s\n",PID,buf);
	for(int i=0;i<argc;i++)
	{
		printf("命令行参数%d:%s\n",i+1,argv[i]);
	}
	
	exit(0);
}
//main.c
#include<windows.h>
#include<stdio.h>

int main()
{
	STARTUPINFO si={sizeof(si)};//初始化si
	PROCESS_INFORMATION pi;
	
	char path[]="./child.exe" ;//子进程exe
	CreateProcess(path,"nihao I am cyy",NULL,NULL,
		FALSE,0,NULL,NULL,&si,&pi);
		
	printf("主进程id=%d,其子进程id=%d\n",GetCurrentProcessId(),pi.dwProcessId);
	
	return 0; 
}

Sleep

void Sleep(DWORD dwMilliseconds);

毫秒为单位

WaitForSingleObject/WaitForMultipleObjects

这个函数一般是搭配信号量使用的,这里仅仅做个介绍。

一般hHandle参数都是信号量,信号量>0就是信号态,否则就无信号。

有时候,需要等待多个对象。bwaitAll如果是False,只需要等待的若干个对象中,任意一个对象有信号,就可以继续运行。True就需要所有对象都有信号。

实际上,如果不搭配信号量,仅仅是用进程句柄,貌似没有什么卵用。下面的代码中,子进程刚休眠,主进程就执行完了,完全没有理论上等子进程执行完主进程再执行的情况。

#include<windows.h>
#include<stdio.h>
#include<string.h>

HANDLE StartClone()
{
	char path[128];
	GetModuleFileName(NULL,path,128); //获取当前路径 
	//创建进程
	STARTUPINFO si={sizeof(si)};
	PROCESS_INFORMATION pi;
	char cmd[128];
	sprintf(cmd,"%s child",path);//拼接cmd 
	CreateProcess(path,cmd,NULL,NULL,
		FALSE,0,NULL,NULL,&si,&pi);
	CloseHandle(pi.hProcess);
	CloseHandle(pi.hThread);
	return pi.hProcess;
}

void MyParent()
{
	printf("start parent\n");
	HANDLE hChild=StartClone();//复制当前进程
	WaitForSingleObject(hChild,INFINITE);//等待子进程结束,不设时间上限 
	printf("end parent\n");
}

void MyChild()
{
	printf("start child\n");
	Sleep(1000);//等待1s
	printf("end child\n"); 
	//exit?
}



int main(int argc,char* argv[])
{
	if(argc>1&&strcmp(argv[1],"child")==0)
	{
		MyChild();//子进程代码 
	}
	else
	{
		MyParent();//子进程代码 
	}
}


进程间通信API(IPC-API)

IPC:InterProcess Communication

Linux


Unix和Linux的标准很混乱,我们主要使用XSI IPC里面的Posix标准,重点在于共享内存区和信号量API。

共享内存区

linux控制台中使用icps命令查看共享内存区默认配置。

shmget/shmat/shmdt/shmctl

两个或者更多进程可以共享一个内存区,一个进程也可以连接多个共享内存区。

程序中用这三个接口:

  1. 共享内存获取shmget
  2. 共享内存区的附加与解除shmat/shmdt
  3. 共享内存区控制shmctl




例子:基本用法

先通过这个例子说明基本用法。

共享内存例子

这个代码写的挺好,拿来可以直接跑,从宏观上来说,这是一个testset程序,使用while循环不断询问。对于代码,我有一些思考:

key和id看起来都可以用来索引一个共享内存区,但是平时更多地使用的是id。我猜,用key指定是要进行搜索的,而id就类似于索引一样,是字典关系,效率高。因此,有id还是用id,没有id才用key去获取id。

int shmid = shmget ( ( key_t ) 1234, sizeof ( struct shared_use_st ), 0666 | IPC_CREAT );

在两个进程中,都使用了同一个shmget写法,参数都一模一样。所以在不同进程之间,要想访问同一个共享内存区,就需要指定相同的key。shmflag一般是IPC_CREAT(0666作用未知),在第一个shmget中,key对应的内存区不存在,所以就新建一个。第二个shmget中,key对应的内存去存在,所以就直接获取对应的id。

总的来说,用key获取id,用的时候用id。

不过有一种特殊情况,就是key=IPC_PRIVATE,即key==0,此时共享内存区是私密的,不允许外部进程使用(无法通过key获取id),但是子进程可以使用,因为有现成的id。

说完shmget,再说一下shmat/shmdt。

在已知shmid的前提下,可以通过shmat获取共享内存的首地址,其指针是void*型的,一般会进行强转。另外两个参数一般都是0。

shared_memory = shmat ( shmid, NULL, 0 );

shmdt的参数是前面的shared_memory,代表本进程解除与共享内存的绑定。

shmdt ( shared_memory )

至于shmctl,一般是不进行配置的。

最后,新手可能疑惑,如何运行两个进程呢?尤其还是一个进程要用来输入。其实比较简单的方法就是开两个终端,一个运行shmwrite,一个运行shmread,当你在shmwrite终端向共享内存写一个串,shmwrite就会检测到,并且输出下图:

另一种方法就是fork。


例子:多进程fork+共享

fork其实也很常用,尤其是生产者消费者这种。这里给出用fork创建两个子进程的例子。

//外部程序,child.c
#include<sys/shm.h>
#include<time.h>
#include<stdio.h>
#define SHMKEY 100
int main()
{
   int *pint, shmid;
   char *addr;
   time_t now;//储存时间 
   shmid = shmget(SHMKEY, 1024, 0666 | IPC_CREAT);//通过key获取id 
   if(shmid==-1)
   {
	   printf("shmget error\n");
	   exit(0);
   }
   pint = (int*)shmat(shmid, 0, 0);//通过id绑定地址,强转后赋给pint 
   sleep(1);
   time(&now);
   printf("%d: process #3 read: %d\n", now, *pint);//读取一次 
   sleep(3);
   time(&now);
   printf("%d: process #3 read: %d\n", now, *pint);//读取一次 
   shmdt(pint);
}
//主进程 main.c
#include<sys/shm.h>
#include<unistd.h>
#include<time.h> 
#include<stdio.h>
#define SHMKEY 100
int main(void)
{
	int *pint,shmid;
	pid_t pid1,pid2;
	time_t now;
	
	shmid=shmget(SHMKEY,1024,0666|IPC_CREAT);
	
	if((pid1=fork())>0)//主进程 
	{
		if((pid2=fork())>0)//主进程,二次fork 
		{
			pint=(int*)shmat(shmid,0,0);
			*pint=20;//写入 
			time(&now);//获取时间 
			printf("%d:process #1 write:%d\n",now,*pint);
			sleep(5);
			time(&now);
			printf("%d:process #1 read:%d\n",now,*pint);//读取 
			shmdt(pint);
			exit(0);
		}
		else if(pid2==0)//子进程2 
		{
			execl("./child",NULL,0);			
		}
		else
		{		
			printf("fork error\n");
			exit(0);
		}

	}
	else if(pid1==0)//子进程1
	{
		pint=(int*)shmat(shmid,0,0);
		sleep(1);
		time(&now);
		printf("%d:process #2 read:%d\n",now,*pint);//读取 
		sleep(1);
		time(&now);
		*pint=500;//写入 
		printf("%d:process #2 write:%d\n",now,*pint);
		shmdt(pint);
		exit(0);
	}
	else
	{
		printf("fork error\n");
		exit(0);
	}
}

信号量

信号量:semaphore

IPC中,信号量不是像伪代码那种单个声明,而是以信号量集的形式声明,通过函数指定信号量进行操作,信号量集中可以有一个或者多个信号量。

  1. 信号量集的获取semget
  2. 信号量集的操作semop
  3. 信号量集的控制semctl
semget/semop/semctl
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);

key和返回的id类似于shmget。nsems为信号量的个数,semflg一般是0666|IPC_CREAT。

#include <sys/sem.h>
int semop(int semid, struct sembuf semarray[], unsigned int nsops);

既然信号量是批量的,那操作也可以是批量的。semop,给定信号量集以及一个sembuf的array,第三个表示本次操作要用到array中的前几个操作(至少为1,一般都是全部)

sembuf的array中,每一个sembuf都是对信号量的一次操作,sembuf的成员规定了操作的模板,通过对sembuf成员值的修改,去自定义模板:

struct sembuf
{
   //要操作的信号量在信号量集中的索引编号
   unsigned short sem_num;
   //对信号量进行的操作值(可为正、负或0)
   short sem_op;
   //操作标志,一般都是0,除非进一步细化操作
   short sem_flg;
} 
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, union semun arg)

这个函数可以对信号量集(semid)中的某一个信号量(semnum索引对应)进行特定操作(cmd),操作用到的值由semun给出(un指的是union),semun还可以承接返回的值。

union semun
{  
   //用于信号量的赋值
   int val;
   //用于返回信号量集信息
   struct semid_ds *buf;
   //用于设置或者获取信号量集成员的取值
   unsigned short *array;
} 

比如赋值,cmd=SETVAL,semun=1;更多的操作如下:

PV封装

上面的操作还是比较复杂,可以进一步封装。封装的目标是:P,V操作封装,只需要指定semid,semnum,而操作数默认为1/-1,flag也是0,每次只进行一次操作。

void P(int sem_id, int sem_num)
{
   struct sembuf xx;
   xx.sem_num = sem_num;
   xx.sem_op = -1;
   xx.sem_flg = 0;
   semop(sem_id, &xx, 1);
}

void V(int sem_id, int sem_num)
{
   struct sembuf xx;
   xx.sem_num = sem_num;
   xx.sem_op = 1;
   xx.sem_flg = 0;
   semop(sem_id, &xx, 1);
}
例子:多进程文件操作
//child.c
#include<sys/sem.h>
#include<string.h>
#include<malloc.h>
#include<unistd.h>
#include<stdlib.h>
#include<time.h>
#define SEMKEY 300

union semun
{
	int val;
	struct semid_ds *buf;
	unsigned short * array;	
};

void P(int sem_id,int sem_num)
{
	struct sembuf temp;
	temp.sem_num=sem_num;
	temp.sem_op=-1;
	temp.sem_flg=0;
	semop(sem_id,&temp,1);
}

void V(int sem_id,int sem_num)
{
	struct sembuf temp;
	temp.sem_num=sem_num;
	temp.sem_op=1;
	temp.sem_flg=0;
	semop(sem_id,&temp,1);
}


void file_operation(int semid,char* filepath,int pid)
{
	time_t now;
	P(semid,0);//对0号信号量P
	FILE* file;//文件操作
	file=fopen(filepath,"a");
	for(int i=0;i<3;i++)
	{
		time(&now);
		fprintf(file,"%d:file operation by process %d\n",now,getpid());
		sleep(1);	
	}
	V(semid,0);//V操作	
}

int main(void)
{
	int semid;
	semid=semget(SEMKEY,0,0666|IPC_CREAT);
	file_operation(semid,"./semfile",getpid());
}
//main.c
#include<sys/sem.h>
#include<string.h>
#include<malloc.h>
#include<unistd.h>
#include<time.h>
#include<stdlib.h>
#define SEMKEY 300

union semun
{
	int val;
	struct semid_ds *buf;
	unsigned short * array;	
};

void P(int sem_id,int sem_num)
{
	struct sembuf temp;
	temp.sem_num=sem_num;
	temp.sem_op=-1;
	temp.sem_flg=0;
	semop(sem_id,&temp,1);
}

void V(int sem_id,int sem_num)
{
	struct sembuf temp;
	temp.sem_num=sem_num;
	temp.sem_op=1;
	temp.sem_flg=0;
	semop(sem_id,&temp,1);
}


void file_operation(int semid,char* filepath,int pid)
{
	time_t now;
	P(semid,0);//对0号信号量P
	FILE* file;//文件操作
	file=fopen(filepath,"a");
	for(int i=0;i<3;i++)
	{
		time(&now);
		fprintf(file,"%d:file operation by process %d\n",now,getpid());
		sleep(1);	
	}
	V(semid,0);//V操作	
}

int main(void)
{
	union semun sem_val;
	int semid,pid1,pid2;
	semid=semget(SEMKEY,1,0666|IPC_CREAT);//获取信号量集 
	sem_val.val=1;
	semctl(semid,0,SETVAL,sem_val);//初始化为1 
	if((pid1=fork())>0)//主进程 
	{
		if((pid2=fork())>0)//主进程 
		{
			file_operation(semid,"./semfile",getpid());
		}
		else if(pid2==0)//子进程2
		{
			execl("./child",NULL,0);//外部程序 
		} 
		else
		{
			printf("fork err\n");
			exit(0);
		}
	}
	else if(pid1==0)//子进程1
	{
		file_operation(semid,"./semfile",getpid());
	}
	else
	{
		printf("fork err\n");
		exit(0);	
	} 
}

可以看到,结果中,每个进程的处理都是连续的。假设没有PV,进程的处理可能就是交错的,甚至会有文件读写bug。

需要说的是,主线程会阻塞shell,子线程不会,所以当你可以用cat命令的时候,子线程可能还没执行完(此时主线程已经结束),所以还可以添加一些操作,让主线程在子线程全部结束后再退出(但是我现在还不会)

Windows

不得不说,Windows真离谱啊,说他方便吧,封装的确实还行,但是缺点很多,网上资料不全,而且bug一大堆,哭。

互斥体

互斥体是后面信号量的特殊情况,所以这里先给出一个简单的例子,作为铺垫。

先说一下线程。线程和进程的区别在于,线程是共享资源的,共享的其实就是全局变量。局部变量是不共享的。线程的参数比较奇怪,需要你自己强转,但是必须按照特定格式声明。下面给出一例子:

这个例子没用共享资源,仅仅展示了线程的基本写法,实际上如果你创建了全局变量,是会共享的。

例子

这个Mutex例子使用线程实现,因为线程是共享资源(全局变量)的,所以不需要共享内存区,但是后面做进程实验的时候就需要共享内存区(文件映射)了。

#include<windows.h>
#include<stdio.h>

//全局变量,mutex和共享变量
int value;
int steps;
HANDLE mutex;

//函数
void doCount(int delta)//不断修改value 
{
	while(steps>0) 
	{
		WaitForSingleObject(mutex,INFINITE);
		value+=delta;
		printf("%d ",value);
		Sleep(500);		
		steps--;
		ReleaseMutex(mutex);
	}
}

DWORD inc(LPVOID IpParam)//线程函数 
{
	doCount(2);
	return 0;
}

DWORD dec(LPVOID IpParam)
{
	doCount(-1);
	return 0;
} 

 
int main(void)
{
	steps=10;//10步 
	mutex=CreateMutex(NULL,FALSE,NULL);//FALSE:初值为1,匿名
	HANDLE incThread=CreateThread(NULL,0,inc,0,0,NULL);//创建线程 
	HANDLE decThread=CreateThread(NULL,0,dec,0,0,NULL);
	WaitForSingleObject(incThread,INFINITE);//阻塞,否则进程先结束,线程就自动结束了 
	WaitForSingleObject(decThread,INFINITE);
	
	ReleaseMutex(mutex); 
	//CloseHandle
	
	return 0;
}

这个例子中,每次操作共享数值,就会先申请mutex,再释放mutex。这个例子还说明了,创建两个线程,并不能确定哪个先执行。

信号量

//创建信号量
HANDLE CreateSemaphore(  
	  lpSemaphoreAttributes, //NULL表示默认属性
	  lInitialCount,         //信号量的初值
	  lMaximumCount,  //信号量的最大值
	  lpName);         //信号量的名称
//释放信号量
BOOL ReleaseSemaphore(
	  hSemaphore,   //信号量的句柄
	  lReleaseCount,     //信号量计数增加值
	  lpPreviousCount);  //返回信号量原来值

//打开已存在信号量,lpName相当于key
HANDLE  OpenSemaphore(dwDesiredAccess,  
	bInheritHandle,  lpName);

CloseHandle(hSemphore)//关闭

下面给出一个简单的例子,用信号量限制线程数:

#include<windows.h>
#include<stdio.h>

//全局变量
HANDLE sem;


//函数 
DWORD func(LPVOID IpParam)
{
	WaitForSingleObject(sem,INFINITE);// 
	printf("threadId=%d begin\n",GetCurrentThreadId(),sem);
	Sleep((int)IpParam);//参数强转 
	printf("threadId=%d end\n",GetCurrentThreadId());
	int sem_num;
	ReleaseSemaphore(sem,1,&sem_num);//V
	printf("now %d sem available\n",sem_num+1);//sem_num是释放前 
}

void main(void)
{
	//总100,同时只有5
	sem=CreateSemaphore(NULL,5,5,NULL);//初值5,最大5,匿名
	HANDLE array[101];
	for(int total=100;total>0;total--)
	{
		HANDLE thread=CreateThread(NULL,0,func,total*5,0,NULL);//暂停时间=total*5 
		array[total-1]=thread;
		//CloseHandle(thread);
	}
	
	//WaitForMultipleObjects(100,array,TRUE,INFINITE);没用?
	Sleep(10000);
	
	return 0;
}

刚开始有5个进程获取了信号,等他们中有一个结束,就会释放,此时马上另一个进程获取。这样,sem保持在0,1之间,最后,进程都执行完了,sem数值会回升。


文件映射

Linux通过在内存中共享一片区域实现进程间大量通信,而Windows通过使用一个临时文件来实现进程大量通信。


//打开/创建文件映射(shmget)
HANDLE  CreateFileMapping(
	HANDLE  hFile,   //欲创建映射的文件句柄,如果是INVALID_HANDLE_VALUE就会创建临时文件对象
	LPSECURITY_ATTRIBUTES  lpAttributes,
	DWORD  flProtect,  //读/写保护参数
	DWORD  dwMaximumSizeHigh,  //高32位
	DWORD  dwMaximumSizeLow,  //低32位,两个都为0就代表磁盘文件的实际长度
	LPCTSTR  lpName);  //对象的名字


//打开一个文件映射
HANDLE  OpenFileMapping (
	DWORD  dwDesiredAccess, //存取访问方式
	BOOL  bInheritHandle,  //继承标记
	LPCTSTR  lpName);      //文件映射对象名称

//在当前进程中打开文件映射的一个视图(shmat)
LPVOID  MapViewOfFile(
	HANDLE  hFileMappingObject, //对象句柄
	DWORD  dwDesiredAccess,  //指定访问权限
	DWORD  dwFileOffsetHigh,  //文件内映射起点
	DWORD  dwFileOffsetLow,  //文件内映射起点
	SIZE_T  dwNumberOfBytesToMap); //文件中要映射的字节数。用0映射整个文件映射对象
//返回值:文件映射的起始地址,void*

//解除映射(shmdt)
BOOL UnmapViewOfFile(
			LPCVOID lpBaseAddress);

例子就不给了,后面直接生产者消费者进程开干就完事了。

储存器管理API

没啥可说的,就纯纯获取结构信息,输出

详见代码阶段

文件操作API

有点类似于我们用户级别的文件操作,具体直接走代码。

详见代码阶段

实验一:Linux内核编译

可以参考下面的讲解,该讲解来自于林东方 ,我这里直接拿来了。

@Felix and Phoenix的主页

如果你是ubantu,可以按照下面的参考来,一步一步来就行,很简单。

ubantu编译内核

注意事项

  1. 最重要的一点——Linux内核编译安装后 高达10~20G ,创建虚拟机时分配 40G硬盘 能够保证后续实验 无存储相关的bug,不然后续要么选择扩容(有点麻烦,笔者不会),要么重装虚拟机(重装了n 次,笔者很会)
  2. gcc版本和linux内核版本要兼容 ,否则会出现源码无法编译的问题,要么选择升级gcc(笔者试过 了,编译安装好久,还是换低版本的linux源码好),要么换低版本的linux
  3. 建议将linux内核源码下载到 用户根目录 下,即/home/[username],例如,用户名为phoenix, 则 用户根目录为/home/phoenix,避免后续的一系列读写权限问题(笔者血的教训)

实验环境

VMware16中文版软件下载和安装教程|兼容WIN10
Hadoop入门(一)——CentOS7下载+VM上安装(手动分区)图文步骤详解(2021)

VMware下载链接
CentOS下载链接

实验步骤

使用镜像源下载linux源码

wget https://mirror.bjtu.edu.cn/kernel/linux/kernel/v5.x/linux-5.4.69.tar.gz

解压linux-5.4.69.tar.gz

tar zxvf linux-5.4.69.tar.gz

进入解压后的文件

cd linux-5.4.69

复制本机的配置文件

cp /boot/config- `uname -r`   ./.config

编译前的环境准备

yum install gcc make ncurses-devel openssl-devel flex bison  elfutils-libelf-devel  -y # 安装编译依赖
yum upgrade -y # 升级所有软件

基于文本选单的配置界面,默认即可

make menuconfig
# save->ok->exit->exit

内核全开,编译与模块编译及安装,大概需要30-60min

make -j6 && make modules_install -j6 && make install -j6

修改引导菜单

gedit /boot/grub2/grub.cfg
  1. 用gedit打开配置文件
  2. Ctrl+F搜索menuentry
  3. 找到menuentry后,后面的字符串就是默认的版本号,修改为班号、学号、姓名及版本号
  4. 保存退出
  5. 重启,即可在引导菜单看到修改后的班号、学号、姓名及版本号

参考博客

【linux内核源码分析】详解Linux内核编译配置(menuconfig)、文件系统制作
Linux下更新GCC
Linux centos7升级内核(两种方法:内核编译和yum更新)
【 GRUB 】修改启动列表项,自定义列表项内容,添加自定义GRUB主题

实验二:生产者消费者进程

  1. 一个大小为3的缓冲区,初始为空
  2. 2个生产者
    • 随机等待一段时间,往缓冲区添加数据,
    • 若缓冲区已满,等待消费者取走数据后再添加
    • 重复6次
  3. 3个消费者
    • 随机等待一段时间,从缓冲区读取数据
    • 若缓冲区为空,等待生产者添加数据后再读取
    • 重复4次
  4. 说明:
    • 显示每次添加和读取数据的时间及缓冲区里的数据
    • 生产者和消费者用进程模拟
    • Linux和Windows都做

Linux版本

头文件

Def.h中,使用宏对各种参数进行声明,便于后期调节。同时把信号量集中的索引也进行了宏替换,防止信号量编程出逻辑bug。最后,定义了缓冲区结构体MyBuffer,以此结构大小创建共享内存区,并使用指针类型转换实现对共享内存的灵活使用。Def.h中还用了一个小技巧,就是include保护,使用ifndef与define结合,防止多次include出现链接错误。

#ifndef DEF_H
#define DEF_H

#include<stdio.h>//标准库 
#include<time.h>
#include<string.h> 
#include<stdlib.h> 

#include<unistd.h>//进程库
#include<sys/ipc.h>
#include<sys/shm.h>
#include<sys/sem.h>
#include<sys/wait.h>

//进程个数
#define PRO_NUM 2
#define CON_NUM 3

//重复次数
#define PRO_REP 6
#define CON_REP 4

//缓冲区大小 
#define BUF_LEN 11
#define BUF_CNT 3

//内存,信号量key 
#define SHM_KEY 1234
#define SEM_KEY 1235
#define MUTEX 0
#define EMPTY 1
#define FULL 2

//模式,可读可写 
#define MODE 0600 

//缓冲区结构
struct MyBuffer
{
	char str[BUF_CNT][BUF_LEN];
	int head;
	int tail;
};

#endif //DEF_H

主程序代码

在Main.c中,首先编写两个辅助函数randMod和randString,用于随机数采样以及随机字符串采样。之后编写PV封装。最后编写进程函数,init为创建内存区,格式化,以及创建信号量集,pro为生产者进程,con为消费者进程。虽然使用两个函数会导致一些代码的冗余,但是胜在逻辑清晰。

在生产者进程中,首先用随机数确定休眠时间,然后通过shmget和semget获取共享内存和信号量集id。之后进行PV操作以及循环队列的读写。注意P(full)一定在P(mutex前),否则会产生死锁:

#include "def.h"

//辅助函数

int randMod(int mod)// 随机获取范围内整数 
{
	return rand() % mod;
}


char *randString()// 得到一个字符串,长度随机,内容随机 
{
    static char buf[BUF_LEN];//static重复使用 
    memset(buf, 0, sizeof(buf));
    int n = randMod(10) + 1;
    for (int i = 0; i < n; i++)
        buf[i] = (char)(randMod(26) + 'A');
    return buf;
}

// pv封装

void P(int sem_id, int sem_num) //P
{
   struct sembuf xx;
   xx.sem_num = sem_num;
   xx.sem_op = -1;
   xx.sem_flg = 0;
   semop(sem_id, &xx, 1);
}

void V(int sem_id, int sem_num) //V
{
   struct sembuf xx;
   xx.sem_num = sem_num;
   xx.sem_op = 1;
   xx.sem_flg = 0;
   semop(sem_id, &xx, 1);
}

//进程函数
void init() //初始化 
{
	//信号量 
	int semid=semget(SEM_KEY,3,IPC_CREAT|MODE);
	if(semid<0)
	{
		printf("sem err\n");
		exit(1);
	}
	semctl(semid,MUTEX,SETVAL,1);
	semctl(semid,EMPTY,SETVAL,BUF_CNT);
	semctl(semid,FULL,SETVAL,0);
	
	//共享内存 
	int shmid=shmget(SHM_KEY,sizeof(struct MyBuffer),IPC_CREAT|MODE);
	if(shmid<0)
	{
		printf("shm err\n");
		exit(1);
	}	
	
	//清空内存
	struct MyBuffer* shmptr=shmat(shmid,0,0);
	if(shmptr<0)
	{
		printf("shmat err\n");
		exit(1);
	}
	memset(shmptr,0,sizeof(struct MyBuffer));
	shmdt(shmptr);
}

void pro() //生产者 
{
	srand((unsigned)getpid());//以pid作为seed
	//获取已有信号量和共享内存 
	//信号量 
	int semid=semget(SEM_KEY,3,IPC_CREAT|MODE);
	if(semid<0)
	{
		printf("sem err\n");
		exit(1);
	}
	//共享内存 
	int shmid=shmget(SHM_KEY,sizeof(struct MyBuffer),IPC_CREAT|MODE);
	if(shmid<0)
	{
		printf("shm err\n");
		exit(1);
	}
	struct MyBuffer* shmptr=shmat(shmid,0,0);
	if(shmptr<0)
	{
		printf("shmat err\n");
		exit(1);
	}
	//重复向储存区写入
	//struct timespec begin;//精确获取时间 
	//struct timespec end;
	for(int i=0;i<PRO_REP;i++)
	{
		//clock_gettime(1,&begin);//记录初始时间 
		P(semid,EMPTY);//P
		P(semid,MUTEX);
		usleep(randMod(1e6));//随机等待
		strncpy(shmptr->str[shmptr->tail],randString(),BUF_LEN);//写入 
		printf("[pid %d] push %-10s ",getpid(),shmptr->str[shmptr->tail]);
		shmptr->tail=(shmptr->tail+1)%BUF_CNT;
		for(int j=0;j<BUF_CNT;j++)//输出当前缓冲区状态
		{
			printf("|%-10s",shmptr->str[j]);
		}
		printf("|\n");
		//fflush(stdout);//清空输出缓冲 
		V(semid,FULL);//V
		V(semid,MUTEX);
		//clock_gettime(1,&end);//获取最终时间,输出耗时
		//double duration=(end.tv_sec-begin.tv_sec)*1000+(end.tv_nsec-begin.tv_nsec)/1000000;
		//printf(" running time:%lf ms\n",duration);
	} 
	
	exit(0);
} 

void con() //消费者 
{
	srand((unsigned)getpid());//以pid作为seed
	//获取已有信号量和共享内存 
	//信号量 
	int semid=semget(SEM_KEY,3,IPC_CREAT|MODE);
	if(semid<0)
	{
		printf("sem err\n");
		exit(1);
	}
	//共享内存 
	int shmid=shmget(SHM_KEY,sizeof(struct MyBuffer),IPC_CREAT|MODE);
	if(shmid<0)
	{
		printf("shm err\n");
		exit(1);
	}
	struct MyBuffer* shmptr=shmat(shmid,0,0);
	if(shmptr<0)
	{
		printf("shmat err\n");
		exit(1);
	}
	//重复从储存区读取 
	//struct timespec begin;//精确获取时间 
	//struct timespec end;
	for(int i=0;i<CON_REP;i++)
	{
		//clock_gettime(1,&begin);//记录初始时间 
		P(semid,FULL);//P
		P(semid,MUTEX);
		usleep(randMod(1e6));//随机等待
		printf("[pid %d] pop %-10s ",getpid(),shmptr->str[shmptr->head]);//读取 
		memset(shmptr->str[shmptr->head],0,sizeof(BUF_LEN));
		shmptr->head=(shmptr->head+1)%BUF_CNT;
		for(int j=0;j<BUF_CNT;j++)//输出当前缓冲区状态
		{
			printf("|%-10s",shmptr->str[j]);
		}
		printf("|\n");
		//fflush(stdout);//清空输出缓冲 
		V(semid,EMPTY);//V
		V(semid,MUTEX);
		//clock_gettime(1,&end);//获取最终时间,输出耗时
		//double duration=(end.tv_sec-begin.tv_sec)*1000+(end.tv_nsec-begin.tv_nsec)/1000000;
		//printf(" running time:%lf ms\n",duration);
	} 
	
	exit(0);
} 


int main(void)
{
	init();
	for(int i=0;i<PRO_NUM+CON_NUM;i++) 
	{
		pid_t pid=fork();
		if(pid<0)
		{
			printf("fork err\n");
			exit(1);
		}
		else if(pid==0)
		{
			//根据数量分割大循环 
			if(i<PRO_NUM)//生产者 
			{
				printf("create pro\n");
				pro();
			}
			else //消费者 
			{
				printf("create con\n");
				con();
			}	
		}
	}
	for(int i=0;i<PRO_NUM+CON_NUM;i++)//等待所有子进程 
	{
		wait(NULL);
	} 
}

函数框架

上面的代码比较多,这里给出基本框架。

这里重点说一下main函数的实现逻辑:
Main函数的核心在于,如何用fork创建多个多种进程。一种思路是建立两个循环,另一种思路是建立一个循环+计数判断。无论是哪一种方式,都需要在子进程函数的最后加上exit函数,否则会引发错误。总的来说,需要加深对fork的理解,fork本身是创建子进程后,子进程再把fork后的代码都执行一次,加上exit可以有效截断fork的执行,将我们执行的代码限制在我们想要的区域。

void pro() //生产者 
{
	printf("pro\n");
	exit(0);
	
} 

void con() //消费者 
{
	printf("con\n");
	exit(0);
} 


int main(void)
{
	init();
	for(int i=0;i<PRO_NUM;i++) 
	{
		pid_t pid=fork();
		if(pid<0)
		{
			printf("fork err\n");
			exit(1);
		}
		else if(pid==0)
		{
			pro();
		}
	}
	for(int i=0;i<CON_NUM;i++)
	{
		pid_t pid=fork();
		if(pid<0)
		{
			printf("fork err\n");
			exit(1);
		}
		else if(pid==0)
		{
			con();
		}
	}
}

fork结构

无论是两个循环,还是单循环+数量控制,都需要在进程函数最后加exit,这样虽然fork是复制了后面所有代码的,但是因为exit阻断,进程代码实际上只是fork后到exit前的一部分。

如果不加exit,就会出现下面的情况:


函数框架里给的做法是双循环,我们这里给出单循环+if else控制数量:

int main(void)
{
	init();
	for(int i=0;i<PRO_NUM+CON_NUM;i++) 
	{
		pid_t pid=fork();
		if(pid<0)
		{
			printf("fork err\n");
			exit(1);
		}
		else if(pid==0)
		{
			if(i<PRO_NUM)//根据数量分割大循环 
			{
				pro();
			}
			else
			{
				con();
			}	
		}
	}
}

结果

Windows版本

windows和linux大同小异。基本流程完全可以对照。解释都在代码里,这里不多赘述了。

不过,这里的Wait就运行正常(进程),而我前面用thread写的就不会阻塞,这大概是因为进程和线程不太一样吧。

头文件

#ifndef DEF_H
#define DEF_H

#include<stdio.h>//标准库 
#include<time.h>
#include<string.h> 
#include<stdlib.h> 

#include<windows.h>//系统库 

//进程个数
#define PRO_NUM 2
#define CON_NUM 3

//重复次数
#define PRO_REP 6
#define CON_REP 4

//缓冲区大小 
#define BUF_LEN 11
#define BUF_CNT 3

//内存,信号量key 
#define SHM_KEY 1234
#define SEM_KEY 1235
#define MUTEX 0
#define EMPTY 1
#define FULL 2

//模式,可读可写 
#define MODE 0600 

// 定义共享内存相关信息
const TCHAR szFileMappingName[] = TEXT("PCFileMappingObject");
const TCHAR szMutexName[] = TEXT("PCMutex");
const TCHAR szSemaphoreEmptyName[] = TEXT("PCSemaphoreEmpty");
const TCHAR szSemaphoreFullName[] = TEXT("PCSemaphoreFull");

//缓冲区结构
struct MyBuffer
{
	char str[BUF_CNT][BUF_LEN];
	int head;
	int tail;
};

//时间变量 
LARGE_INTEGER start_time, end_time;
LARGE_INTEGER freq;
double running_time;

#endif //DEF_H

main.c

#include "def.h"

int main()
{
    HANDLE hMapFile;
    BOOL result;
    DWORD pid = GetCurrentProcessId();
    //创建文件映射 
    hMapFile = CreateFileMapping(
        INVALID_HANDLE_VALUE,  // 临时文件对象 
        NULL,                  
        PAGE_READWRITE,        // 全部权限 
        0,                     // 最小空间 
        sizeof(struct MyBuffer), // 最大空间 
        szFileMappingName);    // 使用定义好的const常量 
    if (hMapFile == NULL)
    {
        printf("Mapping Failed!\n");
        return 1;
    }
    // 创建Mutex ,匿名初值为1(FALSE) 
    HANDLE hMutex = CreateMutex(NULL, FALSE, szMutexName);
    if (hMutex == NULL)
    {
        printf("Mutex Failed!\n");
        return 1;
    }
    // 创建Semaphore empty,初值3,最大3 
    HANDLE hSemaphoreEmpty = CreateSemaphore(NULL, 3, 3, szSemaphoreEmptyName);
    if (hSemaphoreEmpty == NULL)
    {
        printf("Empty Failed!\n");
        return 1;
    }
    // 创建Semaphore full,初值0,最大3 
    HANDLE hSemaphoreFull = CreateSemaphore(NULL, 0, 3, szSemaphoreFullName);
    if (hSemaphoreFull == NULL)
    {
        printf("Full Failed!\n");
        return 1;
    }
    // 打开文件映射,清零 
    struct MyBuffer* pBuf = (struct MyBuffer*)MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS,
														 0, 0, sizeof(struct MyBuffer));
    if (pBuf == NULL)
    {
        printf("View Failed\n");
        CloseHandle(hMapFile);
        return 1;
    }
    memset(pBuf, 0, sizeof(struct MyBuffer));
    UnmapViewOfFile(pBuf);//断开连接 
    pBuf = NULL;

	//创建进程(准备信息) 
    PROCESS_INFORMATION pi[PRO_NUM+CON_NUM] = { 0 };//进程信息 
    STARTUPINFO si[PRO_NUM+CON_NUM] = { 0 };//进程信息 
    for (int i = 0; i < PRO_NUM+CON_NUM; i++)//初始化STARTUPINFO 
    {
        si[i].cb = sizeof(STARTUPINFO);
    }
    //创建 生产者 
    TCHAR ProducerName[] = TEXT("producer.exe");
    TCHAR ConsumerName[] = TEXT("consumer.exe");
    for (int i = 0; i < PRO_NUM; i++)
    {
        result = CreateProcess(NULL, ProducerName,
            NULL, NULL, TRUE,
            NORMAL_PRIORITY_CLASS,
            NULL, NULL, &si[i], &pi[i]);
        if (!result) // fail
        {
            printf("Could not create producer process.\n");
            return 1;
        }
    }

    //创建 消费者 
    for (int i = PRO_NUM; i < PRO_NUM+CON_NUM; i++)
    {
        result = CreateProcess(NULL, ConsumerName,
            NULL, NULL, TRUE,
            NORMAL_PRIORITY_CLASS,
            NULL, NULL, &si[i], &pi[i]);
        if (!result) // fail
        {
            printf("Could not create consumer process.\n");
            return 1;
        }
    }
    //阻塞进程 
    HANDLE hProcesses[PRO_NUM+CON_NUM];
    DWORD ExitCode;
    for (int i = 0; i < PRO_NUM+CON_NUM; i++)
    {
        hProcesses[i] = pi[i].hProcess;
    }
    // wait...
    WaitForMultipleObjects(PRO_NUM+CON_NUM, hProcesses, TRUE, INFINITE);
    printf("exit!\n");
    //释放句柄 
    for (int i = 0; i < PRO_NUM+CON_NUM; i++)
    {
        if (pi[i].hProcess == 0)
            exit(-1);
        result = GetExitCodeProcess(pi[i].hProcess, &ExitCode);
        CloseHandle(pi[i].hProcess);
        CloseHandle(pi[i].hThread);
    }
    CloseHandle(hMapFile);
    return 0;
}

producer.c

#include"def.h"

//辅助函数 
int randMod(int mod)//随机数 
{
	return rand()%mod;
}

char *randString()// 得到一个字符串,长度随机,内容随机 
{
    static char buf[BUF_LEN];//static重复使用 
    memset(buf, 0, sizeof(buf));
    int n = randMod(10) + 1;
    for (int i = 0; i < n; i++)
        buf[i] = (char)(randMod(26) + 'A');
    return buf;
}

int main(void)
{
	HANDLE hMapFile;
	struct MyBuffer* pBuf;
	int pid = GetCurrentProcessId();
	srand(pid);
 
	//shmget获取映射 OpenFileMapping
    hMapFile = OpenFileMapping(
        FILE_MAP_ALL_ACCESS,//全部权限 
        FALSE,      		//不继承 
        szFileMappingName);//使用前面定义的const常量 
    if (hMapFile == NULL)
    {
        printf("Mapping Failed!\n");
        return 1;
    }
	//shmat获取地址 MapViewOfFile
    pBuf = (struct MyBuffer*)MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 
											0, 0, sizeof(struct MyBuffer));
    if (pBuf == NULL)
    {
        printf("View Failed!\n");
        CloseHandle(hMapFile);
        return 1;
    }

    //打开Mutex ,使用const常量的ipName 
    HANDLE hMutex = OpenMutex(MUTEX_ALL_ACCESS, TRUE, szMutexName);
    if (hMutex == NULL)
    {
        printf("Mutex Failed!\n");
        return 1;
    }
    //打开Empty 
    HANDLE hSemaphoreEmpty = OpenSemaphore(SEMAPHORE_ALL_ACCESS, TRUE, szSemaphoreEmptyName);
    if (hSemaphoreEmpty == NULL)
    {
        printf("Emtpy Failed!\n");
        return 1;
    }
    //打开FULL 
    HANDLE hSemaphoreFull = OpenSemaphore(SEMAPHORE_ALL_ACCESS, TRUE, szSemaphoreFullName);
    if (hSemaphoreFull == NULL)
    {
        printf("Full Failed!\n");
        return 1;
    }

    //写入 
    int sleepTime;
    for (int i = 0; i < PRO_REP; i++)
    {
        QueryPerformanceCounter(&start_time);
        sleepTime = rand() % 1000;
        // p(empty)
        WaitForSingleObject(hSemaphoreEmpty, INFINITE);
        // p(mutex)
        WaitForSingleObject(hMutex, INFINITE);
        // sleep
        Sleep(sleepTime);

        // 写入 
        char* s = pBuf->str[pBuf->tail];
        strcpy_s(s, BUF_LEN, randString());
        pBuf->tail = (pBuf->tail + 1) % BUF_CNT;

        printf("[pid %d] push %-10s ", pid, s);
        
        // 显示缓冲区 
        for (int cnt = 0; cnt < BUF_CNT ; cnt++)
                printf("|%-10s", pBuf->str[cnt]);
        printf("|");

        QueryPerformanceCounter(&end_time);
        QueryPerformanceFrequency(&freq);
        running_time = (double)(end_time.QuadPart - start_time.QuadPart) / freq.QuadPart;
        printf(" running time:%lf ms\n", running_time);
        
        // v(mutex)
        ReleaseMutex(hMutex);
        // v(full)
        ReleaseSemaphore(hSemaphoreFull, 1, NULL);
    }

    // release resources
    CloseHandle(hSemaphoreEmpty);
    CloseHandle(hSemaphoreFull);
    CloseHandle(hMutex);
    UnmapViewOfFile(pBuf);
    CloseHandle(hMapFile);
    return 0;
}

consumer.c

#include"def.h"

//辅助函数 从procuder.c里拉取 

int main(void)
{
	HANDLE hMapFile;
	struct MyBuffer* pBuf;
	int pid = GetCurrentProcessId();
	srand(pid);
 
	//shmget获取映射 OpenFileMapping
    hMapFile = OpenFileMapping(
        FILE_MAP_ALL_ACCESS,//全部权限 
        FALSE,      		//不继承 
        szFileMappingName);//使用前面定义的const常量 
    if (hMapFile == NULL)
    {
        printf("Mapping Failed!\n");
        return 1;
    }
	//shmat获取地址 MapViewOfFile
    pBuf = (struct MyBuffer*)MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 
											0, 0, sizeof(struct MyBuffer));
    if (pBuf == NULL)
    {
        printf("View Failed!\n");
        CloseHandle(hMapFile);
        return 1;
    }

    //打开Mutex ,使用const常量的ipName 
    HANDLE hMutex = OpenMutex(MUTEX_ALL_ACCESS, TRUE, szMutexName);
    if (hMutex == NULL)
    {
        printf("Mutex Failed!\n");
        return 1;
    }
    //打开Empty 
    HANDLE hSemaphoreEmpty = OpenSemaphore(SEMAPHORE_ALL_ACCESS, TRUE, szSemaphoreEmptyName);
    if (hSemaphoreEmpty == NULL)
    {
        printf("Emtpy Failed!\n");
        return 1;
    }
    //打开FULL 
    HANDLE hSemaphoreFull = OpenSemaphore(SEMAPHORE_ALL_ACCESS, TRUE, szSemaphoreFullName);
    if (hSemaphoreFull == NULL)
    {
        printf("Full Failed!\n");
        return 1;
    }

    //写入 
    int sleepTime;
    for (int i = 0; i < CON_REP; i++)
    {
        QueryPerformanceCounter(&start_time);
        sleepTime = rand() % 1000;
        // p(full)
        WaitForSingleObject(hSemaphoreFull, INFINITE);
        // p(mutex)
        WaitForSingleObject(hMutex, INFINITE);

		// sleep
        Sleep(sleepTime);
        // 读取 
        char* s = pBuf->str[pBuf->head];
        printf("[pid %d] pop  %-10s ", pid, s);
        memset(s, 0, sizeof(pBuf->str[pBuf->head]));//TODO//清空 
        pBuf->head = (pBuf->head + 1) % BUF_CNT;
        
        // 显示缓冲区 
        for (int cnt = 0; cnt < BUF_CNT ; cnt++)
                printf("|%-10s", pBuf->str[cnt]);
        printf("|");

        QueryPerformanceCounter(&end_time);
        QueryPerformanceFrequency(&freq);
        running_time = (double)(end_time.QuadPart - start_time.QuadPart) / freq.QuadPart;
        printf(" running time:%lf ms\n", running_time);
        
        // v(mutex)
        ReleaseMutex(hMutex);
        // v(empty)
        ReleaseSemaphore(hSemaphoreEmpty, 1, NULL);
    }

    // release resources
    CloseHandle(hSemaphoreEmpty);
    CloseHandle(hSemaphoreFull);
    CloseHandle(hMutex);
    UnmapViewOfFile(pBuf);
    CloseHandle(hMapFile);
    return 0;
}

结果

实验三:内存监控

实验三 内存和进程地址空间实时显示(5分)

设计一个内存监视器,能实时地显示当前系统中内存的使用情况,包括物理内存的使用情况;能实时显示某个进程的虚拟地址空间布局信息等等。

相关的系统调用:

GetSystemInfo, VirtualQueryEx, GetPerformanceInfo, GlobalMemoryStatusEx …

这一章比较简单,因为就是简单的调用接口,返回信息到结构体中,然后输出。难点反而是在于,返回信息的理解与输出格式的调整。

运行结果

使用Visual Studio 2019编写调试:

查看帮助:

查看整体信息:


查看具体信息:



主函数

这个程序仿照了很多linux命令程序的格式。就是那种先进入程序运行模式,之后一直让你输入命令,输一个命令,做一个显示,想退出就exit。

主函数有几个点:

  1. setlocale。这个可能有用,是和语言编码有关的,尤其是中文显示。
  2. 用一个string类型储存cmd
  3. while(1)循环不断询问指令,之后进行多分支判断,分别调用对应的显示函数
  4. pid指令比较特殊,它是分段的,输入pid后进入pid模式,再输一个pid后显示。其实设计的时候也可以用“pid+数字”这种命令来直接一段实现
#include <windows.h>
#include <iostream>
#include <iomanip>
#include <string>
#include <Tlhelp32.h>
#include <stdio.h>
#include <tchar.h>
#include <shlwapi.h>
#include <psapi.h>

#pragma comment(lib, "shlwapi.lib")
#pragma comment(lib,"kernel32.lib")

using namespace std;

//声明
void printProtection(DWORD dwTarget);
void displaySystemConfig(void);
void displayMemoryCondition(void);
void getAllProcessInformation(void);
void ShowHelp(void);
void getProcessDetail(int pid);



int main()
{
	//设置显示语言
	setlocale(LC_ALL, "CHS");
	//初始化输出 
	cout << endl << "*-----------内存管理(1120200944)-----------*" << endl << endl;
	cout << "输入help可查询帮助" << endl << endl;
	string cmd;
	char cmd_charstr[127];
	//循环询问 
	while (1)
	{
		//获取输入 
		cout << "请输入指令> ";
		cin.getline(cmd_charstr, 127);
		cmd = cmd_charstr;
		//判断命令
		if (cmd == "system") {
			cout << endl;
			displaySystemConfig();
		}
		else if (cmd == "memory") {
			cout << endl;
			displayMemoryCondition();
		}
		else if (cmd == "process") {
			cout << endl;
			getAllProcessInformation();
		}
		else if (cmd == "pid") {
			cout << "PID> ";
			int pid = 0;
			cin >> pid;
			cin.getline(cmd_charstr, 127);
			if (pid <= 0) continue;
			cout << endl;
			getProcessDetail(pid);
		}
		else if (cmd == "help") {
			cout << endl;
			ShowHelp();
		}
		else if (cmd == "exit") {
			break;
		}
		else if (cmd == "clear" || cmd == "cls") {
			system("cls");
		}
		else {
			if (cmd != "") cout << "非法命令,请使用\"help\"命令查看提示" << endl;
			fflush(stdin);
			cin.clear();
			continue;
		}
		cin.clear();

	}
	return 0;
}

辅助函数

非核心函数。

显示权限信息:printProtection

dwTarget一般是mbi.Protect,这个本质上是一个二进制串,我们可以通过掩码操作+位运算取得串上的任何一位,每一位都代表某一个权限打开还是关闭。

掩码是一些宏变量,用于取权限位。

#define PAGE_NOACCESS           0x01    //0000 0001
#define PAGE_READONLY           0x02    //0000 0010
#define PAGE_READWRITE          0x04    //0000 0100 
#define PAGE_WRITECOPY          0x08    //0000 1000
//输出权限保护级别
void printProtection(DWORD dwTarget)
{
	char as[] = "----------";
	if (dwTarget & PAGE_NOACCESS) as[0] = 'N';
	if (dwTarget & PAGE_READONLY) as[1] = 'R';
	if (dwTarget & PAGE_READWRITE)as[2] = 'W';
	if (dwTarget & PAGE_WRITECOPY)as[3] = 'C';
	if (dwTarget & PAGE_EXECUTE) as[4] = 'X';
	if (dwTarget & PAGE_EXECUTE_READ) as[5] = 'r';
	if (dwTarget & PAGE_EXECUTE_READWRITE) as[6] = 'w';
	if (dwTarget & PAGE_EXECUTE_WRITECOPY) as[7] = 'c';
	if (dwTarget & PAGE_GUARD) as[8] = 'G';
	if (dwTarget & PAGE_NOCACHE) as[9] = 'D';
	if (dwTarget & PAGE_WRITECOMBINE) as[10] = 'B';
	printf("  %s  ", as);
}

显示帮助:showHelp

打印命令以及其对应的含义。

void showHelp(void)
{
	cout << "--------------------------------------------------------------------------" << endl;
	cout << "命令类型: " << endl
		<< "\"system\"   : 显示计算机整体信息" << endl
		<< "\"memory\": 显示内存信息" << endl
		<< "\"process\"  : 显示活跃进程信息" << endl
		<< "\"pid\"      : 查看某一进程具体信息" << endl
		<< "\"help\"     : 显示帮助" << endl
		<< "\"exit\"     : 退出程序" << endl;
	cout << "--------------------------------------------------------------------------" << endl;
	return;
}

核心函数

显示系统信息:displaySystem

这里需要注意一个函数:StrFormatByteSize,这个函数将DWORD型的大小值,转化为合适的KB,MB,GB,长度可以规定,很方便。

//显示系统整体信息
void displaySystem(void)
{
	SYSTEM_INFO si;
	ZeroMemory(&si,sizeof(si));
	GetSystemInfo(&si);//获取系统信息 

	TCHAR str_page_size[MAX_PATH];
	StrFormatByteSize(si.dwPageSize, str_page_size, MAX_PATH);//自动计算显示格式(KB,MB,GB)

	DWORD memory_size = (DWORD)si.lpMaximumApplicationAddress - (DWORD)si.lpMinimumApplicationAddress;
	TCHAR str_memory_size[MAX_PATH];
	StrFormatByteSize(memory_size, str_memory_size, MAX_PATH);

	cout << "计算机整体信息:" << endl;
	cout << "--------------------------------------------" << endl;
	cout << "处理器架构         | " << (si.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_AMD64 || si.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_INTEL ? "x64" : "x86") << endl;
	cout << "内核数量           | " << si.dwNumberOfProcessors << endl;
	cout << "内存页大小         | " << str_page_size << endl;
	cout << "用户最低地址       | 0x" << hex << setfill('0') << setw(8) << (DWORD)si.lpMinimumApplicationAddress << endl;
	cout << "用户最高地址       | 0x" << hex << setw(8) << (DWORD)si.lpMaximumApplicationAddress << endl;
	cout << "用户可用内存       | " << str_memory_size << endl;
	cout << "--------------------------------------------" << endl;
	return;

}

显示内存信息:displayMemory

因为内存信息普遍比较大,所以干脆就都用GB表示了。

// 显示系统内存信息
void displayMemory(void)
{
	long MB = 1024 * 1024;//1M
	long GB = MB * 1024;//1G
	MEMORYSTATUSEX stat;
	stat.dwLength = sizeof(stat);
	GlobalMemoryStatusEx(&stat);//获取内存信息
	

	cout << "计算机内存信息:" << endl;
	cout << "--------------------------------------------" << endl;
	cout<< "内存使用率          | " << setbase(10) << stat.dwMemoryLoad << "%\n"
		<< "物理内存总量        | " << setbase(10) << (float)stat.ullTotalPhys / GB << "GB\n"
		<< "可用物理内存        | " << setbase(10) << (float)stat.ullAvailPhys / GB << "GB\n"
		<< "总页面大小          | " << (float)stat.ullTotalPageFile / GB << "GB\n"
		<< "进程可获取页面大小  | " << (float)stat.ullAvailPageFile / GB << "GB\n"
		<< "虚拟内存总量        | " << (float)stat.ullTotalVirtual / GB  << "GB\n"
		<< "可用虚拟内存        | " << (float)stat.ullAvailVirtual / GB << "GB" << endl;
	cout << "--------------------------------------------" << endl;
}

获取活跃进程:getAllProcess

这一部分大概是比较有技术含量的一个了。

首先获取系统活跃进程的快照。
之后遍历快照,先用32First,之后在while循环里用32Next,有一种链表的感觉。

// 获取所有进程信息
void getAllProcess(void)
{
	cout << "所有进程信息:" << endl;

	HANDLE hProcessShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);//创建快照
	if (hProcessShot == INVALID_HANDLE_VALUE)//创建失败
	{
		cout << "创建快照失败!请重试!" << endl;
		return;//结束当前函数
	}
	//遍历快照
	cout << " |  序号  |  pid  |  进程名" << endl;
	cout << "-----------------------------------------" << endl;
	PROCESSENTRY32 pe32;
	pe32.dwSize = sizeof(pe32);
	bool more= Process32First(hProcessShot, &pe32);//获取第一个进程
	int process_num = 1;
	while(more)//遍历获取到没有进程为止
	{
		printf(" | %4d  | %5d  |  %s\n", process_num++,
			pe32.th32ProcessID, pe32.szExeFile);

		more=Process32Next(hProcessShot, &pe32);//获取下一个
	}
	cout << "-----------------------------------------" << endl;
	CloseHandle(hProcessShot);//关闭快照
}

获取进程具体信息:getProcessDetail

VirtualQueryEx,这个函数的四个参数,有人刚拿到可能会比较迷惑。
按理来说,给个进程句柄,不久可以一次性把所有内存块信息获取到吗?其实这样的成本比较高,你去自定义获取就好了,所以这个函数只会返回一个区域的信息。

这个函数,给定进程handle与基地址,返回从基地址开始的,第一个属于handle的区域。

所以要想获取进程所有的区域,需要遍历所有的基地址。好在可以通过基地址+区域大小来进行跳跃,大幅缩短遍历时间。

//显示进程具体信息
void getProcessDetail(int pid)
{
	HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, 0, pid);
	if (!hProcess) return;
	cout << " | "
		<< "   Memory Addr    | "
		<< "   Size    | "
		<< "PageStatus| "
		<< "    Protect    | "
		<< "  Type  | "
		<< " ModuleName"
		<< endl;

	SYSTEM_INFO si;					// 系统信息
	ZeroMemory(&si, sizeof(si));
	GetSystemInfo(&si);

	MEMORY_BASIC_INFORMATION mbi;
	ZeroMemory(&mbi, sizeof(mbi));

	LPCVOID pBlock = (LPVOID)si.lpMinimumApplicationAddress;//从最低内存遍历进程所有内存
	while (pBlock < si.lpMaximumApplicationAddress) 
	{
		//给定进程句柄,从pBlock开始查询,将检查到的第一个内存区域信息存到mbi中
		VirtualQueryEx(hProcess, pBlock, &mbi, sizeof(mbi));
		LPCVOID pEnd = (PBYTE)pBlock + mbi.RegionSize;
		// 区域大小		
		TCHAR szSize[MAX_PATH];
		StrFormatByteSize(mbi.RegionSize, szSize, MAX_PATH); //size of block

		// 地址区间与区域大小
		cout.fill('0');
		cout<<" | " << hex << setw(8) << (DWORD)pBlock
			<< "-"
			<< hex << setw(8) << (DWORD)pEnd - 1
			<< " | ";
		printf("%11s", szSize);

		// 输出块状态,提交,空闲,保留。
		switch (mbi.State)
		{

		case MEM_COMMIT:cout << " | " << setw(9) << "Committed" << " | "; break;
		case MEM_FREE:cout << " | " << setw(9) << "   Free  " << " | "; break;
		case MEM_RESERVE:cout << " | " << setw(9) << " Reserved" << " | "; break;
		default: cout << "   None   | "; break;
		}

		// 保护状态
		if (mbi.Protect == 0 && mbi.State != MEM_FREE)
		{

			mbi.Protect = PAGE_READONLY;

		}
		printProtection(mbi.Protect);

		//页面类型:可执行映像,私有内存区,内存映射文件
		switch (mbi.Type)
		{
		case MEM_IMAGE:cout << " |  Image  | "; break;
		case MEM_PRIVATE:cout << " | Private | "; break;
		case MEM_MAPPED:cout << " |  Mapped | "; break;
		default:cout << " |   None  | "; break;
		}

		// 模块名,如果有模块名,就输出
		TCHAR str_module_name[MAX_PATH];
		if (GetModuleFileName((HMODULE)pBlock, str_module_name, MAX_PATH) > 0) {
			PathStripPath(str_module_name);
			printf("%s", str_module_name);
		}
		cout << endl;
		pBlock = pEnd;	// 切换基址
	}
}

实验四:文件复制

完成一个文件复制命令 mycp,要求复制文件夹以及其所有子文件(我还额外写了文件复制的逻辑)。运行结果如下:

Linux: creat,read,write等系统调用,要求支持软链接
Windows: CreateFile(), ReadFile(), WriteFile(), CloseHandle()等函数

特别注意复制后,不仅读写权限一致,而且时间属性也一致。

Windows

运行结果

文件夹最初情况:

复制一个文件后

复制一个目录后:

检查一下目录复制情况:

实现文件夹和文件的完美复制,包括权限,时间等各种信息。

主函数

主函数首先通过Parse函数解析命令,通过其返回的copy_stat状态码判断结果,分别调用对应函数,进行复制,复制后调用SyncInfo函数同步一下信息即可。

#include <stdio.h>
#include <windows.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>

#define MAXN 1024

int Parse(int argc, char* argv[]); // 命令解析
void SyncInfo(char* source_file, char* dest_file); //同步两个文件的属性和时间
void CopyFile(char* source_file, char* dest_file); //复制文件
void CopyDir(char* source_file, char* dest_file); //复制目录,注意保证目标文件夹已经存在
WIN32_FIND_DATA lpFindFileData;

int main(int argc, char* argv[]) 
{
	// 检查输入,-1即真,直接终止程序,否则开始复制
	int copy_stat = Parse(argc, argv);
	if (copy_stat == -1) 
	{ //非法命令
		return -1;
	} 
	else if (copy_stat == 1) 
	{ //标准文件
		//复制文件
		CopyFile(argv[1], argv[2]);
		//同步信息
		SyncInfo(argv[1], argv[2]);
		//打印信息
		printf("复制文件完毕\n");
		return 1;
	} 
	else if (copy_stat == 0) 
	{ //目录
		// 复制目录
		CopyDir(argv[1], argv[2]);
		// 同步属性
		SyncInfo(argv[1], argv[2]);
		// 打印信息
		printf("复制目录完毕\n");
		return 0;
	}

	return 0;

}

命令解析:Parse

Parse命令判断命令是正确呢,还是错误呢:

  1. 错误
    • 参数错误
    • 路径错误
  2. 正确
    • 复制文件
    • 复制文件夹。如果复制文件夹,还需要保证目标文件夹存在
int Parse(int argc, char* argv[]) // 命令解析
{ 
	// 参数出错
	if (argc != 3) 
	{
		printf("非法参数\n");
		printf("请规范格式: .\\mycp.exe <path> <path> \n");
		return -1;
	}
	
	// 找不到路径
	if (FindFirstFile(argv[1], &lpFindFileData) == INVALID_HANDLE_VALUE) 
	{
		printf("位置路径\n");
		return -1;
	}
	
	// 检查src的类型
	struct _stat buf;
	_stat(argv[1], &buf);
	if (_S_IFREG & buf.st_mode) //标准文件
	{ 
		return 1;
	} 
	else //目录
	{
		//确保目标文件夹存在
		if (FindFirstFile(argv[2], &lpFindFileData) == INVALID_HANDLE_VALUE) { //目标目录不存在则创建
			CreateDirectory(argv[2], NULL); //创建目标文件目录
			printf("创建目标目录成功\n");
		}
		return 0;
	}
}

同步属性:SyncInfo

如函数名,将两个文件(普通文件和目录文件都一样)的各种信息同步。

void SyncInfo(char* source_file, char* dest_file) //同步两个文件的属性和时间
{ 
	HANDLE hsource_path = CreateFile(source_file, GENERIC_READ | // 文件句柄与目录句柄
		GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
	HANDLE hdest_path = CreateFile(dest_file, GENERIC_READ |
		GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
	FILETIME create_time, access_time, write_time;// 修改文件时间
	GetFileTime(hsource_path, &create_time, &access_time, &write_time);
	SetFileTime(hdest_path, &create_time, &access_time, &write_time);
	SetFileAttributes(dest_file, GetFileAttributes(source_file));// 设置属性
}

文件复制:CopyFile

文件复制的核心逻辑如下:

  1. 计算文件大小,从堆上使用new关键字开内存
  2. 读取src文件到内存
  3. 将内存中数据写入dst文件
void CopyFile(char* source_file, char* dest_file) //复制文件
{ 
	// CreateFile获取文件句柄与目录句柄,已有的(src)打开,没有的(dst)创建
	WIN32_FIND_DATA lpFindFileData;
	HANDLE hfindfile = FindFirstFile(source_file, &lpFindFileData);
	HANDLE hsource = CreateFile(source_file, GENERIC_READ |
		GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);//OPEN_ALWAYS
	HANDLE hdest_file = CreateFile(dest_file, GENERIC_READ |
		GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);//CREATE_ALWAYS
	//复制文件
	LONG size = lpFindFileData.nFileSizeLow - lpFindFileData.nFileSizeHigh;//计算文件大小
	int* buffer = new int[size];//从堆上开等大内存
	DWORD temp;//记录读取字节数
	bool tmp = ReadFile(hsource, buffer, size, &temp, NULL);//先读
	WriteFile(hdest_file, buffer, size, &temp, NULL);//后写
	
	// 关闭句柄
	CloseHandle(hfindfile);
	CloseHandle(hsource);
	CloseHandle(hdest_file);
}

文件夹复制:CopyDir

文件夹复制是基于文件复制的:

  1. 给定一个目录,遍历其所有子文件
  2. 判断子文件类型
    • 文件夹类型:递归调用CopyDir函数后进行SyncInfo信息同步
    • 标准文件: 调用CopyFile函数后进行SyncInfo信息同步
void CopyDir(char* source_file, char* dest_file) //复制目录,注意保证目标文件夹已经存在
{ 
	WIN32_FIND_DATA lpFindFileData;
	//为了保证递归调用的正确性,需要在每个函数里为source和dest_path单独开空间
	//拼接路径,source_path最初用于获取handle,后面用作临时路径变量
	//source_file dest_file是基础路径,path是拼接后的结果
	char source_path[MAXN], dest_path[MAXN];
	strcpy_s(source_path, source_file);
	strcpy_s(dest_path, dest_file);
	strcat_s(source_path, "\\*.*");
	strcat_s(dest_path, "\\");
	HANDLE hfindfile = FindFirstFile(source_path, &lpFindFileData);//获取目录头
	while (FindNextFile(hfindfile, &lpFindFileData) != 0) //遍历文件夹下所有文件
	{ 
		if (lpFindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) //目录文件,递归调用
		{ 
			if (strcmp(lpFindFileData.cFileName, ".") != 0 && strcmp(lpFindFileData.cFileName, "..") != 0) 
			{
				memset(source_path, 0, sizeof(source_path));//根据基础路径,构建source和dest_path路径
				strcpy_s(source_path, source_file);
				strcat_s(source_path, "\\");
				strcat_s(source_path, lpFindFileData.cFileName);
				memset(dest_path, 0, sizeof(dest_path));
				strcpy_s(dest_path, dest_file);
				strcat_s(dest_path, "\\");
				strcat_s(dest_path, lpFindFileData.cFileName);
				CreateDirectory(dest_path, NULL);//创建目录
				CopyDir(source_path, dest_path);//递归调用CopyDir,复制目录下的子文件
				SyncInfo(source_path, dest_path); //同步信息
			}
		} 
		else //若目标为文件,直接复制
		{ 
			memset(source_path, 0, sizeof(source_path));//根据基础路径,构建source和dest_path路径
			strcpy_s(source_path, source_file);
			strcat_s(source_path, "\\");
			strcat_s(source_path, lpFindFileData.cFileName);
			memset(dest_path, 0, sizeof(dest_path));
			strcpy_s(dest_path, dest_file);
			strcat_s(dest_path, "\\");
			strcat_s(dest_path, lpFindFileData.cFileName);
			CopyFile(source_path, dest_path);//调用CopyFile
			SyncInfo(source_path, dest_path);//同步信息
		}
	}
	CloseHandle(hfindfile);
}

Linux

运行结果

目录复制:


文件复制:

实现思路

思路和windows一模一样,只是在细节方面略有差别。

需要注意的是,软连接文件要单独拿出来判断,处理,甚至他的信息同步也需要单独写一个SyncSoftLink函数。

还有就是文件复制采用了缓冲区多次复制的方法,而不是Windows中的一次性复制

代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <dirent.h>
#include <utime.h>
#include <sys/time.h>
#include <fcntl.h>

#define MAXN 1024

void SyncInfo(char* source,char* dest)//同步文件属性
{
	struct stat statbuf;   //stat结构
	struct utimbuf timeby; //文件时间结构
	stat(source, &statbuf); //获取文件属性
	timeby.actime = statbuf.st_atime;  //修改时间属性,存取时间
	timeby.modtime = statbuf.st_mtime; //修改时间
	utime(dest, &timeby);
}

void SyncSoftLink(char* source,char* dest)//同步软链接
{
	//同步软链接信息
	struct stat statbuf;
	lstat(source, &statbuf);
	struct timeval ftime[2];
	ftime[0].tv_usec = 0;
	ftime[0].tv_sec = statbuf.st_atime;
	ftime[1].tv_usec = 0;
	ftime[1].tv_sec = statbuf.st_mtime;
	lutimes(dest, ftime);
}

int Parse(int argc, char *argv[]) // 检测输入与目标文件是否有误
{
	//判断参数出错
	if (argc != 3)
	{
		printf("非法参数\n");
		printf("请规范格式: ./mycp.exe <path> <path> \n");
		return -1;
	}
	//判断源是否存在
	DIR *dir=opendir(argv[1]);
	int file=open(argv[1],O_RDONLY);
	if(dir==NULL&&file==-1)//打开失败
	{
		printf("未知路径\n");
		close(file);
		closedir(dir);
		return -1;
	}
	//源文件存在,判断类型
	struct stat statbuf;
	lstat(argv[1], &statbuf);
	if (S_IFREG & statbuf.st_mode)//标准文件
	{
		close(file);
		closedir(dir);
		return 1;
	}
	else//目录
	{
		if ((dir = opendir(argv[2])) == NULL)//保证目标目录存在
		{
			mkdir(argv[2], statbuf.st_mode);
			printf("创建%s目录\n",argv[2]);
		}
		close(file);
		closedir(dir);
		return 0;
	}
}

void CopySoftLink(char *source, char *dest) //复制软链接
{
	//复制软链接
    char buffer[2 * MAXN];
    char oldpath[MAXN];
    getcwd(oldpath, sizeof(oldpath));
    strcat(oldpath, "/");
    memset(buffer, 0, sizeof(buffer));
    readlink(source, buffer, 2 * MAXN);//读取软链接到buffer
    symlink(buffer, dest);//将软链接赋给dest
}

void CopyFile(char *source, char *target) // 直接复制
{
    //打开与创建文件
	struct stat statbuf;
    stat(source, &statbuf);
	int fd_source = open(source, 0); //打开文件,文件描述符
    int fd_target = creat(target, statbuf.st_mode); //创建新文件,返回文件描述符
	
	//利用缓冲区传输文件
	char BUFFER[MAXN]; //缓冲区
	int wordbit; //记录读取的字节数
    while ((wordbit = read(fd_source, BUFFER, MAXN)) > 0)//循环读取,直到文件读完
    {
        //写入目标文件
        if (write(fd_target, BUFFER, wordbit) != wordbit)
        {
            printf("写入过程发生错误!\n");
            exit(-1);
        }
    }

	//关闭文件
    close(fd_source); 
    close(fd_target);
}

void CopyDir(char *source, char *dest) // 将源目录信息复制到目标目录下
{
    char source_path[MAXN / 2];//两个path是临时路径,用于构造各种路径。
    char dest_path[MAXN / 2];
	
    //打开源目录
    DIR *dir;
	if (NULL == (dir = opendir(source)))//打开目录,返回指向DIR结构的指针
	{
		printf("打开源文件夹错误\n");
		exit(-1);
	}

	//递归复制目录
	memset(dest_path,0,sizeof(dest_path));
    strcpy(dest_path, dest);
    strcat(dest_path, "/"); 
	struct dirent *entry;
    while ((entry = readdir(dir)) != NULL)//遍历源目录
    {
		//根据类型进行处理
        if (entry->d_type == 4) // 目录文件
        {
			//跳过.和..两个特殊目录
            if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
                continue;
			//正常目录,构造路径
            memset(source_path, 0, sizeof(source_path));
            strcpy(source_path, source);
            strcat(source_path, "/");
            strcat(source_path, entry->d_name);
			memset(dest_path,0,sizeof(dest_path));
			strcpy(dest_path, dest);
			strcat(dest_path, "/");
            strcat(dest_path, entry->d_name);
			//创建目录
			struct stat statbuf;
            stat(source_path, &statbuf);         //统计文件属性信息
            mkdir(dest_path, statbuf.st_mode); //创建目标目录
			//递归调用
            CopyDir(source_path, dest_path);
			//同步信息
            SyncInfo(source_path,dest_path);
        }
        else if (entry->d_type == 10) // 软链接文件
        {
			//构造路径
            memset(source_path, 0, sizeof(source_path));
			strcpy(source_path, source);
			strcat(source_path, "/");
			strcat(source_path, entry->d_name);
			memset(dest_path,0,sizeof(dest_path));
			strcpy(dest_path, dest);
			strcat(dest_path, "/");
			strcat(dest_path, entry->d_name);
			//复制软链接
            CopySoftLink(source_path, dest_path);
			//同步信息,使用软链接的同步函数
			SyncSoftLink(source_path,dest_path);
        }
        else // 普通文件
        {
            //构造路径
			memset(source_path, 0, sizeof(source_path));
			strcpy(source_path, source);
			strcat(source_path, "/");
			strcat(source_path, entry->d_name);
			memset(dest_path,0,sizeof(dest_path));
			strcpy(dest_path, dest);
			strcat(dest_path, "/");
			strcat(dest_path, entry->d_name);
			//复制软链接
			CopyFile(source_path, dest_path);
			//同步信息
			SyncInfo(source_path,dest_path);
        }
    }
    closedir(dir);
}


int main(int argc, char *argv[])
{
	int copy_stat=Parse(argc, argv);
    if(copy_stat==-1)//异常
	{
		return -1;
	}
	else if(copy_stat==1)//标准文件
	{
		CopyFile(argv[1],argv[2]);
		SyncInfo(argv[1],argv[2]);
		printf("文件复制完毕\n");
		
		return 1;
	}
	else if(copy_stat==0)//目录
	{
		CopyDir(argv[1], argv[2]); //开始复制
		SyncInfo(argv[1],argv[2]); //同步信息
		printf("目录复制完毕\n");
		
		return 0;
	}
        
    return 0;    
}

本文标签: 合集 例子 操作系统 北理工 api