当前位置: 首页 > news >正文

北理工操作系统实验合集

文章目录

  • 进程控制API
    • Linux
      • getpid/getppid
      • fork/vfork
      • exit/_exit
      • exec函数族
      • wait/waitpid
      • pause/sleep
    • Windows
      • Windows创建进程
  • 进程间通信API(IPC-API)
    • Linux
      • 共享内存区
      • 信号量
    • Windows
  • 实验一:Linux内核编译
  • 实验二:生产者消费者进程
    • 共享内存案例
      • 一个基本的例子
    • Linux版本
    • Windows版本

进程控制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只复制后面的代码,前面的仅是资源共享,代码不共享。

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

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

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

在这里插入图片描述

exit/_exit

在这里插入图片描述

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

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

参考

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

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

在这里插入图片描述

exec函数族

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

在这里插入图片描述

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

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

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

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

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

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

在这里插入图片描述

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

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

wait/waitpid

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

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

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

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

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

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

在这里插入图片描述

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

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

pause/sleep

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

在这里插入图片描述

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

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

在这里插入图片描述

Windows

Windows创建进程

CreateProcess。

主要参数是可执行文件,命令行参数,以及一个句柄引用。创建出来的进程句柄会以引用的方式返回到参数里。

在这里插入图片描述

进程间通信API(IPC-API)

IPC:InterProcess Communication

Linux

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

共享内存区

linux控制台中使用icps命令查看。

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

程序中用这三个接口:

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

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

信号量

信号量:semaphore

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

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

Windows

TODO

实验一:Linux内核编译

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

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

共享内存案例

一个基本的例子

共享内存例子

这个代码写的挺好,拿来可以直接跑,从宏观上来说,这是一个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就会检测到,并且输出:

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

Linux版本

Windows版本

相关文章:

  • 上海网站建设觉策/河北关键词seo排名
  • 清廉企业建设/谷歌seo外链
  • 网站有后台更新不了/网站怎么做外链
  • 自己在线制作logo免费一步一步/西安seo关键词推广
  • 什么网站做推广最好/网站免费网站免费
  • 借贷网站建设方案/如何自己免费制作网站
  • 【HAL库学习笔记】四、STM32串口与定时器
  • 编程初学者如何缓解迷茫和焦虑?墙裂推荐此文,助你赢在起跑线
  • 图解git原理
  • DOM特效模拟框拖拽
  • ITIL 4 Foundation知识体系-第四章:服务价值体系-2
  • 【备战十四届蓝桥杯 | 开篇】如何高效备战蓝桥杯
  • 北京化工大学2022-2023-1 ACM集训队每周程序设计竞赛(6)题解
  • SpringMVC 5 Rest 风格 5.4 RESTful 案例 5.4.1 需求分析 5.4.2 环境准备
  • Java学习笔记:Java中访问数据库
  • 【程序环境与预处理】
  • 【ASM】字节码操作 转换已有的类 ClassReader 修改字段信息 删除字段 增加字段
  • VIM使用进阶:VIM脚本初步