文章目录

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

传送门

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

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

实验报告与源码下载

百度云提取码:cyyy

前言

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

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

代码参考

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

进程控制API

Linux

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

getpid/getppid

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

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

fork/vfork

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

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

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

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

#include<unistd.h>
#include<stdio.h>
#include<sys/types.h>int main(void)
{printf("main pid=%d\n",getpid());pid_t pid;if((pid=fork())<0){printf("error\n");exit(0);  }   else if(pid==0){printf("child forkpid=%d getpid=%d\n",pid,getpid());}else{printf("father forkpid=%d getpid=%d\n",pid,getpid());}
}

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

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

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

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

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

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

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

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

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

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

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


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

TODO

exit/_exit

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

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

参考

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

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

exec函数族

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

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

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

#include<unistd.h>
#include<stdio.h>int main(void)
{printf("when exec pid=%d\n",getpid());
}
#include<unistd.h>
#include<stdio.h>int main(void)
{printf("before exec pid=%d\n",getpid());execl("./exe",0);printf("after exec pid=%d\n",getpid());//这一行不执行
}

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

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

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

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

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

wait/waitpid

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

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

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

#include<unistd.h>
#include<stdio.h>
#include<sys/types.h>
#include<wait.h> //waitint main(void)
{pid_t pid;if((pid=fork())<0){printf("error\n");}else if(pid==0) //子进程 {printf("child pid=%d\n",getpid());exit(1);}else //父进程 {printf("father pid=%d\n",getpid());int statloc;printf("child pid=%d\n",wait(&statloc));printf("statloc=%d\n",statloc);exit(0);}
}


exit(2)

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

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

pause/sleep

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

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

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

Windows

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

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

GetCurrentProcessId

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

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

CreateProcess

主要参数有4个。

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

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

typedef struct _STARTUPINFO
{ DWORD cb;              //包含STARTUPINFO结构中的字节数.如果Microsoft将来扩展该结构,它可用作版本控制手段.应用程序必须将cb初始化为sizeof(STARTUPINFO) PSTR lpReserved;      //保留。必须初始化为NULLPSTR 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;       //保留。必须被初始化为NULLHANDLE 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)};//初始化siPROCESS_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" ;//子进程exeCreateProcess(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)};//初始化siPROCESS_INFORMATION pi;char path[]="./child.exe" ;//子进程exeCreateProcess(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);//等待1sprintf("end child\n"); //exit?
}int main(int argc,char* argv[])
{if(argc>1&&strcmp(argv[1],"child")==0){MyChild();//子进程代码 }else{MyParent();//子进程代码 }
}

进程间通信API(IPC-API)

IPC:InterProcess Communication

Linux


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

共享内存区

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

shmget/shmat/shmdt/shmctl

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

程序中用这三个接口:

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




例子:基本用法

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

共享内存例子

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

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

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

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

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

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

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

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

shared_memory = shmat ( shmid, NULL, 0 );

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

shmdt ( shared_memory )

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

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

另一种方法就是fork。


例子:多进程fork+共享

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

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

信号量

信号量:semaphore

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

  1. 信号量集的获取semget
  2. 信号量集的操作semop
  3. 信号量集的控制semctl

semget/semop/semctl

#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);

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

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

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

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

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

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

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

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

PV封装

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

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

例子:多进程文件操作

//child.c
#include<sys/sem.h>
#include<string.h>
#include<malloc.h>
#include<unistd.h>
#include<stdlib.h>
#include<time.h>
#define SEMKEY 300union 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号信号量PFILE* 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 300union 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号信号量PFILE* 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); //CloseHandlereturn 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);//Vprintf("now %d sem available\n",sem_num+1);//sem_num是释放前
}void main(void)
{//总100,同时只有5sem=CreateSemaphore(NULL,5,5,NULL);//初值5,最大5,匿名HANDLE array[101];for(int total=100;total>0;total--){HANDLE thread=CreateThread(NULL,0,func,total*5,0,NULL);//暂停时间=total*5 array[total-1]=thread;//CloseHandle(thread);}//WaitForMultipleObjects(100,array,TRUE,INFINITE);没用?Sleep(10000);return 0;
}

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


文件映射

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


//打开/创建文件映射(shmget)
HANDLE  CreateFileMapping(HANDLE  hFile,   //欲创建映射的文件句柄,如果是INVALID_HANDLE_VALUE就会创建临时文件对象LPSECURITY_ATTRIBUTES  lpAttributes,DWORD  flProtect,  //读/写保护参数DWORD  dwMaximumSizeHigh,  //高32位DWORD  dwMaximumSizeLow,  //低32位,两个都为0就代表磁盘文件的实际长度LPCTSTR  lpName);  //对象的名字//打开一个文件映射
HANDLE  OpenFileMapping (DWORD  dwDesiredAccess, //存取访问方式BOOL  bInheritHandle,  //继承标记LPCTSTR  lpName);      //文件映射对象名称//在当前进程中打开文件映射的一个视图(shmat)
LPVOID  MapViewOfFile(HANDLE  hFileMappingObject, //对象句柄DWORD  dwDesiredAccess,  //指定访问权限DWORD  dwFileOffsetHigh,  //文件内映射起点DWORD  dwFileOffsetLow,  //文件内映射起点SIZE_T  dwNumberOfBytesToMap); //文件中要映射的字节数。用0映射整个文件映射对象
//返回值:文件映射的起始地址,void*//解除映射(shmdt)
BOOL UnmapViewOfFile(LPCVOID lpBaseAddress);

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

储存器管理API

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

详见代码阶段

文件操作API

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

详见代码阶段

实验一:Linux内核编译

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

@Felix and Phoenix的主页

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

ubantu编译内核

注意事项

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

实验环境

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

VMware下载链接
CentOS下载链接

实验步骤

使用镜像源下载linux源码

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

解压linux-5.4.69.tar.gz

tar zxvf linux-5.4.69.tar.gz

进入解压后的文件

cd linux-5.4.69

复制本机的配置文件

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

编译前的环境准备

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

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

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

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

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

修改引导菜单

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

参考博客

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

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

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

Linux版本

头文件

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

#ifndef DEF_H
#define DEF_H#include<stdio.h>//标准库
#include<time.h>
#include<string.h>
#include<stdlib.h> #include<unistd.h>//进程库
#include<sys/ipc.h>
#include<sys/shm.h>
#include<sys/sem.h>
#include<sys/wait.h>//进程个数
#define PRO_NUM 2
#define CON_NUM 3//重复次数
#define PRO_REP 6
#define CON_REP 4//缓冲区大小
#define BUF_LEN 11
#define BUF_CNT 3//内存,信号量key
#define SHM_KEY 1234
#define SEM_KEY 1235
#define MUTEX 0
#define EMPTY 1
#define FULL 2//模式,可读可写
#define MODE 0600 //缓冲区结构
struct MyBuffer
{char str[BUF_CNT][BUF_LEN];int head;int tail;
};#endif //DEF_H

主程序代码

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

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

#include "def.h"//辅助函数int randMod(int mod)// 随机获取范围内整数
{return rand() % mod;
}char *randString()// 得到一个字符串,长度随机,内容随机
{static char buf[BUF_LEN];//static重复使用 memset(buf, 0, sizeof(buf));int n = randMod(10) + 1;for (int i = 0; i < n; i++)buf[i] = (char)(randMod(26) + 'A');return buf;
}// pv封装void P(int sem_id, int sem_num) //P
{struct sembuf xx;xx.sem_num = sem_num;xx.sem_op = -1;xx.sem_flg = 0;semop(sem_id, &xx, 1);
}void V(int sem_id, int sem_num) //V
{struct sembuf xx;xx.sem_num = sem_num;xx.sem_op = 1;xx.sem_flg = 0;semop(sem_id, &xx, 1);
}//进程函数
void init() //初始化
{//信号量 int semid=semget(SEM_KEY,3,IPC_CREAT|MODE);if(semid<0){printf("sem err\n");exit(1);}semctl(semid,MUTEX,SETVAL,1);semctl(semid,EMPTY,SETVAL,BUF_CNT);semctl(semid,FULL,SETVAL,0);//共享内存 int shmid=shmget(SHM_KEY,sizeof(struct MyBuffer),IPC_CREAT|MODE);if(shmid<0){printf("shm err\n");exit(1);}    //清空内存struct MyBuffer* shmptr=shmat(shmid,0,0);if(shmptr<0){printf("shmat err\n");exit(1);}memset(shmptr,0,sizeof(struct MyBuffer));shmdt(shmptr);
}void pro() //生产者
{srand((unsigned)getpid());//以pid作为seed//获取已有信号量和共享内存 //信号量 int semid=semget(SEM_KEY,3,IPC_CREAT|MODE);if(semid<0){printf("sem err\n");exit(1);}//共享内存 int shmid=shmget(SHM_KEY,sizeof(struct MyBuffer),IPC_CREAT|MODE);if(shmid<0){printf("shm err\n");exit(1);}struct MyBuffer* shmptr=shmat(shmid,0,0);if(shmptr<0){printf("shmat err\n");exit(1);}//重复向储存区写入//struct timespec begin;//精确获取时间 //struct timespec end;for(int i=0;i<PRO_REP;i++){//clock_gettime(1,&begin);//记录初始时间 P(semid,EMPTY);//PP(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);//VV(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);//PP(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);//VV(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获取映射 OpenFileMappinghMapFile = OpenFileMapping(FILE_MAP_ALL_ACCESS,//全部权限 FALSE,            //不继承 szFileMappingName);//使用前面定义的const常量 if (hMapFile == NULL){printf("Mapping Failed!\n");return 1;}//shmat获取地址 MapViewOfFilepBuf = (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);// sleepSleep(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 resourcesCloseHandle(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获取映射 OpenFileMappinghMapFile = OpenFileMapping(FILE_MAP_ALL_ACCESS,//全部权限 FALSE,            //不继承 szFileMappingName);//使用前面定义的const常量 if (hMapFile == NULL){printf("Mapping Failed!\n");return 1;}//shmat获取地址 MapViewOfFilepBuf = (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);// sleepSleep(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 resourcesCloseHandle(hSemaphoreEmpty);CloseHandle(hSemaphoreFull);CloseHandle(hMutex);UnmapViewOfFile(pBuf);CloseHandle(hMapFile);return 0;
}

结果

实验三:内存监控

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

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

相关的系统调用:

GetSystemInfo, VirtualQueryEx, GetPerformanceInfo, GlobalMemoryStatusEx …

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

运行结果

使用Visual Studio 2019编写调试:

查看帮助:

查看整体信息:


查看具体信息:



主函数

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

主函数有几个点:

  1. setlocale。这个可能有用,是和语言编码有关的,尤其是中文显示。
  2. 用一个string类型储存cmd
  3. while(1)循环不断询问指令,之后进行多分支判断,分别调用对应的显示函数
  4. pid指令比较特殊,它是分段的,输入pid后进入pid模式,再输一个pid后显示。其实设计的时候也可以用“pid+数字”这种命令来直接一段实现
#include <windows.h>
#include <iostream>
#include <iomanip>
#include <string>
#include <Tlhelp32.h>
#include <stdio.h>
#include <tchar.h>
#include <shlwapi.h>
#include <psapi.h>#pragma comment(lib, "shlwapi.lib")
#pragma comment(lib,"kernel32.lib")using namespace std;//声明
void printProtection(DWORD dwTarget);
void displaySystemConfig(void);
void displayMemoryCondition(void);
void getAllProcessInformation(void);
void ShowHelp(void);
void getProcessDetail(int pid);int main()
{//设置显示语言setlocale(LC_ALL, "CHS");//初始化输出 cout << endl << "*-----------内存管理(1120200944)-----------*" << endl << endl;cout << "输入help可查询帮助" << endl << endl;string cmd;char cmd_charstr[127];//循环询问 while (1){//获取输入 cout << "请输入指令> ";cin.getline(cmd_charstr, 127);cmd = cmd_charstr;//判断命令if (cmd == "system") {cout << endl;displaySystemConfig();}else if (cmd == "memory") {cout << endl;displayMemoryCondition();}else if (cmd == "process") {cout << endl;getAllProcessInformation();}else if (cmd == "pid") {cout << "PID> ";int pid = 0;cin >> pid;cin.getline(cmd_charstr, 127);if (pid <= 0) continue;cout << endl;getProcessDetail(pid);}else if (cmd == "help") {cout << endl;ShowHelp();}else if (cmd == "exit") {break;}else if (cmd == "clear" || cmd == "cls") {system("cls");}else {if (cmd != "") cout << "非法命令,请使用\"help\"命令查看提示" << endl;fflush(stdin);cin.clear();continue;}cin.clear();}return 0;
}

辅助函数

非核心函数。

显示权限信息:printProtection

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

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

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

显示帮助:showHelp

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

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

核心函数

显示系统信息:displaySystem

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

//显示系统整体信息
void displaySystem(void)
{SYSTEM_INFO si;ZeroMemory(&si,sizeof(si));GetSystemInfo(&si);//获取系统信息 TCHAR str_page_size[MAX_PATH];StrFormatByteSize(si.dwPageSize, str_page_size, MAX_PATH);//自动计算显示格式(KB,MB,GB)DWORD memory_size = (DWORD)si.lpMaximumApplicationAddress - (DWORD)si.lpMinimumApplicationAddress;TCHAR str_memory_size[MAX_PATH];StrFormatByteSize(memory_size, str_memory_size, MAX_PATH);cout << "计算机整体信息:" << endl;cout << "--------------------------------------------" << endl;cout << "处理器架构         | " << (si.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_AMD64 || si.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_INTEL ? "x64" : "x86") << endl;cout << "内核数量           | " << si.dwNumberOfProcessors << endl;cout << "内存页大小         | " << str_page_size << endl;cout << "用户最低地址       | 0x" << hex << setfill('0') << setw(8) << (DWORD)si.lpMinimumApplicationAddress << endl;cout << "用户最高地址       | 0x" << hex << setw(8) << (DWORD)si.lpMaximumApplicationAddress << endl;cout << "用户可用内存       | " << str_memory_size << endl;cout << "--------------------------------------------" << endl;return;}

显示内存信息:displayMemory

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

// 显示系统内存信息
void displayMemory(void)
{long MB = 1024 * 1024;//1Mlong GB = MB * 1024;//1GMEMORYSTATUSEX 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 1024int Parse(int argc, char* argv[]); // 命令解析
void SyncInfo(char* source_file, char* dest_file); //同步两个文件的属性和时间
void CopyFile(char* source_file, char* dest_file); //复制文件
void CopyDir(char* source_file, char* dest_file); //复制目录,注意保证目标文件夹已经存在
WIN32_FIND_DATA lpFindFileData;int main(int argc, char* argv[])
{// 检查输入,-1即真,直接终止程序,否则开始复制int copy_stat = Parse(argc, argv);if (copy_stat == -1) { //非法命令return -1;} else if (copy_stat == 1) { //标准文件//复制文件CopyFile(argv[1], argv[2]);//同步信息SyncInfo(argv[1], argv[2]);//打印信息printf("复制文件完毕\n");return 1;} else if (copy_stat == 0) { //目录// 复制目录CopyDir(argv[1], argv[2]);// 同步属性SyncInfo(argv[1], argv[2]);// 打印信息printf("复制目录完毕\n");return 0;}return 0;}

命令解析:Parse

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

  1. 错误

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

同步属性:SyncInfo

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

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

文件复制:CopyFile

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

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

文件夹复制:CopyDir

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

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

  1. linux 中级 教程pdf,Linux初中级学习者指导Linux操作系统技术合集.pdf

    红联的个人空间 Linux操作系统技术合集 作者:红联 Linux操作系统技术合 集 ─────Linux初, 中级学习者教程 Linux有些神奇,有人就这么说,Linux有些意思,我想尝尝,尚末安装 ...

  2. H3C NE实验合集

    H3C NE实验合集 一.Telnet.SSH登录 (一)项目拓扑 (二)项目需求 (三)配置步骤 (四)测试 二.vlan-trunk (一)项目拓扑 (二)项目需求 (三)配置步骤 (四)测试 三 ...

  3. 哈工大课程和实验合集

    哈工大课程和实验合集--待更新 1.数据结构 2.计算机系统 3.软件构造 4.算法设计与分析 5.自然语言处理 6.计算机网络 7.机器学习 8. python常用库函数总结 1.数据结构 哈工大数 ...

  4. eNSP综合实验合集(eNSP综合大作业合集)_可先收藏

    作者:BSXY_19计科_陈永跃 BSXY_信息学院 注:未经允许禁止转发任何内容 **注:在该文章中就只对ensp综合实验做一个总结和归纳,只给出相应的topo图和需求说明和对应的文章的连接.有什么 ...

  5. matlab 图像分割 提取人像_数字图像处理实验合集(含代码、报告)

    数字图像处理(MATLAB) 实验一 一.实验名称 图像的采样与量化 二.实验目的 1.熟悉MATLAB软件的使用. 2.掌握采样与量化的原理及数学运算. 3.于MATLAB环境下编程实现对图片的不同 ...

  6. 数字图像处理 采样定理_数字图像处理实验合集

    实验一 一.实验名称 图像的采样与量化 二.实验目的 1.熟悉MATLAB软件的使用. 2.掌握采样与量化的原理及数学运算. 3.于MATLAB环境下编程实现对图片的不同程度的采样与量化. 三.实验内 ...

  7. 《大学“电路分析基础”课程实验合集.实验四》丨线性电路特性的研究

    实验四   线性电路特性的研究 目录 1 实验目的 2 实验仪器与设备 3 实验原理 4 实验预习 5 实验内容与步骤 6 实验要求与注意事项 7 实验报告与思考题 1 实验目的 1. 加深对线性电路 ...

  8. 《大学“电路分析基础”课程实验合集.实验五》丨线性有源二端网络等效电路的研究

    实验五  线性有源二端网络等效电路的研究 目录 实验五  线性有源二端网络等效电路的研究 1 实验目的 2 实验仪器与设备 3 实验预习 4 实验原理 5 实验内容与步骤 6 实验要求与注意事项 7 ...

  9. 大学模电实验合集丨实验五 负反馈放大电路

    目录 一.实验目的 二.实验仪器 三.实验要求 四.实验内容及步骤 1.负反馈放大器开环和闭环放大倍数的测试 2.负反馈对失真的改善作用 3.测放大电路频率特性 五.根据实验报告回答下列问题 一.实验 ...

最新文章

  1. TVM部署和集成Deploy and Integration
  2. 测试用例的设计方法(三)
  3. python ‘float‘object is not iterable
  4. Flash Builder4.7极其简单破解方法-三步搞定(亲测)
  5. JAVA工程师必学技能,进阶涨薪的推进器!这份实战教程请收下
  6. x:Name与Name区别
  7. java s1=abc s2=abc s1==s2_经典问题:String s1 = abc 与 String s2 = new String(abc)的区别...
  8. C语言 memset函数简单实现
  9. mysql-启动、关闭与重启
  10. 安卓学习笔记:使用PopupWindow创建简单菜单
  11. Bag-of-words模型-可用于计算文本及图片相似度
  12. iPhone X 再曝新 Bug:电话无法接听!
  13. virtualbox 桥接模式下虚拟机ping不通网关
  14. Cookie和Session的详细介绍和使用规范
  15. html字体兼容写法,字体兼容写法
  16. STM8S103定时器1,定时器2多路PWM波输出
  17. 最受IT公司欢迎的30款开源软件
  18. 无线桥连后不能访问服务器,路由器设置无线桥接后不能登录副路由器怎么办?...
  19. yarn和npm常用基本命令安装和卸载
  20. react项目中解决IE浏览器下报Promise未定义的错误

热门文章

  1. wps插入html,wps怎么插入脚注?
  2. 解决nginx 502 bad gateway
  3. 分布式系统架构设计原则和理论 --AKF 架构原则
  4. Activiti6教程三
  5. Solidworks怎么镜像实体?下面是solidworks镜像实体教程详细
  6. linux下gcc编译c文件生成可执行文件的四个步骤
  7. EA下载问题解决方案
  8. 关于微星电脑主板开机时右下角出现9C的问题
  9. 新概念英语(第一册)复习(原文及全文翻译)——Lesson 131 - Lesson 143(完结)
  10. 轻松编辑PDF文档的贝茨编码