操作系统实验README
任务1:华为云上openEuler操作系统环境
一、进程相关编程实验
a) 观察进程调度
b) 观察进程调度中的全局变量改变
c) 在子进程中调用system函数
d) 在子进程中调用exec族函数
在此实验中完成:
(1)熟悉操作命令、编辑、编译、运行程序。完成操作系统原理课程教材P103作业 3.7 (采用图3-32所示的程序)的运行验证,多运行程序几次观察结果;去除wait后再观察结果并进行理论分析。
(2)扩展图3-32的程序,添加一个全局变量并在父进程和子进程中对这个变量做不同操作,输出操作结果并解释;在return前增加对全局变量的操作并输出结果,观察并解释;修改程序体会在子进程中调用system函数和在子进程中调用exec族函数;
二、线程相关编程实验
创建两个线程运行后体会线程共享进程信息的知识。
扩展要求:
定义全局变量,在父子进程分别操作,观察结果并解释现象
父子进程运行不同程序(可使用exec函数实现)
使用pthread实现多线程(可直接得满分)
==================================
一、进程相关编程实验
实验内容(1)
熟悉操作命令、编辑、编译、运行程序。完成操作系统原理课程教材P103作业 3.7 (采用图3-32所示的程序)的运行验证,多运行程序几次观察结果;去除wait后再观察结果并进行理论分析。
实验流程
下载putty和winscp
在putty上成功登陆
遇到的问题1:第一次编译时程序报错,因为无法识别函数wait(NULL)
分析原因,没有包含的头文件#include<sys/wait.h>
修改后成功编译
书上的代码
#include<sys/types.h>
#include<stdio.h>
#include<unistd.h>
int main()
{
pid_t pid,pid1;
/*fork child process*/
pid =fork();
if(pid<0){/* error occured */
fprintf(stderr,"Fork Failed");
return 1;
}
else if(pid == 0){/*child process */
pid1 = getpid();
printf("child: pid = %d\n", pid);/* A */
printf("child: pid1 = %d\n", pid1);/* B */
}
else{/* parent process */
pid1 = getpid();
printf("parent: pid = %d\n", pid);/* C */
printf("parent: pid1 = %d\n", pid1);/* D */
wait(NULL);
}
return 0;
}
包含wait(NULL)运行结果:
解释:
- 分析各个pid值相同与不同的原因: fork()
函数调用一次,返回两次。两次返回的区别是:子进程的返回值是0,父进程返回值为新子进程的进程ID。
调用fork()函数以后,若未成功创建子进程,则函数返回值为-1;若成功创建子进程,其中父进程的返回值为子进程的地址,子进程的返回值为0,余下代码由父子进程共同执行。
对于子进程,child:pid值为0,子进程进入else if(pid==0)语句以后,调用getpid()函数返回值为调动函数的进程的地址,即其自身的地址(child:pid1为子进程的地址)。
对于父进程,parent:pid值为子进程的地址,进入else语句后,调用getpid()函数,其返回值为调用函数的进程的地址,即其自身的地址(parent:pid1为父进程的地址)。
综上所述, Parent:pid的值应与child:pid1的值相同,都为子进程的地址; Parent:pid1的值为父进程的地址;Child:pid的值为0。
不包含wait(NULL)运行结果:
- 分析有无wait()函数造成结果差异的原因:
如果在父进程部分加入语句wait(NULL),意味着父进程会等待子进程退出后发出信号后再退出,导致父进程会等待子进程结束。
如果在父进程部分没有加入语句wait(NULL),意味着父进程会等待子进程退出后发出信号后再退出,导致父进程会等待子进程结束。(如图结果所示,在包含wait(NULL)函数时,父进程先退出,在未包含时,子进程先退出)
fork出的父子进程并不保证任何有规律的执行顺序! 不同linux发行版,不同的系统配置环境,不同的系统负载下很可能结果就不一样了。也就是说,在退出之前执行的语句,实际上每一次都是随机的,由操作系统的调度算法决定。
实验内容(2)
扩展图3-32的程序,添加一个全局变量并在父进程和子进程中对这个变量做不同操作,输出操作结果并解释;在return前增加对全局变量的操作并输出结果,观察并解释;修改程序体会在子进程中调用system函数和在子进程中调用exec族函数;
添加全局变量并在父进程和子进程中对这个变量做不同操作
实验步骤
#include<sys/types.h>
#include<sys/wait.h>
#include<stdio.h>
#include<unistd.h>
int global=3;//定义全局变量
int main()
{
pid_t pid,pid1;
/*fork child process*/
int local=3;//定义局部变量
pid =fork();
if(pid<0){/* error occured */
fprintf(stderr,"Fork Failed");
return 1;
}
else if(pid == 0){/*child process */
global++;
printf("i am child: global = %d\n", global);
}
else{/* parent process */
global--;
printf("i am parent: global = %d\n", global);
wait(NULL);
}
return 0;
}
运行结果:
解释:
子进程创建以后,会继承父进程的全局变量(child:global=3),(parent:global=3)但是继承的是父进程刚开始全局变量的值。
但是子进程创建以后,子进程修改了变量,或者父进程修改了全局变量的值,父子进程就互相都不影响了。
如图所示,父进程对其自己堆中的全局变量global进行–操作,得到global=2,子进程再对自己堆中的全局变量global进行++操作,得到global=4。
system函数与exec函数
首先在网上查阅资料了解exec()和system()的功能
exec族函数功能:用exec函数可以把当前进程替换为一个新进程,且新进程与原进程有相同的PID。
在子进程中调用system函数
system函数执行了三步操作:
- fork一个子进程;
- 在子进程中调用exec函数去执行command;
- 在父进程中调用wait去等待子进程结束。对于fork失败,system()函数返回-1。
如果exec执行成功,也即command顺利执行完毕,则返回command通过exit或return返回的值。
如果exec执行失败,也即command没有顺利执行,比如被信号中断,或者command命令根本不存在,system()函数返回127。
如果command为NULL,则system()函数返回非0值,一般为1。
使用system函数
#include<sys/types.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
pid_t pid,pid1;
/*fork child process*/
pid =fork();
if(pid<0){/* error occured */
fprintf(stderr,"Fork Failed");
return 1;
}
else if(pid == 0){/*child process */
printf("I am child\n", pid1);
system("ls -a");
printf("I am child again\n", pid1);
}
else{/* parent process */
printf("i am parent\n");
}
return 0;
}
运行结果:运行了子进程中execl()后的语句
使用execl函数
#include<sys/types.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
pid_t pid,pid1;
/*fork child process*/
pid =fork();
if(pid<0){/* error occured */
fprintf(stderr,"Fork Failed");
return 1;
}
else if(pid == 0){/*child process */
printf("I am child\n", pid1);
execl( "/bin/ls","ls","-a",NULL);
printf("I am child again\n", pid1);
}
else{/* parent process */
printf("i am parent\n");
}
return 0;
}
运行结果:未运行子进程中execl()后的语句
返回子进程的id
解释结果:
system()和exec()都可以执行进程外的命令,system是在原进程上开辟了一个新的进程,去执行特定的任务,但是exec是用新进程(命令)覆盖了原有的进程。
线程pthread
根据操作系统教材中得到的知识,线程是轻量化的线程。其共享code&data,优先权,操作系统资源,但是不共享PC、寄存器、栈指针/空间。 一个进程可以有很多线程,每条线程并行执行不同的任务。
可以这么理解,线程中没有堆这个概念,所有线程共享一个堆区域。
在进行实验前,我们首先需要熟悉线程/进程间的同步和互斥概念。
互斥:是指三部在不同进程之间的若干程序片断,当某个进程运行其中一个程序片段时,其它进程就不能运行它们之中的任一程序片段,只能等到该进程运行完这个程序片段后才可以运行。互斥操作是为了解决进程之间的协作关系而建立的一种机制。
同步:是指散步在不同进程之间的若干程序片断,它们的运行必须严格按照规定的 某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。
同步操作是为了解决进程之间的协作关系而引入的一种机制。
不难看出,进程互斥关系是一种特殊的进程同步关系,即逐次使用互斥共享资源,也是对进程使用资源次序上的一种协调。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
int a = 1; // 全局变量
void run() { ++a; printf("%d\n",a);}
int main() {
int tmp1, tmp2;
pthread_t thread1, thread2;
int ret_thrd1, ret_thrd2;
//线程1--------------------------
ret_thrd1 = pthread_create(&thread1, NULL, (void *)&run, NULL);
if (ret_thrd1 != 0) {
printf("thread1 create error\n");
} else {
printf("thread1 create success\n");
}
pthread_join(thread1,NULL);
//线程2--------------------------
ret_thrd2 = pthread_create(&thread2, NULL, (void *)&run, NULL);
if (ret_thrd2 != 0) {
printf("thread2 create error\n");
} else {
printf("thread2 create success\n");
}
pthread_join(thread2,NULL);
}
运行结果
结果分析:
由于两个线程共享全局变量,因此对其做++操作时即是对全局变量做两次++操作。
如果我们重写run()函数,并让thread执行不同的run()函数,看看pthread_join对线程之间执行顺序有什么影响。
int a = 1; // 全局变量
void run() { int i=0; while(i<100){ a=a+5;i++;printf("%d\n",a);} }
void run2() { int i=0; while(i<100){ a=a-1;i++;printf("%d\n",a);} }
int main() {
int tmp1, tmp2;
pthread_t thread1, thread2;
int ret_thrd1, ret_thrd2;
ret_thrd1 = pthread_create(&thread1, NULL, (void *)&run, NULL);
if (ret_thrd1 != 0) {
printf("thread1 create error\n");
}
else {
printf("thread1 create success\n");
}
pthread_join(thread1,NULL);
ret_thrd2 = pthread_create(&thread2, NULL, (void *)&run2, NULL);
if (ret_thrd2 != 0) {
printf("thread2 create error\n");
} else {
printf("thread2 create success\n");
}
pthread_join(thread2,NULL);
}
以上是输出部分结果,可以看出线程thread1先执行完run()后,线程2才开始执行run2();
倘若我们去除pthread_join()函数,再观察结果。
结果可以看出,线程之间的同步关系被打破了,线程thread1还未执行完,就被thread2强占了资源,而输出结果远远没有达到100次,这是由于操作系统内部实现了线程之间的互斥操作,thread2和thread1同一时间只有一个可以对临界区(critical section)进行操作。
由此,我们可以得出pthread_join()函数的作用:以阻塞的方式等待thread指定的线程结束。当函数返回时,被等待线程的资源被收回。如果线程已经结束,那么该函数会立即返回。
在代码中,当thread1运行结束之后对资源进行回收,在这之前不允许thread2对全局变量进行操作,这实现了进程之间的互斥。
代码中如果没有pthread_join主线程会很快结束从而使整个进程结束,从而使创建的线程没有机会开始执行就结束了(这是thread2也没有执行100次的原因)。加入pthread_join后,主线程会一直等待直到等待的线程结束自己才结束,而在今天的例子中,主线程会等待线程的结束才继续执行下面的语句(创建thread2),使创建的线程有机会执行。