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
- 创建:fork/vfork
- 终止:exit/_exit
- 获取进程标识符:getpid/getppid(获取parent的pid)
- 调用程序:exec
- 进程等待:wait/waitpid
- 暂停: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);
- 父进程可以使用pid指定等待的子进程,pid > 0:pid完全匹配,pid = 0:匹配与当前进程是同一个进程组的任何终止子进程;pid = -1:匹配任何终止的子进程;pid < -1:匹配任何进程组标识等于pid绝对值的任何终止子进程
- 可在option中设置WNOHANG,如果没有任何子进程终止,则立即返回0,如不使用option,参数为0。
- 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个。
- 第一个参数:可执行文件路径.。字符串,绝对路径或者相对路径
- 第二个参数:命令行参数。字符串
- 倒数第二参数:STARTUPINFO结构体指针,储存进程相关的各种信息,比如窗口大小等等,初始化的时候只需要初始化第一个参数即可。比如STARTUPINFO si={sizeof(si)};
- 倒数第一参数: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
两个或者更多进程可以共享一个内存区,一个进程也可以连接多个共享内存区。
程序中用这三个接口:
- 共享内存获取shmget
- 共享内存区的附加与解除shmat/shmdt
- 共享内存区控制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中,信号量不是像伪代码那种单个声明,而是以信号量集的形式声明,通过函数指定信号量进行操作,信号量集中可以有一个或者多个信号量。
- 信号量集的获取semget
- 信号量集的操作semop
- 信号量集的控制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编译内核
注意事项
- 最重要的一点——Linux内核编译安装后 高达10~20G ,创建虚拟机时分配 40G硬盘 能够保证后续实验 无存储相关的bug,不然后续要么选择扩容(有点麻烦,笔者不会),要么重装虚拟机(重装了n 次,笔者很会)
- gcc版本和linux内核版本要兼容 ,否则会出现源码无法编译的问题,要么选择升级gcc(笔者试过 了,编译安装好久,还是换低版本的linux源码好),要么换低版本的linux
- 建议将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
- 用gedit打开配置文件
- Ctrl+F搜索menuentry
- 找到menuentry后,后面的字符串就是默认的版本号,修改为班号、学号、姓名及版本号
- 保存退出
- 重启,即可在引导菜单看到修改后的班号、学号、姓名及版本号
参考博客
【linux内核源码分析】详解Linux内核编译配置(menuconfig)、文件系统制作
Linux下更新GCC
Linux centos7升级内核(两种方法:内核编译和yum更新)
【 GRUB 】修改启动列表项,自定义列表项内容,添加自定义GRUB主题
实验二:生产者消费者进程
- 一个大小为3的缓冲区,初始为空
- 2个生产者
- 随机等待一段时间,往缓冲区添加数据,
- 若缓冲区已满,等待消费者取走数据后再添加
- 重复6次
- 3个消费者
- 随机等待一段时间,从缓冲区读取数据
- 若缓冲区为空,等待生产者添加数据后再读取
- 重复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。
主函数有几个点:
- setlocale。这个可能有用,是和语言编码有关的,尤其是中文显示。
- 用一个string类型储存cmd
- while(1)循环不断询问指令,之后进行多分支判断,分别调用对应的显示函数
- 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命令判断命令是正确呢,还是错误呢:
- 错误
- 参数错误
- 路径错误
- 正确
- 复制文件
- 复制文件夹。如果复制文件夹,还需要保证目标文件夹存在
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
文件复制的核心逻辑如下:
- 计算文件大小,从堆上使用new关键字开内存
- 读取src文件到内存
- 将内存中数据写入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
文件夹复制是基于文件复制的:
- 给定一个目录,遍历其所有子文件
- 判断子文件类型
- 文件夹类型:递归调用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解读与例子 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.freenas.com.cn/jishu/1716192006h670198.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论