南京邮电大学操作系统实验一:进程、线程的创建与并发执行

实验原理及内容

  1. 进程标识符和ps命令
  1. 使用编辑器gedit helloProcess.c,新建一个helloProcess.c源文件,并输入后面的范例代码:

#include <stdio.h>

#include <sys/types.h>

#include <unistd.h>

int main()

{

//pid_t是数据类型,实际上是一个整型,通过typedef重新定义了一个名字,用于存储进程id

pid_t pid;

pid= getpid();   //getpid()函数返回当前进程的id

printf("Process id :%d\n", pid);

return 0;

}

使用gcc helloProcess.c -o helloProcess编译源程序,输入./ helloProcess  运行可执行文件。

1、在第10行前添加int i; scanf(“%d”,&i); 使程序等待输入,重新开启一个Terminal,使用ps -al命令查看当前进程列表,了解进程id,加深对进程概念的理解。

PPID:父进程ID;

PRI:动态值,内核经常更改,进程优先级,此值越小进程优先级越高;

NI:静态值,只是偶尔被用户手工更改,可以从-20到19不等,数值越小优先级越高;

sz :使用掉的内存大小; 

wchan :目前这个程序是否正在运作当中,若为 - 表示正在运作; 

tty :登入者的终端机位置; 

time 使用掉的 cpu 时间;

cmd :所下达的指令名称。

ps命令是“process status”的缩写,ps命令用于显示当前系统的进程状态。-a 显示一个终端的所有进程,除了会话引线。-l 长格式(有F,wchan,C 等字段)利用排序功能,对程序的名字进行排序(注意不是对程序的PID进行排序,因为即使程序相同,启动的时间不同或者操作系统中已经启动程序的数量不同,这个PID号码也就不同。即这个PID号码是自动生成的)

UID是用户IDPID是进程IDPPID是父进程ID

每个进程都有两个影响其调度的值:

第一个是动态值,内核经常更改这个值,也就是ps -l命令里的pri值。

第二个是静态值,只是偶尔被用户手工更改,即ps -l里的ni值。

对任何进程而言,ni这个值可以从-2019不等,其中数值越小优先级越高,数值越大优先级越低,-20的优先级最高,19的优先级最低,需要注意的是普通用户只能在019之间调整应用程序的优先权值,只有超级用户有权调整更高的优先权值(从-2019)。PRI即进程的优先级,此值越小进程的优先级别越高。

NI,也就是我们所要说的nice(通过nice命令设置),其表示进程可被执行的优先级的修正数值。如前面所说,PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice。所以,nice命令设置的优先级不是程序最终的优先级,而只是优先级的修正数值。

  1. 子进程的创建和执行
  1. 使用编辑器gedit新建一个childProcess.c源文件,并输入后面的范例代码。
  1. #include <stdio.h>
  2. #include <sys/types.h>
  3. #include <unistd.h>
  4. int main()
  5. {
  6. pid_t cid;
  7. printf("Before fork Process id :%d\n", getpid());
  8. /*
  9. fork()函数用于创建一个新的进程,该进程为当前进程的子进程,创建的方法是:将当前进程的内存内容完整拷贝一份到内存的另一个区域,两个进程为父子关系,他们会同时(并发)执行fork()语句后面的所有语句。
  10. fork()的返回值:
  11. 如果成功创建子进程,对于父子进程fork会返回不同的值,对于父进程它的返回值是子进程的进程id值,对于子进程它的返回值是0.
  12. 如果创建失败,返回值为-1.
  13. */
  14. cid = fork();
  15. printf("After fork, Process id :%d\n", getpid());
  16. return 0;
  17. }

保存退出gedit,使用gcc对源文件进行编译,然后运行,观察结果并解释原因。

 

在第16行前添加int i; scanf(“%d”,&i);,暂停程序的运行,重新开启一个Terminal,使用ps -al命令查看当前进程列表,查看父、子进程的PIDPPID

如果进程ID最大值没有达到系统进程数的上限,子进程比父进程ID大。

但是如果进程ID达到上限,系统会分配之前分配但是已经退出的进程ID给新进程,这样有可能出现子进程ID比父进程小。

2、通过判断fork的返回值,让父、子进程执行不同的语句,观察父、子进程的并发执行。

使用编辑器gedit重新编辑childProcess.c源文件,并输入后面的范例代码:

#include <stdio.h>

#include <sys/types.h>

#include <unistd.h>

int main()

{

    pid_t cid;

    printf("Before fork process id :%d\n", getpid());

    cid = fork();

  if(cid == 0){ //该分支是子进程执行的代码

     printf("Child process id (my parent pid is %d):%d\n", getppid(),getpid());

       for(int i=0; i<3 ; i++)

            printf("hello\n");

}else{ //该分支是父进程执行的代码

       printf("Parent process id :%d\n", getpid());

       for(int i=0; i<3 ; i++)

            printf("world\n");

    }

   return 0;

}

重新编译观察结果,重点观察父、子进程是否判断正确(通过比较进程id)。父、子进程其实是并发执行的,但实验结果好像是顺序执行的,多执行几遍看看有无变化,如果没有变化试着将两个循环的次数调整高一些,比如30300,然后再观察运行结果并解释原因。

上图解释了fork的工作流程,请大家参照代码仔细理解。

getpid()进程函数和getppid()父进程函数,在调用中都不能返回错误,输出进程ID和父进程ID。

当子进程从父进程内复制后,父进程与子进程内都有一个"pid"变量:在父进程中,fork()函数会将子进程的PID返回给父进程,即父进程的pid变量内存储的是一个大于0的整数;而在子进程中,fork()函数会返回0,即子进程的pid变量内存储的是0;如果创建进程出现错误,则会返回-1,不会创建子进程。

父子进程的运行先后顺序是完全随机的(取决于系统的调度),也就是说在使用fork()函数的默认情况下,无法控制父进程在子进程前进行还是子进程在父进程前进行。

3、验证和理解父进程和子进程内存空间彼此独立

  1. 在终端中进入自己的主目录,使用gedit编辑器直接新建文件helloProcess2.c,输入下面的代码,然后编译运行,解释其原因。
  1. #include <stdio.h>
  2. #include <sys/types.h>
  3. #include <unistd.h>
  4. int main()
  5. {
  6. pid_t cid;
  7. int x = 100;
  8. cid = fork();
  9. if(cid == 0){ //该分支是子进程执行的代码
  10. x++;
  11. printf("In child: x=%d\n",x);
  12. }else { //该分支是父进程执行的代码
  13. x--;
  14. printf("In parent: x=%d\n",x);
  15. }
  16. return 0;
  17. }            

(2)上述程序执行中,通常父进程先于子进程执行结束,我们可以尝试让子进程先执行结束。在上一步的代码的13行后添加如下语句,同时代码最顶端要包含一个新的头文件。

#include <sys/wait.h>

wait(NULL);

wait函数会让调用者陷入等待,直到子进程的状态变为可用(即子进程结束前父进程一直处于等待状态)。

为了让效果更清楚,在11行后加上如下语句:

sleep(3);

sleep该函数可以让调用进程睡上指定的时间长度(单位是second)。

重新编译代码运行,我们特意让子进程输出完毕后睡了3秒,在这期间父进程什么事也没有做一直在wait,直到子进程结束后父进程才执行printf语句。

 

5. 线程的创建和并发执行

(1) gedit helloThread.c以创建一个新的C语言源文件,将下面的代码拷贝进编辑器:

  1. #include <sys/types.h>
  2. #include <unistd.h>
  3. #include <stdio.h>
  4. #include <pthread.h>
  5. void* threadFunc(void* arg){ //线程函数
  6. printf("In NEW thread\n");
  7. }
  8. int main()
  9. {
  10. pthread_t tid;
  11. pthread_create(&tid, NULL, threadFunc, NULL); //线程创建函数
  12. pthread_join(tid, NULL);     //等待指定的线程结束
  13. printf("In main thread\n");
  14. return 0;
  15. }

pthread_create函数的参数列表:线程id的地址、线程属性的地址、线程函数的地址、线程函数参数地址

编译该段代码时,请注意gcc要加入新的参数,命令如下:

gcc helloThread.c -o helloThread -pthread

运行一下观察到什么现象了?将上面第12 行代码的注释掉又观察到了什么现象?查找资料回答为什么?

利用pthread_create()函数创建子线程的方法,创建的线程存在一个问题:在主线程创建完成子线程后,若子线程函数还没结束时,但是此时主线程函数已经结束,那么子线程也会被强制销毁。

pthread_join的第一个参数是创建的子线程线程ID,第二个参数是子线程函数的返回值地址的指针,也就是其返回值地址的地址。pthread_join的作用就是等待第一个参数指定的线程的结束,在等待期间该函数是阻塞的,等到子线程结束后,函数结束阻塞状态。

  1. gedit helloThread2.c以创建一个新的C语言源文件,将下面的代码拷贝进编辑器,编译运行,观察一下程序中同时包含3个线程(main, hello, world)时,输出的效果和并发父子进程的执行效果是否相似。
  1. #include <sys/types.h>
  2. #include <unistd.h>
  3. #include <stdio.h>
  4. #include <pthread.h>
  5. void* hello(void* arg){ //线程函数
  6. for(int i=0;i<300;i++)
  7. printf("hello(%d)\n",i);
  8. }
  9. void* world(void* arg){ //线程函数
  10. for(int i=0;i<300;i++)
  11. printf("world(%d)\n",i);
  12. }
  13. int main()
  14. { pthread_t tid1,tid2;
  15. pthread_create(&tid1, NULL, hello, NULL);
  16. pthread_create(&tid2, NULL, world, NULL);

  1. pthread_join(tid1, NULL);
  2. pthread_join(tid2, NULL);

  1. printf("In main thread\n");
  2. return 0;
  3. }

尝试在两个线程函数的循环体中加入sleep(1);  调节线程的执行速度。

 

线程并发交替执行。

   

加入sleep(1)后,顺序执行。让一个线程先sleep,另一个线程在这段时间内执行完毕,则输出的结果显示为顺序执行。

6. 验证和理解多线程共享进程的内存空间

在程序中增加全局变量value并进行初始化,在helloworld两个线程函数循环体的printf函数中用value++替代i,在主函数的printf语句中也对value变量进行自增和输出,重新编译和运行程序,观察该变量的变化情况,说明原因。

多进程地址空间是独立的,要共享数据需通过进程间通信。Linux同一进程的多线程间地址空间是共享的,产生的线程级别相同(虽然主线程还是需要wait子线程结束)可以直接共享变量实际上就连打开的文件都是共享的,共享进程的代码段、数据段、BSS段。页目录和页表应该是使用进程的页目录和页表。

在一个进程内的所有线程共享全局变量,能够在不适用其他方式的前提下完成多线程之间的数据共享,缺点是:线程是对全局变量随意遂改可能造成多线程之间对全局变量的混乱(即线程非安全)。

  

运用全局变量value最后值为600,表明全局变量在不同的线程当中访问全局变量是共享的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

cookie爱吃小饼干

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值