进程的简要状态

在这里插入图片描述

系统进程

一般而言,每个进程都是由另一个我们称之为父进程的进程启动的,被父进程启动的进程叫做子进程。Linux系统启动时,它将运行一个名为init的进程,该进程是系统运行的第一个进程,它的进程号为1。你可以把init进程看作为操作系统的进程管理器,它是其他所有进程的祖先进程。我们将要看到的其他系统进程要么是由init进程启动的,要么是由被init进程启动的其他进程启动的。
在这里插入图片描述

fork函数

我们可以通过调用fork创建一个新进程。这个系统调用复制当前进程,在进程表中创建一个新的表项,新表项中的许多属性与当前进程是相同的。新进程几乎与原进程一模一样,执行的代码也完全相同,但新进程有自己的数据空间、环境和文件描述符。fork和exec函数结合在一起使用就是创建新进程所需要的一切了。
在这里插入图片描述
与普通函数不同,fork函数会返回两次。一般说来,创建两个完全相同的进程并没有太多的价值。大部分情况下,父子进程会执行不同的代码分支。fork函数的返回值就成了区分父子进程的关键。fork函数向子进程返回0,并将子进程的进程ID返给父进程。当然了,如果fork失败,该函数则返回-1,并设置errno。
在这里插入图片描述
在这里插入图片描述
如果fork失败,它将返回-1。失败通常是因为父进程所拥有的子进程数目超过了规定的限制(CHILD_MAX),此时errno将被设为EAGAIN。如果是因为进程表里没有足够的空间用于创建新的表单或虚拟内存不足,errno变量将被设为ENOMEM。
在这里插入图片描述

实验fork函数

#include<sys/types.h>
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>

int main()
{
        pid_t pid;
        char *message;
        int n;

        printf("fork program starting!\n");
        pid = fork();
        switch(pid)
        {
                case -1:
                        perror("fork failed");
                        exit(1);
                case 0:
                        message = "This is the child";
                        n = 5;
                        break;
                default:
						message = "This is the parent";
                        n = 3;
                        break;
        }
        for(; n > 0; n--)
        {
                puts(message);
                sleep(1);
        }
        exit(0);
}

这个程序以两个进程的形式在运行。子进程被创建并且输出消息5次。原进程(即父进程)只输出消息3次。父进程在子进程打印完它的全部消息之前就结束了,因此我们将看到在输出内容中混杂着一个shell提示符。
在这里插入图片描述
程序在调用fork时被分为两个独立的进程。程序通过fork调用返回的非零值确定父进程,并根据该值来设置消息的输出次数,两次消息的输出之间间隔一秒。

fork创建子进程

#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<math.h>

int main()
{
        pid_t child;

        if((child = fork()) == -1)
        {
                printf("Fork Error:%s\n", strerror(errno));
                exit(1);
        }
        else
        {
                if(child == 0)
                {
                        printf("I am the child:%d\n", getpid());
                        exit(0);
				}
                else
                {
                        printf("I am the father:%d\n", getpid());
                        return 0;
                }
        }
}

在这里插入图片描述
fork之后,对于父子进程,谁先获得CPU资源,而率先运行呢?

从内核2.6.32开始,在默认情况下,父进程将成为fork之后优先调度的对象。采取这种策略的原因是:fork之后,父进程在CPU中处于活跃的状态,并且其内存管理信息也被置于硬件内存管理单元的转译后备缓冲器(TLB),所以先调度父进程能提升性能。
在这里插入图片描述
该值默认是0,表示父进程优先获得调度。如果将该值改成1,那么子进程会优先获得调度。
在这里插入图片描述
POSIX标准和Linux都没有保证会优先调度父进程。因此在应用中,决不能对父子进程的执行顺序做任何的假设。如果确实需要某一特定执行的顺序,那么需要使用进程间同步的手段。

进程的创建之vfork()

在早期的实现中,fork没有实现写时拷贝机制,而是直接对父进程的数据段、堆和栈进行完全拷贝,效率十分低下。很多程序在fork一个子进程后,会紧接着执行exec家族函数,这更是一种浪费。

所以BSD引入了vfork。既然fork之后会执行exec函数,拷贝父进程的内存数据就变成了一种无意义的行为,所以引入的vfork压根就不会拷贝父进程的内存数据,而是直接共享。再后来Linux引入了写时拷贝的机制,其效率提高了很多,这样一来,vfork其实就可以退出历史舞台了。除了一些需要将性能优化到极致的场景,大部分情况下不需要再使用vfork函数了。

vfork会创建一个子进程,该子进程会共享父进程的内存数据,而且系统将保证子进程先于父进程获得调度。子进程也会共享父进程的地址空间,而父进程将被一直挂起,直到子进程退出或执行exec。

注意,vfork之后,子进程如果返回,则不要调用return,而应该使用_exit函数。如果使用return,就会出现诡异的错误

请看下面的示例代码:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>

int glob = 88;
int main()
{
        int var;
        var = 88;
        pid_t pid;

        if((pid = vfork()) < 0)
        {
                printf("vfork error");
                exit(-1);
        }
        else if(pid == 0)
        {
                var++;
                glob++;
                return 0;
        }
        printf("pid = %d, glob = %d, var = %d\n", getpid(), glob, var);
        return 0;
}

调用子进程,如果使用return返回,就意味着main函数返回了,因为栈是父子进程共享的,所以程序的函数栈发生了变化。main函数return之后,通常会调用exit系的函数,父进程收到子进程的exit之后,就会开始从vfork返回,但是这时整个main函数的栈都已经不复存在了,所以父进程压根无法执行。于是会返回一个诡异的栈地址,对于在某些内核版本中,进程会直接报栈错误然后退出,但是在某些内核版本中,有可能就会再次进出main,于是进入一个无限循环,直到vfork返回错误。笔者的Redhat版本就是后者。

一般来说,vfork创建的子进程会执行exec,执行完exec后应该调用_exit返回。注意是_exit而不是exit。因为exit会导致父进程stdio缓冲区的冲刷和关闭。
在这里插入图片描述
vfork深度分析看陈皓的VFORK 挂掉的一个问题

vfork创建子进程

#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<math.h>

int main()
{
        pid_t child;

        if((child = vfork()) == -1)
        {
                printf("Fork Error:%s\n", strerror(errno));
                exit(1);
        }
        else if(child == 0)
        {
                sleep(1);
                printf("I am the child:%d\n", getpid());
                exit(0);
        }
		 else
        {
                printf("I am the father:%d\n", getpid());
                exit(0);
        }
}

在这里插入图片描述

进程等待

在前面的fork示例程序中,父进程在子进程之前结束,由于子进程还在继续运行,所以得到的输出结果有点乱。我们可以通过在父进程中调用wait函数让父进程等待子进程的结束。
在这里插入图片描述
wait系统调用将暂停父进程直到它的子进程结束为止。这个调用返回子进程的PID,它通常是已经结束运行的子进程的PID。状态信息允许父进程了解子进程的退出状态,即子进程的main函数返回的值或子进程中exit函数的退出码。如果stat_loc不是空指针,状态信息将被写入它所指向的位置。

实验wait函数

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>

int main(int argc, char *argv[])
{
        printf("hello world(pid:%d)\n", getpid());
        
        pid_t child = fork();
        if(child == -1)
        {
                fprintf(stderr, "fork failed\n");
                exit(1);
        }
        else if(child == 0)
        {
                printf("hello, I am child(pid:%d)\n", getpid());
        }
        else
        {
                int wc = wait(NULL);
				printf("hello, I am parent of %d(wc:%d)(pid:%d)\n", child, wc, getpid());
        }
        return 0;
}

在这里插入图片描述

进程等待

#include<stdio.h>
#include<math.h>
#include<stdlib.h>
#include<sys/types.h>
#include<errno.h>
#include<unistd.h>
#include<sys/wait.h>

int main()
{
        pid_t child;

        if((child = fork()) == -1)
        {
                printf("Fork Error:%s\n", strerror(errno));
                exit(1);
        }
        else
        {
                if(child == 0)
                {
                        printf("the child process is running\n");
                        sleep(1);
						printf("I am the child:%d\n", getpid());
                		exit(0);
      			 }
       			else
     		   {
					wait(NULL);
	                printf("the father process is running\n");
	                printf("I am the father:%d\n", getpid());
	                return 0;
        		}
        }
}

在这里插入图片描述

如果喜欢我的文章,请记得三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持,下期更精彩!!!

Logo

技术共进,成长同行——讯飞AI开发者社区

更多推荐