学习如何创建进程;了解进程互斥的意义。
1、编写一段程序,利用系统调用(如linux的fork( ))创建两个进程。当此程序运行时,在系统中有一个父进程和两个子进程活动,让每一个进程在屏幕上显示一个字符:父进程显示字符“a”;子进程分别显示字符“b”和字符“c”。
2、修改已经编号的程序(使用fork()创建进程),将每个进程输出一个字符改为每个进程用for循环重复输出一句话,再观察程序执行时屏幕上出现的现象,并分析原因。
3、如果在程序中使用系统调用lockf()来给每一个进程加锁,可以实现进程之间的互斥,观察并分析出现的现象。
1、当运行该程序时,即创建了一个父进程,再通过linux的fork()函数创建两个子进程。
2、创建进程时当第一次调用fork函数时,创建出一个新的子进程,即重新开始一个进程从该程序开头重新执行,程序大致内容相同,唯一不同的地方在于再次调用该fork函数时返回值为0。
3、fork()函数的使用:查阅资料得知fork函数返回的是一个pid型的变量,也可以用int型(整型)存储,当返回值是小于0时,意味着进程创建失败,可以用while函数来表示该步骤遭到阻塞;当返回值等于0时,意味着该进程已经是创建完成的子进程;当返回值大于0时,意味着该进程成功创建了一个子进程,返回值为该子进程的pid(个人认为在理解的时候可以将其当作一个地址指针)。
4、lockf函数的使用:lockf函数是linux自带的一个进行进程互斥的函数(即具体的PV函数),该函数一般包含三个参数,第一个参数查资料说是通过open返回的打开文件描述符,但是具体还是没有理解该参数的意义,只知道在实际应用的过程中赋值为1可以将该操作看成上锁与解锁;第二个参数是一个cmd指令,具体表现为上锁(1),解锁(0),检查是否上锁等等;第三个参数是代表一个长度,个人理解是在程序编译成最基础的汇编指令时,上锁的作用域具体有多大,即上锁有效的字长有多少。
5、互斥的具体使用:当多进程同时占用I/O设备时,若没有上锁和解锁的操作(即互斥),就会导致I/O设备遭到争抢,具体体现在实验1,2中就是输出没有固定顺序,趋于随机。
1、创建进程:使用fork函数时有两种办法判断进程是否创建成功,第一种是用if(fork()<0)判断;第二种是使用while(fork()<0);来判断,相较于第一种,第二中的好处在于如果由于内存不够或者其他暂时性原因无法创建子进程,反复请求可以在条件允许的情况下第一时间将其从阻塞态变成就绪态,但是缺点是一直都在占用资源,不会挂起。
while ((son1 = fork()) < 0); //保证成功创建进程
if (son1 == 0) cout << "b\n"; //son1==0时,表示到达子进程,输出
2、互斥:具体操作即使用lockf(1,1,0)(上锁),lockf(1,0,0)(解锁),但是要注意放置的位置,否则可能不能成功完成进程的互斥逻辑。
lockf(1, 1, 0); //上锁
for (int i = 1; i <= 3; i++) {
cout << "son process 1\n";
}
lockf(1, 0, 0); //解锁
代码
#include
#include
#include
using namespace std;
int main() {
int son1, son2;
while ((son1 = fork()) < 0); //保证成功创建进程
if (son1 == 0) cout << "b\n"; //son1==0时,表示到达子进程,输出
else {
while ((son2 = fork()) < 0); //保证成功创建进程
if (son2 == 0) cout << "c\n"; //son2==0时,表示到达子进程,输出
else {
//wait(0);添加后结果固定,有进程的同步
cout << "a\n"; //子进程结束,执行父进程
}
}
}
当不添加13行进程同步语句时,由于进程之间争夺I/O设备,结果并不一致
代码
#include
#include
using namespace std;
int main() {
int son1, son2;
while ((son1 = fork()) < 0); //保证成功创建进程
if (son1 == 0) { //son1==0时,表示到达子进程,输出
for (int i = 1; i <= 3; i++) {
cout << "son process 1\n";
}
} else {
while ((son2 = fork()) < 0); //保证成功创建进程
if (son2 == 0) { //son2==0时,表示到达子进程,输出
for (int i = 1; i <= 3; i++) {
cout << "son process 2\n";
}
} else {
for (int i = 1; i <= 3; i++) {
cout << "parent process \n";
}
}
}
}
实验结果
由于进程间没有互斥,每次编译运行结果随机。
代码
#include
#include
using namespace std;
int main() {
int son1, son2;
while ((son1 = fork()) < 0); //保证成功创建进程
if (son1 == 0) { //son1==0时,表示到达子进程,输出
lockf(1, 1, 0); //上锁
for (int i = 1; i <= 3; i++) {
cout << "son process 1\n";
}
lockf(1, 0, 0); //解锁
} else {
while ((son2 = fork()) < 0); //保证成功创建进程
if (son2 == 0) { //son2==0时,表示到达子进程,输出
lockf(1, 1, 0); //上锁
for (int i = 1; i <= 3; i++) {
cout << "son process 2\n";
}
lockf(1, 0, 0); //解锁
} else {
lockf(1, 1, 0); //上锁
for (int i = 1; i <= 3; i++) {
cout << "parent process \n";
}
lockf(1, 0, 0); //解锁
}
}
}
实验结果
由于通过lockf()函数实现上锁和解锁的操作保证了进程的互斥性,输出结果稳定。
值得注意的是,进程上锁的位置很重要,如果将上锁位置改变(如下图),则无法达到进程互斥的目的。
熟悉signal函数,理解信号处理的原理。
通过Linux提供的系统调用signal(),来说明如何执行一个预先安排好的信号处理函数。signal()的返回值是指向一个函数的指针,该函数的参数为一个整数。
signal函数往往有两个参数,第一个参数指明了需要处理信号的类型,如SIGINT(中断),SIGILL(非法指令异常)等指令类型;第二个参数描述了描述了与信号关联的动作,当然也可以自己写一个函数来进行操作,就像该次实验一样。
另外,signal函数的返回值是handler(似乎可以理解成一种指针),在本次实验中,我们可以重新定义输入指令的意义,即使得ctrl+c(原义为中断),重定义为计算该类型输入的次数,同时再重定义为原本功能(中断)。
重点函数在事先貌似已经给出,如下。
__sighandler_t old_p = signal(SIGINT, ctrl_c);//改变ctrl+c读入意义,并存储原意义
void ctrl_c(int signum) {
signal(SIGINT, ctrl_c);
++ctrl_c_count; //ctrl_c_count设置为全局变量
}//将ctrl+c改为计数按此种读入的次数
代码
#include
#include
#include
using namespace std;
int ctrl_c_count;
void ctrl_c(int signum) {
signal(SIGINT, ctrl_c);
++ctrl_c_count; //ctrl_c_count设置为全局变量
}//将ctrl+c改为计数按此种读入的次数
int main() {
char c;
__sighandler_t old_p = signal(SIGINT, ctrl_c);//改变ctrl+c读入意义,并存储原意义
while ((c = getchar()) != '\n') ;
cout << ctrl_c_count << endl;//输出按ctrl+c次数
signal(SIGINT, old_p);//还原ctrl+c原始功能
while ((c = getchar()) != '\n') ;
cout << ctrl_c_count << endl;//尝试再次输出
}
实验结果
可以发现,在更改ctrl+c功能后,我们第一次输入就变成了计数,但是在还原原始功能后,就变成了中断,并没有继续执行18行的代码。
实现软中断通信,子进程的终止。
结合前两次实验内容以及kill函数实现以下功能:
编制一段程序,使用系统调用 fork()创建两个子进程,再用系统调用 signal()让父进程捕捉键盘上来的中断信号(即按 ctrl+c 键),当捕捉到中断信号后,父进程用系统调用kill()向两个子进程发出信号,子进程捕捉到信号后,分别输出下列信息后终止:
child process1 is killed by parent!
child process2 is killed by parent!
父进程等待两个子进程终止后,输出以下信息后终止:
parent process is killed!
kill函数的使用:kill函数主要也有两个参数,第一个参数即为中断进程的pid值(即指明要中断的进程),第二个值是进程的类型(如中断本进程(SIGABRT),中断子进程(SIGCHILD)等),
其中第二个参数既可以是数值,也可以是中断类型,而17 即是SIGCHILD,所以不论传值17还是传值SIGCHILD都是等价的。
wait函数完成进程同步,防止在子进程还未终止时父进程已经结束。
wait_signal();
kill(p1, SIGCHLD);
kill(p2, SIGCHLD); //SIGCHILD换成17也是同样的效果
wait(NULL);//进程同步,防止在子进程还未中断时父进程先中断
代码
#include
#include
#include
#include
using namespace std;
int process_stop;
void ctrl_c_stop(int signum) {
process_stop = 1;
}//当按下ctrl+c时process_stop值变为1
void wait_signal() {
while (process_stop == 0); //等待按下ctrl+c
}
int main() {
int p1, p2;
while ((p1 = fork()) < 0);
if (p1 != 0) {
while ((p2 = fork()) < 0);
if (p2 != 0) {
process_stop = 0;
signal(SIGINT, ctrl_c_stop);
wait_signal();
kill(p1, SIGCHLD);
kill(p2, SIGCHLD); //SIGCHILD换成17也是同样的效果
wait(NULL);//进程同步,防止在子进程还未中断时父进程先中断
lockf(1, 1, 0); //上锁
cout << "parent process is killed!\n";
lockf(1, 0, 0); //解锁
} else {
process_stop = 0;
signal(SIGINT, ctrl_c_stop);
wait_signal();
lockf(1, 1, 0); //上锁
cout << "child process1 is killed by parent!\n";
lockf(1, 0, 0); //解锁
}
} else {
process_stop = 0;
signal(SIGINT, ctrl_c_stop);
wait_signal();
lockf(1, 1, 0); //上锁
cout << "child process2 is killed by parent!\n";
lockf(1, 0, 0); //解锁
}
}
实验结果
由于中断没有互斥,所以两个子进程的输出先后顺序不一定固定。
模拟基本分页存储管理
模拟基本分页存储管理
输入:一个逻辑地址
输出:相应的物理地址
(页表由自己设计一个数据结构,不考虑页表寄存器)
页表存储逻辑,包含两部分,页码、内存块编号,本质其实就是一个对照表查找的过程。
在输入是十进制的情况下,我们可以使用相对地址/页的大小得到在该进程中该地址处于第几页,再使用相对地址%页的大小得到在该页的哪个相对位置,其中,当我们计算处于第几页后,我们需要通过查找页表来判断是否缺页。
在输入是二进制的情况下,若页表项占32位,页面大小占10位,其实通过模拟二进制除法我们不难发现,其实过程与十进制类似,最后10位代表在某一页中的相对地址,前面22位代表在第几页。
int cal_real_page(string bina_oppo_loca) {
int page_number = 0;
for (int i = 0; i <= 21; i++) {
page_number += (1 << (21 - i)) * (bina_oppo_loca[i] - '0');
}
return page_number;
}//二进制转十进制,读取前22位得到页码
int cal_real_oppo(string bina_oppo_loca) {
int loca_number = 0;
for (int i = 22; i <= 31; i++) {
loca_number += (1 << (31 - i)) * (bina_oppo_loca[i] - '0');
}
return loca_number;
}//二进制转十进制,读取后10位得到页码内相对位置
1、普通暴力法(单次查找时间复杂度O(n))
int search(int pre_page_number) {
for (int i = 0; i < N; i++) {
if (pre_page_number == Temp_Page[i].page_number) return Temp_Page[i].seg_number;
}
return -1;
}//通过查找页号对应内存块
2、数据结构法(单次查找时间复杂度O(logn))
该方法使用了红黑树,由于手写红黑树篇幅较大,我们不妨使用stl自带的map函数,该方法能加快查找速度。
int search(int pre_page_number) {
if(Temp_Page.count(pre_page_number)) {
return Temp_Page[pre_page_number];
}
return -1;
}//通过查找页号对应内存块
代码
方法一,十进制暴力查找,时间复杂度O(n)
#include
const int N = 1000010;
using namespace std;
struct page_list {
int page_number;
int seg_number;
} Temp_Page[N]; //页表
struct seg_store {
int abs_loca;
int next_cmd;
} Temp_cmd[N]; //内存块对应指令
int search(int pre_page_number) {
for (int i = 0; i < N; i++) {
if (pre_page_number == Temp_Page[i].page_number) return Temp_Page[i].seg_number;
}
return -1;
}//通过查找页号对应内存块
int get_next(int abs_loca) {
for (int i = 0; i < N; i++) {
if (Temp_cmd[i].abs_loca == abs_loca) {
return Temp_cmd[i].next_cmd;
}
}
return -1;
}//通过内存块取下一地址
int main() {
int oppo_loca;
int page_size;
int page_list_size;
for (int i = 0; i < N; i++) {
Temp_cmd[i].abs_loca = i;
Temp_cmd[i].next_cmd = i + 1;
}//设置每个地址位置的实际指令,可以自行设置,这里假设全部都是下一指令位置
cout << "页面大小:10位\n";
page_size = (1 << 10); //默认32位,其中页面大小为1024,可自行更改
cout << "输入页表大小:\n";
cin >> page_list_size;
cout << "输入页表内容:\n页码 内存块位置\n";
for (int i = 0; i < page_list_size; i++) {
//cin>>Temp_Page[i].page_number>>Temp_Page[i].seg_number;
Temp_Page[i].page_number = i;
Temp_Page[i].seg_number = i; //可以自行输入,默认一一对应
}
cout << "输入逻辑地址:\n";
cin >> oppo_loca;
int pre_page_number = oppo_loca / page_size; //输入是十进制时商即为页码
while (search(pre_page_number) != -1) {
int now_seg_number = search(pre_page_number); //对照查找在哪一个内存块
int abs_loca = now_seg_number * page_size + oppo_loca % page_size; //求绝对地址
cout << "now_seg_number: " << now_seg_number << '\n'; //内存块
cout << "abs_loca:" << abs_loca << '\n'; //绝对地址
oppo_loca = get_next(abs_loca); //访问指令集接收下一逻辑地址
pre_page_number = oppo_loca / page_size; //求页码
if (oppo_loca == -1) { //若没有找到,则终止循环
cout << "error:fail to find next command\n";
return 0;
}
}
cout << "* " << pre_page_number << '\n'; //没有找到页码则缺页
cout << "error:short of page\n";
}
//十进制暴力写法
方法二,十进制红黑树map查找,时间复杂度O(logn)
#include
const int N = 1000010;
using namespace std;
mapTemp_Page;//页表
mapTemp_cmd;//内存块对应指令
int search(int pre_page_number) {
if(Temp_Page.count(pre_page_number)) {
return Temp_Page[pre_page_number];
}
return -1;
}//通过查找页号对应内存块
int get_next(int abs_loca) {
if(Temp_cmd.count(abs_loca)) {
return Temp_cmd[abs_loca];
}
return -1;
}//通过内存块取下一地址
int main() {
int oppo_loca;
int page_size;
int page_list_size;
for(int i=0;i>page_list_size;
cout<<"输入页表内容:\n页码 内存块位置\n";
for(int i=0;i>page_number>>seg_number;
Temp_Page[i]=i;//可以自行输入,默认一一对应
}
cout<<"输入逻辑地址:\n";
cin>>oppo_loca;
int pre_page_number=oppo_loca/page_size;//输入是十进制时商即为页码
while(search(pre_page_number)!=-1){
int now_seg_number=search(pre_page_number);//对照查找在哪一个内存块
int abs_loca=now_seg_number*page_size+oppo_loca%page_size;
cout<<"now_seg_number: "<
方法三,二进制红黑树map查找,时间复杂度O(logn)
#include
const int N = 1000010;
using namespace std;
mapTemp_Page; //页表
mapTemp_cmd; //内存块对应指令
int search(int pre_page_number) {
if (Temp_Page.count(pre_page_number)) {
return Temp_Page[pre_page_number];
}
return -1;
}//通过查找页号对应内存块
string get_next(int abs_loca) {
if (Temp_cmd.count(abs_loca)) {
return Temp_cmd[abs_loca];
}
return "-1";
}//通过内存块取下一地址
int cal_real_page(string bina_oppo_loca) {
int page_number = 0;
for (int i = 0; i <= 21; i++) {
page_number += (1 << (21 - i)) * (bina_oppo_loca[i] - '0');
}
return page_number;
}//二进制转十进制,读取前22位得到页码
int cal_real_oppo(string bina_oppo_loca) {
int loca_number = 0;
for (int i = 22; i <= 31; i++) {
loca_number += (1 << (31 - i)) * (bina_oppo_loca[i] - '0');
}
return loca_number;
}//二进制转十进制,读取后10位得到页码内相对位置
string to_bina(int p) {
string res;
for (int i = 0; i <= 31; i++) {
res = char((((1 << i)&p) >> i) + '0') + res;
}
return res;
}//将十进制物理地址转化为二进制
int main() {
int page_size;
int page_list_size;
for (int i = 0; i < N / 10; i++) {
Temp_cmd[i] = to_bina(i + 1);
}//设置每个地址位置的实际指令,可以自行设置,这里假设全部都是下一指令位置
cout << "大小:32位(前22位代表页码,后10位代表页内相对地址)\n";
page_size = (1 << 10); //默认32位,其中页面大小为1024,可自行更改
cout << "输入页表大小:\n";
cin >> page_list_size;
cout << "输入页表内容:\n页码 内存块位置\n";
for (int i = 0; i < page_list_size; i++) {
// int page_number,seg_number;
// cin>>page_number>>seg_number;
Temp_Page[i] = i; //可以自行输入,默认一一对应
}
cout << "输入逻辑地址:\n";
string bina_oppo_loca;
cin >> bina_oppo_loca;
int pre_page_number = cal_real_page(bina_oppo_loca);
//输入是二进制时即为前22位二进制表示
while (search(pre_page_number) != -1) {
int now_seg_number = search(pre_page_number); //对照查找在哪一个内存块
int abs_loca = now_seg_number * page_size + cal_real_oppo(bina_oppo_loca);
cout << "now_seg_number: " << now_seg_number << '\n'; //内存块
cout << "abs_loca:" << abs_loca << '\n'; //绝对地址
bina_oppo_loca = get_next(abs_loca); //访问指令集接收下一逻辑地址
pre_page_number = cal_real_page(bina_oppo_loca); //求页码
if (bina_oppo_loca == "-1") { //若没有找到,则终止循环
cout << "error:fail to find next command\n";
return 0;
}
}
cout << "* " << pre_page_number << '\n'; //没有找到页码则缺页
cout << "error:short of page\n";
}
//二进制map优化写法