实验原理及内容
- 进程标识符和ps命令
- 使用编辑器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是用户ID,PID是进程ID,PPID是父进程ID。
每个进程都有两个影响其调度的值:
第一个是动态值,内核经常更改这个值,也就是ps -l命令里的pri值。
第二个是静态值,只是偶尔被用户手工更改,即ps -l里的ni值。
对任何进程而言,ni这个值可以从-20到19不等,其中数值越小优先级越高,数值越大优先级越低,-20的优先级最高,19的优先级最低,需要注意的是普通用户只能在0~19之间调整应用程序的优先权值,只有超级用户有权调整更高的优先权值(从-20~19)。PRI即进程的优先级,此值越小进程的优先级别越高。
NI,也就是我们所要说的nice值(通过nice命令设置),其表示进程可被执行的优先级的修正数值。如前面所说,PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice。所以,nice命令设置的优先级不是程序最终的优先级,而只是优先级的修正数值。
- 子进程的创建和执行
- 使用编辑器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());
- /*
- fork()函数用于创建一个新的进程,该进程为当前进程的子进程,创建的方法是:将当前进程的内存内容完整拷贝一份到内存的另一个区域,两个进程为父子关系,他们会同时(并发)执行fork()语句后面的所有语句。
- fork()的返回值:
- 如果成功创建子进程,对于父子进程fork会返回不同的值,对于父进程它的返回值是子进程的进程id值,对于子进程它的返回值是0.
- 如果创建失败,返回值为-1.
- */
- cid = fork();
- printf("After fork, Process id :%d\n", getpid());
- return 0;
- }
保存退出gedit,使用gcc对源文件进行编译,然后运行,观察结果并解释原因。
在第16行前添加int i; scanf(“%d”,&i);,暂停程序的运行,重新开启一个Terminal,使用ps -al命令查看当前进程列表,查看父、子进程的PID和PPID。
如果进程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)。父、子进程其实是并发执行的,但实验结果好像是顺序执行的,多执行几遍看看有无变化,如果没有变化试着将两个循环的次数调整高一些,比如30、300,然后再观察运行结果并解释原因。
上图解释了fork的工作流程,请大家参照代码仔细理解。
getpid()进程函数和getppid()父进程函数,在调用中都不能返回错误,输出进程ID和父进程ID。
当子进程从父进程内复制后,父进程与子进程内都有一个"pid"变量:在父进程中,fork()函数会将子进程的PID返回给父进程,即父进程的pid变量内存储的是一个大于0的整数;而在子进程中,fork()函数会返回0,即子进程的pid变量内存储的是0;如果创建进程出现错误,则会返回-1,不会创建子进程。
父子进程的运行先后顺序是完全随机的(取决于系统的调度),也就是说在使用fork()函数的默认情况下,无法控制父进程在子进程前进行还是子进程在父进程前进行。
3、验证和理解父进程和子进程内存空间彼此独立
- 在终端中进入自己的主目录,使用gedit编辑器直接新建文件helloProcess2.c,输入下面的代码,然后编译运行,解释其原因。
- #include <stdio.h>
- #include <sys/types.h>
- #include <unistd.h>
- int main()
- {
- pid_t cid;
- int x = 100;
- cid = fork();
- if(cid == 0){ //该分支是子进程执行的代码
- x++;
- printf("In child: x=%d\n",x);
- }else { //该分支是父进程执行的代码
- x--;
- printf("In parent: x=%d\n",x);
- }
- return 0;
- }
(2)上述程序执行中,通常父进程先于子进程执行结束,我们可以尝试让子进程先执行结束。在上一步的代码的13行后添加如下语句,同时代码最顶端要包含一个新的头文件。
#include <sys/wait.h>
wait(NULL);
wait函数会让调用者陷入等待,直到子进程的状态变为可用(即子进程结束前父进程一直处于等待状态)。
为了让效果更清楚,在11行后加上如下语句:
sleep(3);
sleep该函数可以让调用进程睡上指定的时间长度(单位是second)。
重新编译代码运行,我们特意让子进程输出完毕后睡了3秒,在这期间父进程什么事也没有做一直在wait,直到子进程结束后父进程才执行printf语句。
5. 线程的创建和并发执行
(1) gedit helloThread.c以创建一个新的C语言源文件,将下面的代码拷贝进编辑器:
- #include <sys/types.h>
- #include <unistd.h>
- #include <stdio.h>
- #include <pthread.h>
- void* threadFunc(void* arg){ //线程函数
- printf("In NEW thread\n");
- }
- int main()
- {
- pthread_t tid;
- pthread_create(&tid, NULL, threadFunc, NULL); //线程创建函数
- pthread_join(tid, NULL); //等待指定的线程结束
- printf("In main thread\n");
- return 0;
- }
pthread_create函数的参数列表:线程id的地址、线程属性的地址、线程函数的地址、线程函数参数地址
编译该段代码时,请注意gcc要加入新的参数,命令如下:
gcc helloThread.c -o helloThread -pthread
运行一下观察到什么现象了?将上面第12 行代码的注释掉又观察到了什么现象?查找资料回答为什么?
利用pthread_create()函数创建子线程的方法,创建的线程存在一个问题:在主线程创建完成子线程后,若子线程函数还没结束时,但是此时主线程函数已经结束,那么子线程也会被强制销毁。
pthread_join的第一个参数是创建的子线程线程ID,第二个参数是子线程函数的返回值地址的指针,也就是其返回值地址的地址。pthread_join的作用就是等待第一个参数指定的线程的结束,在等待期间该函数是阻塞的,等到子线程结束后,函数结束阻塞状态。
- 用gedit helloThread2.c以创建一个新的C语言源文件,将下面的代码拷贝进编辑器,编译运行,观察一下程序中同时包含3个线程(main, hello, world)时,输出的效果和并发父子进程的执行效果是否相似。
- #include <sys/types.h>
- #include <unistd.h>
- #include <stdio.h>
- #include <pthread.h>
- void* hello(void* arg){ //线程函数
- for(int i=0;i<300;i++)
- printf("hello(%d)\n",i);
- }
- void* world(void* arg){ //线程函数
- for(int i=0;i<300;i++)
- printf("world(%d)\n",i);
- }
- int main()
- { pthread_t tid1,tid2;
- pthread_create(&tid1, NULL, hello, NULL);
- pthread_create(&tid2, NULL, world, NULL);
- pthread_join(tid1, NULL);
- pthread_join(tid2, NULL);
- printf("In main thread\n");
- return 0;
- }
尝试在两个线程函数的循环体中加入sleep(1); 调节线程的执行速度。
线程并发交替执行。
加入sleep(1)后,顺序执行。让一个线程先sleep,另一个线程在这段时间内执行完毕,则输出的结果显示为顺序执行。
6. 验证和理解多线程共享进程的内存空间
在程序中增加全局变量value并进行初始化,在hello和world两个线程函数循环体的printf函数中用value++替代i,在主函数的printf语句中也对value变量进行自增和输出,重新编译和运行程序,观察该变量的变化情况,说明原因。
多进程地址空间是独立的,要共享数据需通过进程间通信。Linux同一进程的多线程间地址空间是共享的,产生的线程级别相同(虽然主线程还是需要wait子线程结束)可以直接共享变量实际上就连打开的文件都是共享的,共享进程的代码段、数据段、BSS段。页目录和页表应该是使用进程的页目录和页表。
在一个进程内的所有线程共享全局变量,能够在不适用其他方式的前提下完成多线程之间的数据共享,缺点是:线程是对全局变量随意遂改可能造成多线程之间对全局变量的混乱(即线程非安全)。
运用全局变量value最后值为600,表明全局变量在不同的线程当中访问全局变量是共享的。