• 北理工操作系统实验合集 | API解读与例子(持续更新)


    文章目录

    传送门

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

    操作系统笔记——概述、进程、并发控制
    操作系统笔记——储存器管理、文件系统、设备管理
    操作系统笔记——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 
    pid_t getpid(void);
    pid_t getppid(void); //父进程
    
    • 1
    • 2
    • 3

    fork/vfork

    #include 
    #include 
    pid_t fork (void);
    
    • 1
    • 2
    • 3

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

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

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

    #include
    #include
    #include
    
    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());
    	}
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

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

    在这里插入图片描述

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

    #include 
    #include 
    #include 
    #include 
    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    在这里插入图片描述

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

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

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

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

    #include 
    #include 
    pid_t vfork(void);
    
    • 1
    • 2
    • 3

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

    在这里插入图片描述

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

    #include
    #include 
    #include
    #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);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    在这里插入图片描述
    进一步,如果想要创建任意进程,就需要编写递归函数或者循环函数了:

    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
    #include
    
    int main(void)
    {
    	printf("when exec pid=%d\n",getpid());	
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    #include
    #include
    
    int main(void)
    {
    	printf("before exec pid=%d\n",getpid());
    	execl("./exe",0);
    	printf("after exec pid=%d\n",getpid());//这一行不执行
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

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

    在这里插入图片描述

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

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

    在这里插入图片描述

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

    在这里插入图片描述

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

    在这里插入图片描述

    wait/waitpid

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

    #include 
    #include 
    pid_t wait(int *statloc);
    
    • 1
    • 2
    • 3

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

    #include
    #include
    #include
    #include //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);
    	} 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    在这里插入图片描述
    exit(2)

    在这里插入图片描述

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

    #include 
    #include 
    pid_t waitpid(pid_t, int *statloc, int options);
    
    • 1
    • 2
    • 3
    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
    #include
    #include  /* 简单的进程同步: 父进程等待子进程输出后再输出*/
    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);
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    在这里插入图片描述

    Windows

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

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

    GetCurrentProcessId

    #include
    DWORD GetCurrentProcessId(void);
    
    • 1
    • 2

    返回一个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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    PROCESS_INFORMATION就简单很多

    typedef struct _PROCESS_INFORMATION{
    	HANDLE  hProcess;      //新进程句柄
    	HANDLE  hThread;      //新线程句柄
    	DWORD  dwProcessId; //新进程标识符
    	DWORD  dwThreadId; //新线程标识符
    }PROCESS_INFORMATION;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

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

    //child.c,子进程
    #include
    #include
    
    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    //main.c
    #include
    #include
    
    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; 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

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

    在这里插入图片描述

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

    在这里插入图片描述

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

    在这里插入图片描述

    GetModuleFileName

    DWORD GetModuleFileName(
    		HMODULE hModule,
    		LPTSTR lpFilename, DWORD nSize);
    
    • 1
    • 2
    • 3

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

    //child.c
    #include
    #include
    
    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    //main.c
    #include
    #include
    
    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; 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    在这里插入图片描述

    Sleep

    void Sleep(DWORD dwMilliseconds);
    
    • 1

    毫秒为单位

    WaitForSingleObject/WaitForMultipleObjects

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

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

    在这里插入图片描述

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

    在这里插入图片描述

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

    #include
    #include
    #include
    
    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();//子进程代码 
    	}
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    在这里插入图片描述

    进程间通信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 );
    
    • 1

    在两个进程中,都使用了同一个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 );
    
    • 1

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

    shmdt ( shared_memory )
    
    • 1

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

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

    另一种方法就是fork。

    在这里插入图片描述
    在这里插入图片描述

    例子:多进程fork+共享

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

    //外部程序,child.c
    #include
    #include
    #include
    #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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    //主进程 main.c
    #include
    #include
    #include 
    #include
    #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);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    在这里插入图片描述

    信号量

    信号量:semaphore

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

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

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

    #include 
    int semop(int semid, struct sembuf semarray[], unsigned int nsops);
    
    • 1
    • 2

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

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

    struct sembuf
    {
       //要操作的信号量在信号量集中的索引编号
       unsigned short sem_num;
       //对信号量进行的操作值(可为正、负或0)
       short sem_op;
       //操作标志,一般都是0,除非进一步细化操作
       short sem_flg;
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    #include 
    int semctl(int semid, int semnum, int cmd, union semun arg)
    
    • 1
    • 2

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

    union semun
    {  
       //用于信号量的赋值
       int val;
       //用于返回信号量集信息
       struct semid_ds *buf;
       //用于设置或者获取信号量集成员的取值
       unsigned short *array;
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    比如赋值,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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    例子:多进程文件操作
    //child.c
    #include
    #include
    #include
    #include
    #include
    #include
    #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());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    //main.c
    #include
    #include
    #include
    #include
    #include
    #include
    #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);	
    	} 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83

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

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

    在这里插入图片描述

    Windows

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

    互斥体

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

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

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

    例子

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

    #include
    #include
    
    //全局变量,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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

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

    在这里插入图片描述

    信号量

    //创建信号量
    HANDLE CreateSemaphore(  
    	  lpSemaphoreAttributes, //NULL表示默认属性
    	  lInitialCount,         //信号量的初值
    	  lMaximumCount,  //信号量的最大值
    	  lpName);         //信号量的名称
    //释放信号量
    BOOL ReleaseSemaphore(
    	  hSemaphore,   //信号量的句柄
    	  lReleaseCount,     //信号量计数增加值
    	  lpPreviousCount);  //返回信号量原来值
    
    //打开已存在信号量,lpName相当于key
    HANDLE  OpenSemaphore(dwDesiredAccess,  
    	bInheritHandle,  lpName);
    
    CloseHandle(hSemphore)//关闭
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

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

    #include
    #include
    
    //全局变量
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    刚开始有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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

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

    储存器管理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
    
    • 1

    解压linux-5.4.69.tar.gz

    tar zxvf linux-5.4.69.tar.gz
    
    • 1

    进入解压后的文件

    cd linux-5.4.69
    
    • 1

    复制本机的配置文件

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

    编译前的环境准备

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

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

    make menuconfig
    # save->ok->exit->exit
    
    • 1
    • 2

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

    make -j6 && make modules_install -j6 && make install -j6
    
    • 1

    修改引导菜单

    gedit /boot/grub2/grub.cfg
    
    • 1
    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//标准库 
    #include
    #include 
    #include 
    
    #include//进程库
    #include
    #include
    #include
    #include
    
    //进程个数
    #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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    主程序代码

    在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);
    	} 
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210

    函数框架

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

    这里重点说一下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();
    		}
    	}
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    fork结构

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

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

    
    
    • 1

    在这里插入图片描述

    函数框架里给的做法是双循环,我们这里给出单循环+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();
    			}	
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    结果

    在这里插入图片描述

    Windows版本

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

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

    头文件

    #ifndef DEF_H
    #define DEF_H
    
    #include//标准库 
    #include
    #include 
    #include 
    
    #include//系统库 
    
    //进程个数
    #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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112

    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;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96

    结果

    在这里插入图片描述

    实验三:内存监控

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

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

    相关的系统调用:

    GetSystemInfo, VirtualQueryEx, GetPerformanceInfo, GlobalMemoryStatusEx …
    
    • 1

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

    运行结果

    使用Visual Studio 2019编写调试:

    查看帮助:

    在这里插入图片描述

    查看整体信息:

    在这里插入图片描述
    查看具体信息:

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    主函数

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

    在这里插入图片描述

    主函数有几个点:

    1. setlocale。这个可能有用,是和语言编码有关的,尤其是中文显示。
    2. 用一个string类型储存cmd
    3. while(1)循环不断询问指令,之后进行多分支判断,分别调用对应的显示函数
    4. pid指令比较特殊,它是分段的,输入pid后进入pid模式,再输一个pid后显示。其实设计的时候也可以用“pid+数字”这种命令来直接一段实现
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    #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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84

    辅助函数

    非核心函数。

    显示权限信息: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
    
    • 1
    • 2
    • 3
    • 4
    //输出权限保护级别
    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    显示帮助:showHelp

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

    在这里插入图片描述

    void showHelp(void)
    {
    	cout << "--------------------------------------------------------------------------" << endl;
    	cout << "命令类型: " << endl
    		<< "\"system\"   : 显示计算机整体信息" << endl
    		<< "\"memory\": 显示内存信息" << endl
    		<< "\"process\"  : 显示活跃进程信息" << endl
    		<< "\"pid\"      : 查看某一进程具体信息" << endl
    		<< "\"help\"     : 显示帮助" << endl
    		<< "\"exit\"     : 退出程序" << endl;
    	cout << "--------------------------------------------------------------------------" << endl;
    	return;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    核心函数

    显示系统信息: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;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    显示内存信息: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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    获取活跃进程: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);//关闭快照
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    获取进程具体信息: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;	// 切换基址
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77

    实验四:文件复制

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

    在这里插入图片描述

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

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

    Windows

    运行结果

    文件夹最初情况:

    在这里插入图片描述

    复制一个文件后

    在这里插入图片描述

    复制一个目录后:

    在这里插入图片描述

    检查一下目录复制情况:

    在这里插入图片描述

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

    主函数

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

    #include 
    #include 
    #include 
    #include 
    #include 
    
    #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;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    命令解析:Parse

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

    1. 错误
      • 参数错误
      • 路径错误
    2. 正确
      • 复制文件
      • 复制文件夹。如果复制文件夹,还需要保证目标文件夹存在
    int Parse(int argc, char* argv[]) // 命令解析
    { 
    	// 参数出错
    	if (argc != 3) 
    	{
    		printf("非法参数\n");
    		printf("请规范格式: .\\mycp.exe   \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;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    同步属性: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));// 设置属性
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    文件复制: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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    文件夹复制: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);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    Linux

    运行结果

    目录复制:

    在这里插入图片描述
    文件复制:

    在这里插入图片描述

    实现思路

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

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

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

    代码

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    #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   \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;    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
  • 相关阅读:
    数据库基本操作(一)
    C#使用词嵌入向量与向量数据库为大语言模型(LLM)赋能长期记忆实现私域问答机器人落地
    程序猿怎么选赛道|规划
    构建Buildroot根文件系统(I.MX6ULL)
    一文总结提示工程框架,除了CoT还有ToT、GoT、AoT、SoT、PoT
    二元线性方程组与二阶行列式
    XCode 去除 UserInterfaceState.xcuserstate 文件困扰
    CSS3-多列布局
    cJson堆内存释放问题
    Rocky(centos)安装nginx并设置开机自启
  • 原文地址:https://blog.csdn.net/weixin_50295745/article/details/127305311