操作系统实验 进程调度

实验一 华为云上openEuler操作系统环境

1. 实验要求

进程相关编程实验

a) 观察进程调度
b) 观察进程调度中的全局变量改变
c) 在子进程中调用system函数
d) 在子进程中调用exec族函数

在此实验中完成:

(1)熟悉操作命令、编辑、编译、运行程序。完成操作系统原理课程教材P103作业 3.7 (采用图3-32所示的程序)的运行验证,多运行程序几次观察结果;去除wait后再观察结果并进行理论分析。
(2)扩展图3-32的程序,添加一个全局变量并在父进程和子进程中对这个变量做不同操作,输出操作结果并解释;在return前增加对全局变量的操作并输出结果,观察并解释;修改程序体会在子进程中调用system函数和在子进程中调用exec族函数;

验收包括:readme 、程序源码、程序运行及回答问题

验收内容

  • 基本要求:
    • 多次运行程序,观察输出顺序,解释现象
  • 扩展要求:
    • 定义全局变量,在父子进程分别操作,观察结果并解释现象
    • 父子进程运行不同程序(可使用exec函数实现)
    • 使用pthread实现多线程(可直接得满分)

2. 实验内容

2.1 搭建华为云实验环境

指导资料:华为云环境搭建.docx
一些主要操作:
(1) ssh登录

ssh是Secure Shell的缩写,即“安全外壳协议”。
传统的网络服务程序,如:ftp、pop和telnet在本质上都是不安全的,因为它们在网络上用明文传送口令和数据,别有用心的人非常容易就可以截获这些口令和数据。而且,这些服务程序的安全验证方式也是有其弱点的, 就是很容易受到“中间人”(man-in-the-middle)这种方式的攻击。所谓“中间人”的攻击方式, 就是“中间人”冒充真正的服务器接收你传给服务器的数据,然后再冒充你把数据传给真正的服务器。服务器和你之间的数据传送被“中间人”一转手做了手脚之后,就会出现很严重的问题。通过使用SSH,你可以把所有传输的数据进行加密,这样"中间人"这种攻击方式就不可能实现了,而且也能够防止DNS欺骗和IP欺骗。使用SSH,还有一个额外的好处就是传输的数据是经过压缩的,所以可以加快传输的速度。SSH有很多功能,它既可以代替Telnet,又可以为FTPPoP、甚至为PPP提供一个安全的"通道" [1] 。
参考链接:ssh (安全外壳协议)

进入命令行界面,输入ssh root@119.3.213.162,其中IP地址为我的云服务器地址。输入密码即可登录到服务器。
(2) 使用WinSCP管理文件

WinSCP 是一个 Windows 环境下使用的 SSH 的开源图形化 SFTP 客户端。同时支持 SCP 协议。它的主要功能是在本地与远程计算机间安全地复制文件,并且可以直接编辑文件。
参考链接:WinSCP

(3) 查看服务器信息

命令显示内容
uname –a查看总体架构
cat /etc/os-release查看操作系统信息
lscpu查看CPU信息
free查看内存信息
fdisk -l查看磁盘信息
top查看系统资源实时信息(按q退出)
gcc -version查看gcc版本

2.2 验证教材示例程序

任务要求:完成操作系统原理课程教材P103作业 3.7 的运行验证,多运行程序几次观察结果;去除wait后再观察结果并进行理论分析。

要求验证的代码如下:
pidtest.c

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

int main()
{
    pid_t pid,pid1;
    //fork a child process
    printf("\n");
    pid = fork();

    if(pid < 0)
    {
        //error occured
        fprintf(stderr, "Fork Failed");
        return 1;
    }
    else if (pid == 0)
    {
        //child process
        pid1 = getpid();
        printf("A child: pid = %d\n",pid);//A
        printf("B child: pid1 = %d\n",pid1);//B
    }
    else 
    {
        //parent process
        pid1 = getpid();
        printf("C parent: pid = %d\n",pid);//C
        printf("D parent: pid1 = %d\n",pid1);//D
        wait(NULL);           
    }
    
    return 0;
}

将_pidtest.c_上传到云服务器上,使用命令gcc pidtest.c -o pidtest编译,./pidtest执行,得到如图2.2.1所示结果。
![image.png](https://img-blog.csdnimg.cn/img_convert/bf29f731a878faeb32321cfd6edf70c9.png#clientId=uedbe467c-bdbb-4&from=paste&height=87&id=u5faac40a&margin=[object Object]&name=image.png&originHeight=91&originWidth=210&originalType=binary&ratio=1&size=15820&status=done&style=none&taskId=u2db75e69-432e-494e-83fa-3f6f2469aa3&width=200)
图2.2.1 执行pidtest所得结果
使用**_while true ;do ./pidtest; sleep 1; done;_**重复运行该程序,得到如图2.2.2所示结果。
![image.png](https://img-blog.csdnimg.cn/img_convert/ebc8136afaaed7fa254cbef87d28cbf2.png#clientId=ub88b9f10-2129-4&from=paste&height=435&id=ue2591949&margin=[object Object]&name=image.png&originHeight=398&originWidth=183&originalType=binary&ratio=1&size=17856&status=done&style=none&taskId=u932bbdca-747f-4cd9-9fe4-85d27453a33&width=200)
图2.2.2 重复执行pidtest所得结果
在多次运行中发现,既有可能child先执行,又有可能parent先执行。
去除wait(NULL)后观察结果,得到图2.2.3.
![image.png](https://img-blog.csdnimg.cn/img_convert/35ab3919e1beb1ab9ef071a4dc1e874c.png#clientId=ub88b9f10-2129-4&from=paste&height=203&id=uf310d41f&margin=[object Object]&name=image.png&originHeight=405&originWidth=191&originalType=binary&ratio=1&size=17839&status=done&style=none&taskId=u441e6a6b-49c3-4ba0-b9c8-872a125bd67&width=95.5)
图2.2.3 去掉wait(NULL)后执行所得结果
如图2.2.3所示,在去掉wait()后,同样也是既有可能child先执行,又有可能parent先执行。

fork()函数的功能如图2.2.4。执行了fork()后,系统便开始同时执行父进程和子进程。对于父进程,fork()函数返回子进程的id,所以C处打印的是子进程的id;对于子进程,fork()函数返回的是0,所以A处打印的是0。在B、D处打印的分别是子进程、父进程的id,所以B、C处打印的值相同。综上所述,A处打印的是0、B和C处打印的是子进程id、D处打印的是父进程的id。
![image.png](https://img-blog.csdnimg.cn/img_convert/310c855d08aa4e1fc0e83079d8012176.png#clientId=ua97fcea2-42d1-4&from=paste&height=207&id=u5f97d1a3&margin=[object Object]&name=image.png&originHeight=366&originWidth=884&originalType=binary&ratio=1&size=287472&status=done&style=none&taskId=u58e3237c-ac24-4915-a39b-fc25536fae3&width=500)
图2.2.4 fork()函数的功能
在去掉wait()前后,都是既有可能parent先输出,又有可能child先输出,这是因为父子进程的执行顺序是不确定的,既有可能父进程先执行,也有可能子进程先执行。wait()的作用是让父进程挂起等待子进程结束。所以只有将wait()放在父进程打印语句之前才能确保子进程先输出。

2.3 扩展示例程序

**任务要求:**扩展图3-32的程序,添加一个全局变量并在父进程和子进程中对这个变量做不同操作,输出操作结果并解释;在return前增加对全局变量的操作并输出结果,观察并解释;修改程序体会在子进程中调用system函数和在子进程中调用exec族函数;
扩展要求:

  • 定义全局变量,在父子进程分别操作,观察结果并解释现象
  • 父子进程运行不同程序(可使用exec函数实现)
  • 使用pthread实现多线程(可直接得满分)
2.3.1 定义全局变量并观察结果

定义一个全局整数global并初始化为0,在子进程中让其自增1,在父进程中使其自增2,将代码修改如下

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

int global = 0;

int main()
{
    pid_t pid,pid1;
    //fork a child process
    printf("\n");
    pid = fork();

    if(pid < 0)
    {
        //error occured
        fprintf(stderr, "Fork Failed");
        return 1;
    }
    else if (pid == 0)
    {
        //child process
        pid1 = getpid();
        global+=1;
        printf("A child: pid = %d\n",pid);//A
        printf("B child: pid1 = %d\n",pid1);//B
        printf("Global: %d\n",global);
    }
    else 
    {
        //parent process
        pid1 = getpid();
        global+=2;
        printf("C parent: pid = %d\n",pid);//C
        printf("D parent: pid1 = %d\n",pid1);//D
        printf("Global: %d\n",global);
        wait(NULL);           
    }
    
    return 0;
}

![image.png](https://img-blog.csdnimg.cn/img_convert/b7dc6e1a82b5fddef3c5648a7ac2ee46.png#clientId=ub88b9f10-2129-4&from=paste&height=116&id=udae9b7a1&margin=[object Object]&name=image.png&originHeight=107&originWidth=184&originalType=binary&ratio=1&size=5629&status=done&style=none&taskId=ud82d3fe6-ba7d-4c24-a0ce-91981dad7cb&width=200)
图2.3.1 父子进程对全局变量不同操作
子进程在创建时,继承了全局变量,虽然这两个变量都是Global,但是它们其实已经变成了两个,执行时在各自的物理空间内执行。子进程对该全局变量的操作是相互独立的,因此会分别对global操作,打印出的global值不同。

在return前加一个操作使global自增3,代码如下所示

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

int global = 0;

int main()
{
    pid_t pid,pid1;
    //fork a child process
    printf("\n");
    pid = fork();

    if(pid < 0)
    {
        //error occured
        fprintf(stderr, "Fork Failed");
        return 1;
    }
    else if (pid == 0)
    {
        //child process
        pid1 = getpid();
        global+=1;
        printf("A child: pid = %d\n",pid);//A
        printf("B child: pid1 = %d\n",pid1);//B
        printf("Global: %d\n",global);
    }
    else 
    {
        //parent process
        pid1 = getpid();
        global+=2;
        printf("C parent: pid = %d\n",pid);//C
        printf("D parent: pid1 = %d\n",pid1);//D
        printf("Global: %d\n",global);
        wait(NULL);           
    }
    global += 3;
    printf("Global: %d\n",global);
    return 0;
}

得到结果如图2.3.2所示。
![image.png](https://img-blog.csdnimg.cn/img_convert/572ccf79faa76971b24289beb2b16386.png#clientId=ub88b9f10-2129-4&from=paste&height=154&id=u72d9641d&margin=[object Object]&name=image.png&originHeight=143&originWidth=186&originalType=binary&ratio=1&size=6448&status=done&style=none&taskId=u4cdaba9e-4c61-4bed-af95-d9262c4c185&width=200)
图2.3.2 在return前加入操作
经过大量实验发现总是最后输出Global:5,这是因为在父进程中使用了wait(),在等待子进程结束后自己才会向下运行。

2.3.2 在子进程中调用system函数

在子进程中通过system()函数调用事先编译好的文件,修改代码如下

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

int main()
{
    pid_t pid,pid1;
    //fork a child process
    pid = fork();

    if(pid < 0)
    {
        //error occured
        fprintf(stderr, "Fork Failed");
        return 1;
    }
    else if (pid == 0)
    {
        //child process
        pid1 = getpid();
        printf("B child: pid1 = %d\n",pid1);//B
        system("./call_system_in_child");
        printf("子进程不会输出\n");
        // printf("A child: pid = %d\n",pid);//A        
    }
    else 
    {
        //parent process
        pid1 = getpid();
        // printf("C parent: pid = %d\n",pid);//C
        printf("D parent: pid1 = %d\n",pid1);//D
        wait(NULL);           
    }

    return 0;
}

事先编译好的_call_system_in_child.c_的源文件如下

# include<stdio.h>
# include <sys/types.h>
int main()
{
    pid_t pid = getpid();
    printf("\n在子进程中调用system函数,pid为%d\n",pid);
}

运行结果如图2.3.3所示
![image.png](https://img-blog.csdnimg.cn/img_convert/a9c9c8b0268bdba7b38364d45e49fc82.png#clientId=ub88b9f10-2129-4&from=paste&height=110&id=u7fd50606&margin=[object Object]&name=image.png&originHeight=67&originWidth=304&originalType=binary&ratio=1&size=6611&status=done&style=none&taskId=u70988f9e-34b7-4eca-8c20-5e67bb222e9&width=500)
图2.3.3 在子程序中调用system()函数
可以看到调用system()之后pid和之前有所不同,这是因为system()函数先执行了fork()函数,然后新产生的子进程立刻执行了exec()函数,产生了一个新进程,所以新进程的pid等与原进程不同。system.c的代码如下.
system.c

int system(const char * cmdstring)
{
  pid_t pid;
  int status;
 
  if(cmdstring == NULL){
      
      return (1);
  }
 
 
  if((pid = fork())<0){
 
        status = -1;
  }
  else if(pid == 0){
    execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);
    -exit(127); //子进程正常执行则不会执行此语句
    }
  else{
        while(waitpid(pid, &status, 0) < 0){
          if(errno != EINTER){
            status = -1;
            break;
          }
        }
    }
    return status;
}

另外,调用system()函数之后的打印语句没有执行,这是system()函数的特性,替换以后子进程不会运行原来的代码。

2.3.3 在子进程中调用exec函数

在子进程中通过exec()函数调用事先编译好的文件,修改代码如下

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

int main()
{
    pid_t pid,pid1;
    //fork a child process
    pid = fork();

    if(pid < 0)
    {
        //error occured
        fprintf(stderr, "Fork Failed");
        return 1;
    }
    else if (pid == 0)
    {
        //child process
        // printf("A child: pid = %d\n",pid);//A
        pid1 = getpid();
        printf("B child: pid1 = %d\n",pid1);//B
        execl("./call_exec_in_child","");
        printf("子进程不会输出\n");
        
    }
    else 
    {
        //parent process
        pid1 = getpid();
        // printf("C parent: pid = %d\n",pid);//C
        printf("D parent: pid1 = %d\n",pid1);//D
        wait(NULL);           
    }

    return 0;
}

事先编译好的_call_exec_in_child.c_的源文件如下

# include<stdio.h>
# include <sys/types.h>
int main()
{
    pid_t pid = getpid();
    printf("\n在子进程中调用exec族函数,pid为%d\n",pid);
}

运行结果如图2.3.4所示。
![image.png](https://img-blog.csdnimg.cn/img_convert/370180bd0b15990f565844faf87a8ba3.png#clientId=ub88b9f10-2129-4&from=paste&height=107&id=uffd45ab6&margin=[object Object]&name=image.png&originHeight=65&originWidth=304&originalType=binary&ratio=1&size=6571&status=done&style=none&taskId=ue2f8adc4-c042-4872-88ab-fd7745eb1d7&width=500)
图2.3.4 在子程序中调用exec()函数
可以看到在调用exec()函数后pid仍与原来子进程的pid相同,这是因为执行exec函数后,并没有产生新的进程,只是用调用的内容替换掉了原来的内容,所以pid不变。另外调用exec()后的打印语句仍然没有改变,这也说明了exec()函数会替换原来的内容,原来的内容不会执行。
参考链接:https://www.cnblogs.com/qingergege/p/6601807.html
https://blog.csdn.net/u014530704/article/details/73848573

2.3.4 使用pthread实现多线程

使用以下的代码使不同的线程打印自己的线程号
其中使用pthread_t创建线程号,pthread_create()创建线程,pthread_join()来等待线程执行完成,否则线程可能还没有执行主函数就结束了。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define NUMBER_OF_THREADS 10
 
void* threadfunc(void* tid);
int main(void){
    pthread_t threads[NUMBER_OF_THREADS];//tid线程标识符
    int status,i;
    for(i=0;i<NUMBER_OF_THREADS;i++){//循环创建10个现场
    	printf("主函数中创建第%d个线程\n",i);
        //创建线程,线程函数传入参数为i
    	status=pthread_create(&threads[i],NULL,threadfunc,(void*)i);//最后一个参数是给线程要运行的函数传的参数
		if(status!=0){//线程创建不成功,打印错误信息
    		printf("线程创建失败 %d\n",status);
    		exit(-1);
		}
	}
for(i=0;i<NUMBER_OF_THREADS;i++){
		pthread_join(threads[i],NULL);
	}
	exit(0);
}
void* threadfunc(void* tid){
	printf("这是第%d个线程\n",tid);//在线程函数中打印函数的参数
	pthread_exit(0);
}

运行结果如图2.3.5.
![image.png](https://img-blog.csdnimg.cn/img_convert/affeb474822078251f2830dde5f9bf87.png#clientId=ub88b9f10-2129-4&from=paste&height=303&id=u481eef38&margin=[object Object]&name=image.png&originHeight=321&originWidth=212&originalType=binary&ratio=1&size=16122&status=done&style=none&taskId=uc8c96a1a-a99b-4953-80cd-1ce295e0aa8&width=200)
图2.3.5 多线程结果1
可以看到线程的执行顺序是不一定的。
为了测试进程的互斥,修改代码如下

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define NUMBER_OF_THREADS 10
int global = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 
void* threadfunc(void* tid);
int main(void){
    pthread_t threads[NUMBER_OF_THREADS];//tid线程标识符
    int status,i;
    for(i=0;i<NUMBER_OF_THREADS;i++){//循环创建10个现场
    	printf("主函数中创建第%d个线程\n",i);
        //创建线程,线程函数传入参数为i
    	status=pthread_create(&threads[i],NULL,threadfunc,(void*)i);//最后一个参数是给线程要运行的函数传的参数
		if(status!=0){//线程创建不成功,打印错误信息
    		printf("线程创建失败 %d\n",status);
    		exit(-1);
		}
	}
for(i=0;i<NUMBER_OF_THREADS;i++){
		pthread_join(threads[i],NULL);
	}
	exit(0);
}
void* threadfunc(void* tid){
	printf("这是第%d个线程,",tid);//在线程函数中打印函数的参数
    pthread_mutex_lock(&mutex);
    global++;
    printf("global的值为%d\n",global);
    pthread_mutex_unlock(&mutex);
	pthread_exit(0);
}

输出效果如图2.3.6所示。
![image.png](https://img-blog.csdnimg.cn/img_convert/905c95cba81b482daebe181d1e25eb73.png#clientId=ub88b9f10-2129-4&from=paste&height=232&id=u62a5573d&margin=[object Object]&name=image.png&originHeight=327&originWidth=282&originalType=binary&ratio=1&size=31196&status=done&style=none&taskId=u68cd92df-b414-494d-919e-319b80d1488&width=200)
图2.3.6 多线程结果2
由于线程共享进程的变量,所以global的值会不断增加。
如果没有加互斥锁,会出现如图2.3.7的情况
![image.png](https://img-blog.csdnimg.cn/img_convert/fe5c86aa51fef8600a4eb12e4776abab.png#clientId=u3c13dbcf-4a2f-4&from=paste&height=161&id=ua4b368d5&margin=[object Object]&name=image.png&originHeight=322&originWidth=365&originalType=binary&ratio=1&size=29614&status=done&style=none&taskId=ueb3723b1-2c1a-441c-a852-a7ed494f989&width=182.5)
图2.3.7 没有加互斥锁的情况

参考链接:https://blog.csdn.net/briblue/article/details/87859716
https://blog.csdn.net/jay_zzs/article/details/106380659

附1:出现的问题

1. WinSCP拖拽文件时不响应/SSH client无法输入命令

一段时间不操作会出现连接重置,类似于系统休眠,可以尝试重新连接。

2. 无法切换目录

在命令行中使用pwd可以查看当前目录,默认在/root下。注意这个不是根目录,需要用cd …切换到根目录,也可以使用绝对路径一步切换。

3. 在子进程中调用execl函数,得到的pid不同于子进程

这是因为调用参数写反了,execl()第一个参数为某目录下某程序,第二个参数为传给该程序的参数。

4. 编译有关pthread的代码都会报错

pthread不是Linux下的默认的库,也就是在链接的时候,无法找到pthread库中函数的入口地址,于是链接会失败。在gcc编译的时候,附加要加 -lpthread参数即可解决。
参考链接:https://blog.csdn.net/jiangxinyu/article/details/7778864

附2:代码用到的一些头文件、数据类型和函数

1. 头文件sys/types.h

sys/types.h中文名称为基本系统数据类型。在应用程序源文件中包含 <sys/types.h> 以访问 _LP64 和 _ILP32 的定义。此头文件还包含适当时应使用的多个基本派生类型。尤其是以下类型更为重要:
caddr_t 核心地址。
clock_t 表示系统时间(以时钟周期为单位)。
comp_t 压缩的时钟滴答。
dev_t 用于设备号。
fd_set 文件描述集。
fpos_t 文件位置。
gid_t 数组值ID。
ino_t i节点编号。
off_t 用于文件大小和偏移量。
mode_t 文件类型,文件创建模式。
pid_t 进程ID和进程组ID
ptrdiff_t 是一种带符号整型,用于对两个指针执行减法运算后所得的结果。
rlim_t 资源限制;
size_t 反映内存中对象的大小(以字节为单位)。
ssize_t 供返回字节计数或错误提示的函数使用。
time_t 以秒为单位计时。
uid_t 数值用户ID。
wchar_t 能表示所有不同的字符码。
所有这些类型在 ILP32 编译环境中保持为 32 位值,并会在 LP64 编译环境中增长为 64 位值。
参考链接:

  1. ​头文件内容见https://www.cnblogs.com/johnnyzen/p/8016796.html
  2. 本简介引自https://www.cnblogs.com/suanec/p/4029539.html

2. 头文件unistd.h

In the C programming language , unistd.h is the name of the header file that provides access to the POSIX operating system API. It is defined by the POSIX.1 standard, the base of the Single Unix Specification, and should therefore be available in any conforming (or quasi-conforming) operating system/compiler (all official versions of Unix, including Mac OS X, Linux, etc.).
On Unix-like systems, the interface defined by unistd.h is typically made up largely of system call wrapper functions such as fork, pipe and I/O primitives (read, write, close, etc.).
Unix compatibility layers as Cygwin and MinGW also provide their own versions of unistd.h. In fact, those systems provide it along with the translation libraries that implement its functions in terms of Win32 functions. For example, in Cygwin, a header file can be found in /usr/include that sub-includes a file of the same name in /usr/include/sys. Not everything is defined in there but some definitions are done by references to the GNU C standard library headers (like stddef.h) which provide the type size_t and many more. Thus, unistd.h is only a generically defined adaptive layer that might be based upon already existing system and compiler specific definitions. This has the general advantage of not having a possibly concurrent set of header file defined, but one that is built upon the same root which, for this reason, will raise much fewer concerns in combined usage cases.

Functions

参考链接:

  1. 本简介引自https://en.wikibooks.org/wiki/C_Programming/POSIX_Reference/unistd.h
  2. 有关POSIX的简介见https://zhuanlan.zhihu.com/p/392588996

3. 几种文件类型和函数

pid_t
fork()
execlp()
wait()

参考资料

中国大学慕课-操作系统原理

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值