一.进程的创建
1.fork()函数的使用
操作系统中有用户区和内核区两个部分,用户区主要是一些环境变量,命令行参数,堆、栈、动态库加载、代码区等等,内核区最主要的就是PCB了(进程id在这里),对于创建进程这块,我们会调用fork()函数,fork()会根据父进程来创建子进程,父子进程具有相同的用户区,不同的内核区
fork的函数原型:
pid_t pid =fork();
通过fork产生的子进程和父进程执行两个相互独立的代码段。通过查手册(man 2 fork)能够会看到很多注意事项如下:
fork函数的返回值:返回值是一pid_t类型 。父进程返回子进程的pid(也就是一个大于零的数),子进程返回0,注意:不是不是父进程返回两个值,而是父子进程各返回一个值。父进程执行返回值大于0的那段代码,子进程执行返回值等于0的那段代码。这也就成了我们区分父子进程的关键。
1 #include<stdio.h>
2 #include<string.h>
3 #include<sys/types.h>
4 #include<unistd.h>
5 #include<sys/stat.h>
6
7 int main()
8 {
9 printf("before fork [%d] [%d]\n",getpid(),getppid());
10 pid_t pid=fork();
11 if(pid<0)
12 {
13 perror("fork error\n");
14 return -1;
15 }
16 else if(pid==0)//子进程
17 {
18 printf("this is a child peocess [%d] [%d]\n",getpid(),getppid());
19 }
20 else if(pid>0)//父进程
21 {
22 printf("this is a father process [%d] [%d]\n",getpid(),getppid());
23 //sleep(1);
24 }
25 printf("after fork [%d] [%d]\n",getpid(),getppid());
26 return 0;
27 }
以上就是一段测试代码,我们在创建子进程之前先输出一下当前进程的进程id和其父进程的进程id,其实就是pid。我们通过getpid()和getppid()两个函数分别来获取当前的pid和其父进程的pid,注意这个在后面会常用。父进程执行pid大于0的逻辑,子进程执行pid小于0的逻辑,父子进程的执行是相互独立的,那么在程序的最后执行的那段代码和在程序开头执行的代码会被执行几次呢?
观察运行结果发现,只有after这行代码逻辑是被执行了两次的 ,是因为父子进程执行完else if里面的语句后最后都会走到这个共同的代码逻辑这,开头的那段子进程就不能执行是因为子进程在调用fork函数的时候创建的。可以看到父进程的pid=2327,子进程的pid=2328,但是观察发现这里的子进程的ppid不等于父进程的pid这里是什么原因呢?这里涉及到父子进程谁先执行的问题,这里就是父进程先执行完导致子进程变成了孤儿进程(ppid=1),后面会细说这个问题,这里只需要知道父子进程的执行顺序是随机的,谁抢到时间片谁就先执行。上面的代码中注释了 “sleep(1)”这行代码,这行代码就是休眠一秒钟的意思,取消注释,运行一下:
观察结果发现,刚刚让在父进程的代码逻辑中休眠了一秒钟,父进程就后执行完,只要就不会产生孤儿进程了。在实际的开发中,肯定有多个进程,了解了fork函数创建进程之后,我们来了解一下如何循环创建多个进程吧
2.循环创建多个进程
对于循环创建多个进程,是不是用一个简单的循环逻辑就能解决问题呢?有没有其他的注意事项呢?当然有,下面先来看看错误的循环创建子进程的方式:
1 #include<stdio.h>
2 #include<string.h>
3 #include<sys/types.h>
4 #include<unistd.h>
5 #include<sys/stat.h>
6
7 int main()
8 {
9 int i=0;
10 for(i=0;i<3;i++)
11 {
12 pid_t pid=fork();
13 if(pid<0)
14 {
15 perror("fork error\n");
16 return -1;
17 }
18 else if(pid>0)//父进程
19 {
20 printf("father process pid=%d ppid=%d\n",getpid(),getppid());
21 }
22 else if(pid==0)
23 {
24 printf("child process pid=%d ppid=%d\n",getpid(),getppid());
25 }
26 }
27 sleep(1);
28 return 0;
29 }
上面的代码就是简单的在循环内创建子进程,不考虑其他条件,结果如下:
我们发现一共循环创建了七个子进程,观察发现,这里有子进程继续创建子进程的现象,很显然,这不符合我们的预期,我们只想循环创建三个子进程,下面先来分析一下这七个子进程是如何创建的:
当i=0时,此时只有父进程创建一个子进程,当i=1时,这里子进程也在创建子进程,加上最初的父进程也在创建子进程,这样往后推,上面创建的七个进程就合理了,通过这里我们就可以知道,问题的根本是子进程在创建多余的子进程,我们只需要父进程创建的那三个子进程,那么要解决这个问题我们该如何修改代码呢?其实很简单,每次执行到子进程的代码逻辑的时候,我们直接让其跳出循环,这样就可以避免子进程继续创建子进程了。知道了这个之后,我们顺便判断一下这是创建的第几个进程,这些都是放在for循环外面,每当跳出循环,就会判断当前进程是第几个进程,就得到了我们想要的结果:
1 #include<stdio.h>
2 #include<string.h>
3 #include<sys/types.h>
4 #include<sys/stat.h>
5 #include<unistd.h>
6
7 int main()
8 {
9 int i=0;
10 for(i=0;i<3;i++)
11 {
12 pid_t pid=fork();
13 if(pid<0)
14 {
15 perror("fork errror\n");
16 return -1;
17 }
18 else if(pid==0)//子进程
19 {
20 printf("child process pid=[%d],ppid=[%d]\n",getpid(),getppid());
21 break;//当检查到是子进程时候就会跳出,子进程就不会循环创建孙子进程
22 }
23 else if(pid>0)//父进程
24 {
25 printf("father process pid=[%d],ppid=[%d]\n",getpid(),getppid());
26 }
27
28 }
29 if(i==0)//第一个子进程
30 {
31 printf("child 1 :pid = [%d],ppid = [%d]\n",getpid(),getppid());
32 }
33 if(i==1)//第二个子进程
34 {
35 printf("child 2 :pid = [%d],ppid = [%d]\n",getpid(),getppid());
36 }
37 if(i==2)//第三个子进程
38 {
39 printf("child 3 :pid = [%d],ppid = [%d]\n",getpid(),getppid());
40 }
41 if(i==3)//父进程
42 {
43 printf("father :pid = [%d],ppid = [%d]\n",getpid(),getppid());
44 }
45 return 0;
46 }
通过下面的运行结果,我们观察到我们这里正确的创建了三个子进程,pid相对应,这里的执行顺序还是不同的,还是上面说的时间片问题。这中循环创建子进程的方式需要牢记!
3. 验证父子进程能否共享全局变量问题:
定义一个全局变量,在父进程中对全局变量进行修改,如果在子进程中读到了这个修改后的变量,那么则说明父子间是可以共享全局变量的,反之不能共享。
1 #include<stdio.h>
2 #include<string.h>
3 #include<sys/types.h>
4 #include<unistd.h>
5 #include<sys/stat.h>
6 int global=100;//定义一个全局变量
7 //然后在父进程中修改其值,看看子进程能否访问到这个改变
8 int main()
9 {
10 pid_t pid=fork();
11 if(pid<0)
12 {
13 perror("fork error\n");
14 return -1;
15 }
16 else if(pid==0)//子进程
17 {
18 sleep(1);//为了避免子进程先执行
19 printf("global = %d\n",global);
20 printf("address:%p\n",&global);
21 printf("this is a child peocess [%d] [%d]\n",getpid(),getppid());
22 }
23 else if(pid>0)
24 {
25 printf("this is a father process [%d] [%d]\n",getpid(),getppid());
26 global=200;
27 printf("address:%p\n",&global);
28 //sleep(1);
29 }
30 return 0;
31 }
定义一个全局变量global=100,在父进程中将全局变量的值修改为200,如果在子进程中读到的数据也是200,那么就说明父子进程可以共享全局变量,反之则不行。
结果这个全局变量的值依旧是100,没有被修改,说明父子进程不能共享全局变量,在底层中其实是父进程把物理内存的值进行拷贝了一份,然后对这份拷贝的值进行修改最后返回这个拷贝的值,物理内存中实际的值并不会被修改,而子进程中读到的数据依旧是物理内存中没有被修改的值。(大概就是下面这个意思)可以理解成写时复制,读时共享。
如果有父子进程只是对全局变量进行读操作,则这个全局变量只有一份,如果父子进程是对这个全局变量进行修改,那么这个全局就会生成一个副本,然后再对这个副本进行操作,最后再映射回相应的父子进程。
4.exec函数族
父进程创建了子进程之后,就可以通过这个函数再进程中拉起一个命令或者是一个函数,比如说可以在子进程中执行 “ls” 命令,在父进程中运行一个可执行文件。这个就是exec函数族可以做到的事情。
通过man 3 exec 打开查询手册,我们只使用最上面的两个函数,也就是execl 和execlp,通过参数我们发现,execl的第一个参数是path也就是路径,就是要传入你要执行的命令的路径,查询命令的路径我们可以通过 which +命令 来实现 :
这里返回的就是命令ls的路径,而execlp函数就不需要路径了,第一个参数直接传入要执行的命令名称即可,如果是程序,则传入程序对应的路径即可,对于第二个参数就是一个占位参数,通常我们都传入要执行的命令名称,如果是要拉起一个可执行程序,则直接传入文件名即可,再往后后的参数就是执行命令或者运行应用程序所需要的参数了,最后以NULL结尾。还以一个值得注意的是exec函数族的返回值,如果这个函数执行成功了,那么他们将不会执行后面的代码,如果执行失败了才会继续往后执行。
execl("/usr/bin/ls","ls","-l",NULL);
execlp("ls","ls","-l",NULL);
先编写一个简单的可执行程序test.c,在子进程中利用exec函数启动它。
1 #include<stdio.h>
2
3 int main(int argc,char *argv[])
4 {
5 int i=0;
6 for(i=0;i<argc;i++)
7 {
8 printf("argv[%d]=%s\n",i,argv[i]);
9 }
10 return 0;
11
12 }
在这个程序中,我们就直接输出我们传入的参数,下面是主程序,我们在子进程中拉起这个可执行程序使用下面这两种传入方式都是可以的:
execlp("./test","test","Hello","world","ni","hao",NULL);
execl("./test","test","Hello","world","haha","666",NULL);
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<string.h>
4 #include<sys/types.h>
5
6
7 int main()
8 {
9
10 pid_t pid=fork();
11 if(pid<0)
12 {
13 perror("fork error\n");
14 return -1;
15 }
16 else if(pid==0)
17 {
18 execlp("./test","test","Hello","world","ni","hao",NULL);
19 //execlp("ls","ls","-l",NULL);
20 //execl("./test","test","Hello","world","haha","666",NULL);
21 //execl("/usr/bin/ls","ls","-l",NULL);
22 perror("execl error\n");
23 }
24 else if(pid>0)
25 {
26 printf("father process: pid=[%d]\n",getpid());
27 }
28
29 return 0;
30 }
只要程序成功拉起,后面的那个perror是不会执行的。
下面再测试一下ls命令。解析一下exec函数的底层,其实它就是就是替换了相应进程的代码区的相应代码来达到拉起一个可执行程序的效果。
注意:子进程的pid和地址空间都没有变化。
5.孤儿进程和僵尸进程
前面已经遇到了孤儿进程,就是父进程先执行完,子进程后后执行完,这时候子进程就是一个孤儿进程,在父进程执行完之后子进程会被inti进程领养,init进程是pid=1的进程,这就很好的解释了前面的很多进程的父进程pid为什么会变成1了。在Linux系统下面必须保证每个子进程都有一个父进程,来回收其资源。
既然有父进程先执行完就一定有子进程先执行完的情况,子进程先执行完,父进程没有完成对子进程的回收,因此子进程就成为了一个僵尸进程,注意僵尸进程是一个已经执行完的进程,不能再使用kill命令将其杀死,但是可以通过kill命令杀死它的父进程,让这个子进程被init进程领养,最后完成子进程的回收。我们可以通过ps -ef 命令观察僵尸进程
1 #include<stdio.h>
2 #include<string.h>
3 #include<unistd.h>
4 #include<sys/types.h>
5 #include<sys/stat.h>
6
7 int main()
8 {
9 pid_t pid=fork();
10 if(pid<0)
11 {
12 perror("fork error\n");
13 return -1;
14 }
15 else if(pid==0)
16 {
17 printf("child process pid=[%d],ppid=[%d]\n",getpid(),getppid());
18 }
19 else if(pid>0)
20 {
21 sleep(100);
22 printf("father process pid=[%d],ppid=[%d]\n",getpid(),getppid());
23 }
24 return 0;
25 }
编写一个能产生僵尸进程的程序,也就是让子进程先退出,让父进程sleep(100),这样子进程一定会先退出,在通过ps -ef 命令观察:
当后面带这个字符串的就是僵尸进程。进程的回收放到下一个大问题里面。
二.进程的回收
1.kill命令和ps命令
通过上面我们已经了解到ps命令就是用来查看当前进程的详细信息,kill命令可以用来杀死进程,在这里我们最常使用的是ps -ef命令,当然ps 后面可以有很多种参数组合,可以通过man ps命令查看。kill命令就是用来杀死进程的,可以通过kill -l 查看很多信号signal。
我们在这里的用法就是kill -9 +进程pid 来杀死某个进程 。
我们手动设置一个休眠,然后先用ps命令查看其pid,再用kill命令将其手动杀死
//ps和kill
ps -ef //查看所有进程信息
kill -9 pid //杀死某个进程
2.使用kill命令解决僵尸进程问题
上面说到了僵尸进程是因为子进程先执行完而它的资源没有被回收,这时候我们就可以杀死它的父进程,让这个子进程被inti进程(就是1号进程)领养,这样就可以完成对进程的回收。
上面的这种方式是在进程外面通过kill命令回收的子进程,当然有更好的办法。
3.wait/waitpid函数回收子进程
wait/waitpid函数都是在父进程中调用,完成对子进程的回收,阻塞并等待子进程退出,回收子进程的资源,获取子进程的退出状态(是正常退出还是异常退出)。
pid_t wait(int *status);
pid_t waitpid(int fd,int *statud,int )
根据返回的status和wait函数提供的宏就可以获取进程的退出状态,最常用的只有这俩,第一个WIFEXITED(status)判断进程是否是正常退出,如果是正常退出,就返回最后进程执行完要返回的结果WEXITSTATUS(status)。第二个WIFSIGNALED(status)是用来判断进程是否是被信号给终止,如果是,则返回相应的信号值(就是kill -l 查看到的那些)WTERMSIG(status)。
对于wait的返回值,如果回收成功,就返回进程的pid,如果没有进程需要回收,就返回-1。注意:调用一次wait函数只能回收一个子进程,因此如果有多个子进程需要回收就需要循环回收子进程。如果不需要返回进程的退出状态,wait的这个参数就可以直接传NULL。
1 #include<stdio.h>
2 #include<sys/wait.h>
3 #include<sys/types.h>
4 #include<unistd.h>
5 #include<string.h>
6
7
8 int main()
9 {
10 int status=0;
11 pid_t pid=fork();
12 if(pid<0)
13 {
14 perror("fork error");
15 return -1;
16 }
17 else if(pid==0)//子进程
18 {
19 sleep(50);
20 printf("child process pid=[%d],ppid=[%d]\n",getpid(),getppid());
21 return 5;//子进程正常结束,这个返回值会被获取
22 }
23 else if(pid>0)//父进程
24 {
25
26 printf("father process pid=[%d],ppid=[%d]\n",getpid(),getppid());
27 //pid_t wpid=wait(NULL);//如果不关心子进程退出的状态,就直接传入NULL
28 pid_t wpid=wait(&status);
29 if(WIFEXITED(status))//判断子进程是否正常运行结束
30 {
31 printf("child normal exit status:%d\n",WEXITSTATUS(status));//用来捕获子进程正常运行结束返回的值
32 }
33 else if(WIFSIGNALED(status))//判断子进程是否是被一个信号给杀死
34 {
35 printf("child killed by signal status:%d\n",WTERMSIG(status));//捕获杀死进程的那个信号
36 }
37 printf("wpid=[%d]\n",wpid);
38
39 }
40 return 0;
41 }
这里在子进程中设置一个休眠,执行程序时候会发现程序会阻塞在那一直等到子进程执行完才退出。
这是正常退出的情况,并且返回了子进程最后的值“5”,下面看一个被信号杀死的。
注意这里的第二个wait才是当前阻塞的进程,我们用的是15,最后就返回了15.下面来看看waitpid函数
waitpid的参数比wait的多,当然功能就就更强大,除了能够获取进程状态,对于waitpid的第一个参数,常用的就下面两种:
//当pid=-1时,表示回收任意子进程,当你有很多子进程需要回收的时候就可以使用这个参数
//当pid>0时,表示对特定的pid的子进程的回收,
waitpid的第三个参数options:WNOHANG 设置为非阻塞 0 设置为阻塞 (默认是阻塞的),设置为非阻塞父进程就可以区干其他的,一般都设置为非阻塞
对于其返回值,和wait函数是一样的,当返回值大于0时,返回回收的子进程的pid,等于-1就表示没有进程需要回收了,还有一种情况就是和WNOHANG一起使用时候,返回0表示没有子进程退出。waitpid可以完全替代wait。
第三个参数传入0的写法和wait是一样的,下面看看非阻塞的写法:
观察运行结果会发现这次的运行结果是不是少了点什么,这次的结果没有进程的退出状态,说明进程没有正常回收,它变成了一个僵尸进程。产后这个问题的原因是我们没有设置阻塞,父进程不会阻塞等待子进程,这个逻辑只会执行一次,子进程很可能没有被回收,因此要想子进被回收就需要一个循环反复执行这个逻辑,直到wpid的值为-1即表示所有的进程被全部回收,方可退出。
#include<stdio.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<unistd.h>
#include<string.h>
int main()
{
int status=0;
pid_t pid=fork();
if(pid<0)
{
perror("fork error");
return -1;
}
else if(pid==0)//子进程
{
sleep(5);
printf("child process pid=[%d],ppid=[%d]\n",getpid(),getppid());
return 5;
}
else if(pid>0)//父进程
{
printf("father process pid=[%d],ppid=[%d]\n",getpid(),getppid());
while(1)
{
pid_t wpid=waitpid(-1,&status,WNOHANG);//现在不发生阻塞
if(wpid>0)//返回回收的子进程pid
{
if(WIFEXITED(status))//判断子进程是否正常运行结束
{
printf("child normal exit status:%d\n",WEXITSTATUS(status));//用来捕获子进程正常运行结束返回的值
}
else if(WIFSIGNALED(status))//判断子进程是否是被一个信号给杀死
{
printf("child killed by signal status:%d\n",WTERMSIG(status));//捕获杀死进程的那个信号
}
printf("wpid=[%d]\n",wpid);
}
else if(wpid==0)//进程还活着
{
continue;
}
else if(wpid==-1)//没有子进程
{
printf("child process was recycled pid=[%d]\n",wpid);
break;
}
}
return 0;
}
}
上面就是最终版本的代码,当判断到子进程还活着的时候就直接继续循环。
观察结果,最后结束的时候的wpid=-1,说明是所有的进程回收完了父进程才退出的。
注意事项:调用一次wait/waitpid函数只能回收一个子进程,wait只能阻塞,waitpid可设置阻塞,也可设置非阻塞,注意使用这两个函数要使用头文件<sys/wait.h>。