博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Linux - 进程
阅读量:6261 次
发布时间:2019-06-22

本文共 13521 字,大约阅读时间需要 45 分钟。

复制进程映像

【说明:利用fork复制的新进程,并未全部都复制了父进程的东东,这个在结尾时还会说明,先有个了解】

关于fork进程,可以用下面这种的通俗方式来理解:

首先我们先来理解一个概念,这个之后在最后还会进行总结的:

fork新创建的进程,会复制父进程的所有信息(代码段+数据段+堆栈段+PCB),但是“所有”并非绝对,还是有少部分信息是不一样的,另外还可以理解,每一个进程都有自己独立的4GB的地址空间(对于32位系统来说)

子进程与父进程的区别在于: 1、父进程设置的锁,子进程不继承 对于排它锁,如果说子进程会共享父进程的锁的话,那就有矛盾了。

2、各自的进程ID和父进程ID不同

3、子进程的未决告警被清除【了解】

4、子进程的未决信号集设置为空集【了解】

fork系统调用

【说明:关于 fork函数,它有一个特征:调用一次,返回两次,就如上面返回值的说明】

如上图所说:fork()出来的子进程成功,对于子进程来说则返回0;而对于父进程来说返回子进程ID,这是有原因的,对于进程中的PCB保存了pid和ppid,所以对于子进程来说,有办法知道pid和ppid,而对于父进程来说,如果不返回子进程的id,则就无法知道新创建的进程的号码,因为PCB中并不会保存子进程的ID列表,这样就会让PCB膨胀,所以这也是有原因的。

#include 
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define ERR_EXIT(m) \ do \ { \ perror(m); \ exit(EXIT_FAILURE); \ } while(0)int main(int argc, char *argv[]){ printf("before fork pid = %d\n", getpid());//打印当前进程ID,也就是原始父进程 pid_t pid; pid = fork();//产生一个新的进程,注意:它里会有两个进程执行 if (pid == -1) ERR_EXIT("fork error"); if (pid > 0) {//证明是父进程 printf("this is parent pid=%d childpid=%d\n", getpid(), pid);//getpid()为当前父进程的ID,而pid则为新创建的进程id,也就是子进程 } else if (pid == 0) {//证明是子进程 printf("this is child pid=%d parentpid=%d\n", getpid(), getppid());//getpid()为当前子进程的ID,getppid()为当前子进程所属父进程的ID } return 0;}复制代码

编译运行:

原因是由于当执行父进程之后,它就退出了,这时子进程执行时,这时子进程的父进程就变为init进程,所以就变成了1,如果我们让父进程输出延时一下,保证子进程执行时父进程没退出,就如我们的预期了:

这次再看效果:

对于fork函数,可能有一点比较难以理解,为啥它一次调用会有两次返回呢?这里再来用文字来解释一下:fork成功意味着创建了一个进程副本,意味着也就有两个进程了,两个进程都要执行各自相应的动作,所以两个进程都得要返回,实际上在内核中,是在各自的进程地址空间中返回的:

fork 系统调用注意点

关于上面的第二个注意点,,如果父进程退出了,子进程还没有退出,我们将子进程称为孤儿进程,这时会将子进程托孤给init进程。 关于第三点,其中提到了“僵尸进程”,用程序来看下现象:

这时,查看一下当前的进程状态:

僵尸状态,我们尽量得避免它,避免它的方法之一,可以采用一个系统调用----signal(信号,关于它,之后会详述,这里只是先了解一下):

这时,编译运行,再看效果:

下面再来理解一来系统是如何实现fork()的.

写时复制copy on write

实际系统实现时,并未真正把所有的数据(代码段+数据段+堆栈段+PCB)都复制一份,只是为了方便理解,我们可以认为是数据(代码段+数据段+堆栈段+PCB)都复制了一份,实际上代码段是只读的,是可以被共享的,每个进程只要保存一个指向这个资源的指针既可,这可以加快进程的创建速度,大大提高了效率

而对于需要修改的进程才会复制那份资源,对于linux而言,它是基于页的的方式进行复制的,并没将所有数据都进行复制,只是复制需要页,其它页是不会复制的,所以我们得正确理解“每个进程有自己独立的4GB(对于32位系统来说)的地址空间”,实际上不被修改的数据是共享的,对于这个理论,大致了解下,也是为了加深对fork()函数的理解。

fork之后父子进程共享文件

父进程打开两个文件:

这时通过fork()函数新建了一个子进程,这时它共享父进程的文件,其结构如下:

#include 
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define ERR_EXIT(m) \ do \ { \ perror(m); \ exit(EXIT_FAILURE); \ } while(0)int main(int argc, char *argv[]){ signal(SIGCHLD, SIG_IGN); printf("before fork pid = %d\n", getpid()); int fd; fd = open("test.txt", O_WRONLY);//父进程打开一个文件 if (fd == -1) ERR_EXIT("open error"); pid_t pid; pid = fork(); if (pid == -1) ERR_EXIT("fork error"); if (pid > 0) { printf("this is parent pid=%d childpid=%d\n", getpid(), pid); write(fd, "parent", 6);//父进程往文件中写入内容 sleep(1);//睡眠是为了避免孤儿进程的产生,保证子进程执行的时候,父进程没有退出 } else if (pid == 0) { printf("this is child pid=%d parentpid=%d\n", getpid(), getppid()); write(fd, "child", 5);//子进程往文件中写入内容 } return 0;}复制代码

先创建一个"test.txt",里面是空内容:

也就是可以说明,子进程是共享父进程打开的文件表项的。

注意:有时候可能会test.txt的内容输出如下:

上面这种输出并没有按照我们的预想,可能的原因是跟两个进程的静态问题造成的,这个问题比较复杂,可以这样理解:也就是还没等子进程执行,父进程就已经结束了,这时子进程的文件偏移量会从0开始,所以之前父进程写入了parent,由于它退出来,子进程从0的位置开始写,所以最终输出就如上图所示了,为了保证如我们预期来输出,可以将睡眠时间加长上些,保证子进程执行时,父进程没有退出,如下:

fork【几乎都用它】与vfork【几乎很少用它】

上节中我们知道,fork的拷贝机制是copy on write,图中所说的exec函数,是指加载一个新的程序来执行,这个下面会有介绍到,先大概了解下,如果说没有copy on write机制的话,那父子进程都有自己独立的进程空间,也就是子进程需要完完全全的拷贝父进程的地址空间,而如果子进程中执行exec的话,等于它被一个新的程序替换掉了,它根本不需要拷贝父进程的数据,所以就会造成地址空间的浪费,这时才引入了vfork,也就是vfork之后子进程在执行exec之前,是不会拷贝父进程的地址空间的,不管子进程有没有改写数据,它是一个历史问题(说得有点抽象,下面会以具体代码来一一阐述的)。

#include 
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define ERR_EXIT(m) \ do \ { \ perror(m); \ exit(EXIT_FAILURE); \ } while(0)int gval = 100;int main(int argc, char *argv[]){ signal(SIGCHLD, SIG_IGN); printf("before fork pid = %d\n", getpid()); pid_t pid; pid = fork();//这里是用的copy on write机制 if (pid == -1) ERR_EXIT("fork error"); if (pid > 0) { sleep(1);//它的目的是为了让子进程先对gval进行++操作,以便观察父进程是否会受影响 printf("this is parent pid=%d childpid=%d gval=%d\n", getpid(), pid, gval); sleep(3); } else if (pid == 0) { gval++;//子进程来改写数据 printf("this is child pid=%d parentpid=%d gval=%d\n", getpid(), getppid(), gval); } return 0;}复制代码

编译运行:

其原因也就是由于fork()是采用copy on write的机制,下面用图来解析一下上面的结果:

下面将其改为vfork来实现:

编译运行:

这个输出结果,可以表明,vfork产生子进程,当改写数据时也不会拷贝父进程的空间的,父子是共享一份空间,所以当子进程改写的数据会反映到父进程上。

另外这段程序中出现了一个“段错误”,这是因为:

这时,编译再运行,就不会有错误了:

另外执行exec函数也一样,关于它的使用,之后再来介绍。

提示:vfork是一个历史问题,了解一下既可,实际中很少用它!

在演示vfork时,提到“子进程必须立刻执行_exit”,那如果用exit(0)退呢?

编译运行:

我们通常会将return 0 与exit(0)划等号,但如果在vfork()中,还是划等号么?

编译运行:

那exit与_exit有啥区别呢?下面来探讨下,在探讨之前,先来回顾一下进程的五种终止方式:

下面以一个图来说明exit与_exit的区别:

区别一:exit是C库中的一个函数;而_exit是系统调用。

区别二:exit在调用内核之前,做了“调用终止处理程序、清除I/O缓冲”;而_exit是直接操作内核,不会做这两件事。

编译运行:

exit(0)相当于return 0;所以可想将上面return 0换为exit(0)也是一样的能在屏幕上打印出来,那如果换成_exit(0)呢?

这时编译运行:

这时就正常显示了:

另外对于exit来说,它会调用“终止处理程序”,所谓“终止处理程序”,就是指在程序结束的时候会调用的函数代码段,这些代码段,需要我们安装才可以,可以用如下函数:

其中传递的参数是函数指针。

编译运行:

如果换成是_exit()呢?

编译运行:

插一句:对于fork函数,有一个问题需进一步阐述一下,以便加深对它的理解:

#include 
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define ERR_EXIT(m) \ do \ { \ perror(m); \ exit(EXIT_FAILURE); \ } while(0)int main(int argc, char *argv[]){ signal(SIGCHLD, SIG_IGN); printf("before fork pid = %d\n", getpid()); pid_t pid; pid = fork(); if (pid == -1) ERR_EXIT("fork error"); if (pid > 0) { printf("this is parent pid=%d childpid=%d\n", getpid(), pid); sleep(3); } else if (pid == 0) { printf("this is child pid=%d parentpid=%d\n", getpid(), getppid()); } return 0;}复制代码

输出:

对于上面这段程序,就是上节中学习过的,但是有个问题值得思考一下,为啥fork()之后,不是从"before fork"从main的第一行起输出,而是从fork()之后的代码中去输出,这时因为fork()之后,拷贝了“代码段+数据段+堆栈段+PCB”,也就是两个进程的信息几乎都是一样,而由于堆栈段+PCB几乎是一样的,所以它会维护当前运行的信息,所以每个进程会从fork()之后的代码继续执行,这一点需要理解。

另外,再看一个跟fork()相关的程序:

#include 
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define ERR_EXIT(m) \ do \ { \ perror(m); \ exit(EXIT_FAILURE); \ } while(0)int main(int argc, char *argv[]){ fork(); fork(); fork(); printf("ok\n"); return 0;}复制代码

编译运行:

这是为什么呢?因为第一个fork()时,会产生两个进程,这时这两个进程都会执行它后面的代码,也就是第二个fork(),这时就有四个进程执行第二个fork()了,同样的,这时四个进程就会执行它下面的代码,也就是第三个fork(),这时就再产生四个进程,总共也就是八个进程了,这个比较不好理解,好好想一下!

最后,我们来说明一下execve函数,这个在上面介绍vfork()函数时,已经提到过了,它的作用是:替换进程映像,这时对它进行使用说明:

#include 
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define ERR_EXIT(m) \ do \ { \ perror(m); \ exit(EXIT_FAILURE); \ } while(0)int gval = 100;int main(int argc, char *argv[]){ signal(SIGCHLD, SIG_IGN); printf("before fork pid = %d\n", getpid()); pid_t pid; pid = vfork(); if (pid == -1) ERR_EXIT("fork error"); if (pid > 0) { printf("this is parent pid=%d childpid=%d gval=%d\n", getpid(), pid, gval); } else if (pid == 0) { char *const args[] = { "ps", NULL}; execve("/bin/ps", args, NULL);//将子进程完全替换成/bin/ps中的ps进程命令,所以这句话之后的代码就不会执行了,因为是完全被替换了 gval++; printf("this is child pid=%d parentpid=%d gval=%d\n", getpid(), getppid(), gval); } return 0;}复制代码

编译运行:

exec替换进程映象

对于fork()函数,它创建了一个新进程,新进程与原进程几乎是一样的,而对于shell命令,如:

对于shell命令,它本身就是一个进程,要想执行ls程序,则需去加载ls程序,这时shell命令进程则需fork()创建一个新进程,而我们知道新创建的进程与原进程几乎是一样的,也就意味着新的进程的代码还是跟shell程序本身是一样的,也就无法执行ls程序,所以,这时我们只有将新进程用ls程序替换,也就是用exec系列函数来替换,这也就是它的意义所在。

exec关联函数组

编译运行:

那如果被execlp函数替换后的进程ID是否会发生变化呢?为了说明这个问题,我们先编写一个打印进程ID的程序:

hello.c:

编译,会用execl替换我们编写的程序,来论证我们提出的问题:

再来用execl替换成我们写的hello程序:

这时运行:

如果将程序做一点小改动,如下:

这时编译运行:

这时为什么呢?这是因为execlp函数执行失败了,所以没有替换成功,可以打印一下错误信息:

编译运行:

这时因为:

其中linux的环境变量如下:

下面就具体对execlp系列的每个函数进行研究,先从整体上来看一下这些函数:

下面用代码来演示一下execlp与execvp这两个函数用法的差别:

编译运行:

换成不带l的函数,看下它的使用方式:

其运行结果跟上面一样,这就是带l的函数与不带l函数的使用区别。

下面来说明一下函数参数的意义:

下面,我们来研究一下下面两个函数的区别:

编译运行:

这是因为execl中的程序名需要带上全路径,而execlp不需要定全路径,会自动在环境变量中去搜寻,这就是带p与不带p的区别,于是我们看一下ls命令的路径:

于是,将这个路径替换一下:

再次编译运行:

所以,对于下面这两个函数也就明白啥区别了:

这里就不做实验了,对于exec系列的函数,最后还剩一个execle函数:

下面就以实际代码来解析下这个参数的含义:

hello.c还是之前的代码,再贴出来:

编译运行:

下面我们将hello.c来输出程序的环境变量,实际上有对应的shell命令能够输出,效果如下:

于是改装我们的hello.c:

而对于environ的数据结构是这样的:

这时,编译一下执行hello:

这时,我们再执行之前替换hello的函数,这时也会输出环境信息:

这时我们将execl函数,改为execle,并传递我们自己的环境信息:

编译运行:

至此,我们已经把exec系列相关的函数的区别,就已经全部学完了,可以好好体会下,对于这些函数,下面再来说明下:

execve我们可以看一下帮助:

最后,再来补充一个知识,在之前我们学过了fcntl函数,该函数功能很强大,其中还漏了一个没有学到,就是:

编译运行:

如果没有用fcntl设置,我们是能看到./hello程序的输出结果的,这也就是FD_CLOEXEC标志的作用了,它会对exec系列的函数产生影响,记住这点就可以了。

其实,打开一个文件时,也可以带上FD_CLOEXEC:

SIGCHLD

【说明:关于信号,很快就会有一个专题来仔细研究它,现在可以简单认为:它是一种异步通知事件】

【说明:如果父进程没有查询子进程的退出状态,子进程是没有办法真正完全退出的,这时子进程的状态就称为僵尸状态,该进程就叫僵尸进程】

wait

#include 
#include
#include
//提供wait函数声明#include
#include
#include
#include
#include
#include
#include
#define ERR_EXIT(m) \ do \ { \ perror(m); \ exit(EXIT_FAILURE); \ } while(0)int main(int argc, char *argv[]){ pid_t pid; pid = fork(); if (pid == -1) ERR_EXIT("fork error"); if (pid == 0) { sleep(3);//子进程休眠,是为了看到父进程会等待子进程退出 printf("this is child\n"); exit(100); } printf("this is parent\n"); int status; wait(&status);等待子进程退出 return 0;}复制代码

看一下编译运行效果,下面用动画来展现,以便能体现到wait的意义:

从图中可以感受到,父进程虽然是已经输出了,但是一直是等到子进程退出了才退出,这也就是wait会让父进程去查子进程的退出状态,从而避免了僵尸进程的出现。

编译运行:

对于这些状态信息,可以通过调用系统宏来查询,下面具体来介绍下:

编译运行:

下面我们可以用abort函数,来模拟子进程非法退出的情况:

这时再编译运行:

实际上,对于子进程非法退出的,还可以判断得再仔细一些,因为有好几种情况可以造成子进程非法退出,如上图所示,一是因为捕获信号而终止,二是被暂停了,具体用法如下:

编译运行:

【说明:上面的这些宏在sys/wait.h头文件里定义】

waitpid

对于上面刚学完的wait,它是等待随意的进程退出,因为一个父进程可以有多个子进程,如果只要有一个子进程退出,父进程的wait就会返回;

而waitpid则可以等待特定的进程退出,这是两者的区别,下面就来具体学习下这个函数的用法:

对于waitpid的pid参数的解释与其值有关:

实际上可以通过查看man帮助得到这个信息:

可以将我们之前的程序用waitpid替换一下,效果一样:

也同样,用它来改装我们之前的程序,对于父进程,实际上只有一个子进程,就可以直接传子进程的id既可,只等待这个子进程:

效果一样:

比如:waitpid(-100,&status,0)的意思就是,等待进程组ID=100里面的任一一个子进程。

最后,关于wait和waitpid,进行一个总结:

另外,对于僵进程,已经被提到过好几次了,最后再来总结一下:

僵进程

如何避免僵进程

system

实际上,它就等于用代码去执行我们在命令行中敲的那些shell命令,下面就以实际代码来认识这个函数的使用:

编译运行:

实际上,system()函数是调用"/bin/sh -c",如下:

对于这个函数的使用,其实没什么难的,但是这个函数很有代表性,我们可以通过它来综合运用我们所学习东西,这样其实还是挺有意义的,接下来,会自己实现一个跟system同样的功能,来达到理解system函数的实现原理:

#include 
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define ERR_EXIT(m) \ do \ { \ perror(m); \ exit(EXIT_FAILURE); \ } while(0)int my_system(const char *command);//自己实现有system函数声明int main(int argc, char *argv[]){ my_system("ls -l | wc -w");//这里改用自己实现在system return 0;}int my_system(const char *command){ pid_t pid; int status; if (command == NULL) return 1;  if ((pid = fork()) < 0) status = -1;//出现不能执行system调用的其他错误时返回-1  else if (pid == 0) {//子进程 execl("/bin/sh", "sh", "-c", command, NULL);//替换成sh进程 exit(127);//如果无法启动shell运行命令,system将返回127,因为如果成功替换了之后,是不会执行到这句来的 } else {//父进程会等到子进程执行完 while (waitpid(pid, &status, 0) < 0) { if (errno == EINTR)//如果是被信号打断的,则重新waitpid continue; status = -1; break; }     //这时就顺利执行完了 } return status;}复制代码

编译运行:

什么是守护进程

守护进程的创建步骤

在描述它之前,首先得先了解两个概念:进程组、会话期:

而它里面有bash shell进程组,里面只有bash进程:

而一个会话期,实际上就对应一个终端,当我们打开多个虚拟终端时,可以用tty来查看终端数:

而守护进程是跟控制终端无关的,并且是在后台执行的,如果想让我们在shell中启动的进程变成守护进程,则应该将它放到会话期当中:

那这时,我们需要一个创建新的会话期的函数,实际上是系统函数,它为setsid(),通过man来查看一下它的说明:

这就意味着,我们在创建一个新的会话期之前,需要准备一个进程,保证该进程不是一个进程组组长,那如何保证呢?由于我们运行的shell命令的父进程可能是进程组组长,所以需要让父进程退出,这样就可以保证fork出来的子进程不是进程组组长,从而可以创建一个新的会话期了,总结一下上面说的流程:

按照上面的步骤下面以具体代码来实现一个守护进程:

#include 
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define ERR_EXIT(m) \ do \ { \ perror(m); \ exit(EXIT_FAILURE); \ } while(0)int setup_daemon(void);int main(int argc, char *argv[]){ return 0;}int setup_daemon(void){ pid_t pid; pid = fork(); if (pid == -1) ERR_EXIT("fork error"); if (pid > 0)//将父进程退出,保证子进程不是进程组组长 exit(EXIT_SUCCESS); setsid();//如果走到这,代表是子进程,由于它不是一个进程组组长,所以可以创建一个新的会话期 return 0; }复制代码

当我们用setsid()创建一个新的会话期之后,会有一个什么样的影响呢,还是接着看它的说明介绍:

也就是如下图所示:

其实上面的程序就已经实现了一个守护进程,我们调用一下运行看下:

编译运行:

我们来查看下进程:

守护进程通常是在系统运行而运行的,通常将当前目录改为根目录,因为有可能守护进程是在某个shell提示符下运行的, 那么当前目录就是shell提示符所在的目的, 就拿我们创建的这个守护进程而言,它的当前目录为:

这样,系统管理员就无法umount这个目录,因为守护进程是学期在后期运行的,这个目录不应该作为它的环境,所以这就产生了创建守护进程的第四个步骤:

修改代码:

最后还有一个步骤:

【说明:/dev/null表示空设备,这里就是把日志记录到空设备里,就是不记录日志。】

这时再运行,如果我们往屏幕输出内容,这时是看不到内容的,因为已经将标准输出重定向了空设备:

daemon

实际上linux上已经有现成的方法可以创建一个守护进程了,如下:

在运行它之前,我们来看下现在应该有几个守护进程了:

先将其都杀掉,以便来观察调用系统的创建守护进程是否成功:

这时,再运行:

对于系统的这个函数,都是传递的0,如果传递1会怎样呢?

编译运行:

实际上,对于我们写的守护进程,也可以模拟成跟系统调用方式一样,修改程序如下:

#include 
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define ERR_EXIT(m) \ do \ { \ perror(m); \ exit(EXIT_FAILURE); \ } while(0)int setup_daemon(int nochdir, int noclose);//模拟系统创建守护进程的函数声明int main(int argc, char *argv[]){ setup_daemon(1, 1);//这时改用跟调用系统创建守护进程的自己实现的函数 printf("test ...\n"); for (;;) ; return 0;}int setup_daemon(int nochdir, int noclose){ pid_t pid; pid = fork(); if (pid == -1) ERR_EXIT("fork error"); if (pid > 0) exit(EXIT_SUCCESS); setsid(); if (nochdir == 0)//实现很简单,做下参数判断既可 chdir("/"); if (noclose == 0) { int i; for (i=0; i<3; ++i) close(i); open("/dev/null", O_RDWR); dup(0); dup(0); } return 0;}复制代码

编译运行:

【提示:在创建守护进程时,不重定向至空设备其实对于开发期间便于调试,如果等程序发布了之后,就得重定向了!】

转载于:https://juejin.im/post/5ce8aca3e51d45775f5169ea

你可能感兴趣的文章
EXCHANGE2003系列总结-7:OWA下修改密码
查看>>
Zabbix安装图解教程
查看>>
oracle数据类型
查看>>
MSSQL sum()计算expression转化为数据类型int时发生算术溢出错误解决
查看>>
oracle 11g rac 笔记(VMware 和esxi主机都可以使用)
查看>>
golang钉钉群机器人订阅自定义主题百度新闻
查看>>
Backend-as-a-Service (BaaS) for Efficient Software Development
查看>>
php的curl获取https加密协议请求返回json数据进行信息获取
查看>>
检查HP服务器硬盘状态脚本
查看>>
Java基础之函数
查看>>
NAT负载均衡_ftp
查看>>
kafka集群搭建
查看>>
Mongodb大数据语法大全
查看>>
Linux的简单SHELL
查看>>
bat清理日志文件
查看>>
python——“破解”私有属性
查看>>
httpclient请求域名自定义域名指向ip
查看>>
安装 MySQL报错 -bash: mysql: command not found
查看>>
RedHat6.4使用CentOS163yum源在线安装及更新软件
查看>>
BUG: soft lockup - CPU#0 stuck for 22s! [kworker/0:2:27076]
查看>>