Linux系统编程 -- 进程的创建、退出和回收(一)

进程是具有独立功能的程序在数据集合上的一次动态执行。通过fork创建子进程,子进程继承父进程的内容但拥有独立的地址空间。子进程结束需由父进程回收,否则可能成为僵尸进程。孤儿进程会被init收养,而exit和_exit的区别在于是否刷新缓冲区。waitpid用于灵活地等待子进程状态变化,避免僵尸进程。
摘要由CSDN通过智能技术生成

进程的概念

具有一定独立功能的程序在一个数据集合上的一次动态执行过程。它是动态的,创建、调度、执行和消亡都是由OS完成的。

进程的创建

通过man查询fork函数的定义与使用

Fork

NAME
       fork - create a child process

SYNOPSIS
       #include <sys/types.h>
       #include <unistd.h>

       pid_t fork(void);

DESCRIPTION
       fork()  creates  a new process by duplicating the calling process.  The new process is referred to as the child process.  The calling process is referred to as the parent
       process.

RETURN VALUE
       On success, the PID of the child process is returned in the parent, and 0 is returned in the child.  On failure, -1 is returned in the parent, no child  process  is  cre‐
       ated, and errno is set appropriately.
  • 头文件:
    #include <sys/types.h>
    #include <unistd.h>
  • 函数原型:
    pid_t fork(void);
  • 返回值:
    成功,父进程返回子进程的进程号(大于0的非零整数),子进程中返回0;所以可以通过返回值来判断分区父子进程。
    失败,返回-1.

进程的两个返回值

一般地,一个函数的返回值只有一个值,但是该函数的返回值却有两个。实际上关于这
个函数的返回值究竟有几个,可以换一种方式来理解,因为这个函数执行之后,系统中会存
在两个进程----父进程和子进程,在每个进程中都返回了一个值,所以给用户的感觉就是返
回了两个值。

  • 父进程:执行fork函数的进程,主要为了创建子进程。
  • 子进程:新的进程,用于开展新的任务。

进程的特点

  • 在Linux中,一个进程必须是另外一个进程的子进程,或者说一个进程必须有父进程,但是可以没有子进程。
  • 子进程继承了父进程的内容,包括父进程的代码、变量、PCB(process control block,进程控制块)。甚至当前的PC值。在父进程中,PC指向当前fork函数下一条指令地址,因此子进程也是从fork函数的下一条指令开始执行。父子进程的执行顺序是不确定的,可能子进程先执行,也可能父进程先执行,取决于当前系统的调度。
  • 父子进程有独立的地址空间、独立的代码空间,互不影响,就算父子进程有同名的全局变量,但是由于它们处在不同的地址空间,因此不能共享。
  • 子进程结束后,必须由它的父进程回收它的一切资源,如果父进程比子进程先退出,父进程没有通过wait回头子进程的资源,同时又没有被INIT进程接管,子进程就成了僵尸进程。
  • 如果父进程先结束,子进程会成为孤儿进程,它会被INIT进程收养,INIT进程是内核启动之后,首先被创建的进程。

程序验证

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

int main(int argc, char *argv[])
{
	pid_t pid;

	pid = fork(); // 创建子进程

	if (pid == 0) { // 子进程
		int i = 0;
		for (i = 0; i < 5; i++) {
			usleep(100);
			printf("this is child process i=%d\n", i);
		}
	}

	if (pid > 0) { // 父进程
	
		int i = 0;
		for (i = 0; i < 5; i++) {
			usleep(100);
			printf("this is parent process i=%d\n", i);
		}
	}

	while(1); //不让进程结束,以便我们查看进程的一些状态信息
	return 0;
}

解析说明

下面通过程序创建子进程,生成进程号为7977的进程id
在这里插入图片描述

通过ps --forest命令查看父子进程间的关系,子进程号为7978
在这里插入图片描述

通过命令cat /proc/7978/status查看进程信息,可以看到7978的父进程是7977
在这里插入图片描述

孤儿进程

当一个进程正在运行时,他的父进程忽然退出,此时该进程就是一个孤儿进程。作为一个进程,需要找到一个父进程,否则这种进程在退出之后没人回收他的进程描述符,空耗内存。此时该进程会找到一个父进程,如果自己所在的进程组没人收养,那就作为init进程的子进程。

基于上述程序的进一步实验,制造一个孤儿进程

通过kill -9 pid号,杀死父进程。此后子进程被init进程接管,也就是ppid变成了1。此时子进程仍然处于运行的状态。

在这里插入图片描述
在这里插入图片描述
init进程会为每一个子进程使用wait系统调用,确保不会产生僵尸进程。这里的wait系统调用指的是waitpid(),会传入一个要等待的进程pid,等待的指定进程,而不阻塞当前进程去等待。

僵尸进程

当进程exit()退出之后,他的父进程没有通过wait()系统调用回收他的进程描述符的信息,该进程会继续停留在系统的进程表中,占用内核资源,这样的进程就是僵尸进程。

接下来通过一个demo构造一个僵尸进程:

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

int main(int argc, char *argv[])
{
	pid_t pid;

	pid = fork(); // 创建子进程

	if (pid == 0) { // 子进程
		sleep(10);//父进程延时30s,让子进程先退出,父进程30秒给我们操作观察
		printf("this is child process id=%d\n", getpid());
	
	}
	
	if (pid > 0) { // 父进程
		sleep(30);
		printf("this is parent process id=%d\n", getpid());
	}

	return 0;
}

通俗一点,僵尸进程就是指子进程先于父进程挂掉,但是父进程并没有正确回收子进程的资源。

结果验证

通过top命令可以查看到系统中存在一个zombie线程,随着父进程的结束,僵尸进程最终被init进程所回收。
在这里插入图片描述
在这里插入图片描述

孤儿进程和僵尸进程的危害

  • 孤儿进程会由INIT进程收养作为子进程,所以不会有什么危害;
  • 如果父进程不调用wait / waitpid的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统的进程表容量是有限的,所能使用的进程号也是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程。

总结

子进程成为 defunct 直到父进程 wait(),除非父进程忽略了 SIGCLD 。更进一步,父进程没有 wait() 就消亡的子进程(活动的或者 defunct)成为 init 的子进程,init 着手处理它们。

结束进程

exit()和_exit()都用于正常终止一个函数。
通过查询手册来查看两个函数的具体细节。

NAME
       exit - cause normal process termination

SYNOPSIS
       #include <stdlib.h>

       void exit(int status);

DESCRIPTION
       The exit() function causes normal process termination and the value of status & 0377 is returned to the parent (see wait(2)).
RETURN VALUE
       The exit() function does not return.

  • 函数功能:
    使用 exit 函数来结束一个进程
  • 头文件:
    #include <stdlib.h>
  • 函数原型:
    void exit (int status);
NAME
       _exit, _Exit - terminate the calling process

SYNOPSIS
       #include <unistd.h>

       void _exit(int status);

       #include <stdlib.h>

       void _Exit(int status);

DESCRIPTION
       The function _exit() terminates the calling process "immediately".  Any open file descriptors belonging to the process are closed.  Any children of the process are inher‐
       ited by init(1) (or by the nearest "subreaper" process as defined through the use of the prctl(2) PR_SET_CHILD_SUBREAPER operation).   The  process's  parent  is  sent  a
       SIGCHLD signal.

       The value status & 0377 is returned to the parent process as the process's exit status, and can be collected using one of the wait(2) family of calls.

       The function _Exit() is equivalent to _exit().

RETURN VALUE
       These functions do not return.
  • 函数功能:
    使用_exit 函数来结束一个进程
  • 头文件:
    #include <unistd.h>
  • 函数原型:
    void _exit(int status);

先说结论:

  • 两个函数的区别是:exit 结束进程时会刷新缓冲区,_exit 不会;
  • 这两个退出函数和 return 函数又有什么区别呢?exit 和_exit 函数是返回给操作系统的,
    return 函数是当前函数返回,返回到调用它的函数中,如果正好是在 main 函数中,return 函
    数也返回给了操作系统,这个时候 return 和 exit、_exit 起到了类似的作用。

程序验证

使用exit退出程序:

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

int main(int argc, char *argv[])
{
	printf("hello world\n");
	printf("will exit");
	exit(0); //使用 exit 退出
}

使用_exit退出程序:

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

int main(int argc, char *argv[])
{
	printf("hello world\n");
	printf("will exit");
	_exit(0); //使用_exit 退出
}

使用exit退出程序的输出结果:
在这里插入图片描述
使用_exit退出程序的输出结果:
在这里插入图片描述

结果分析

在两个程序中,同样是上面一行比下面一行的打印语句多了一个“\n”,它会强制将待打印的字符刷新到缓冲区,为了对比 exit 和_exit 的区别,在下面一行will exit中就没有加上“\n”,按照上面两个退出函数的区别,exit程序会同时打印“hello world”和“will exit”,_exit 程序只会打印“hello world”,不会打印“will exit”。

回收进程

当我们只fork()一次后,存在父进程和子进程。这时有两种方法来避免产生僵尸进程:

  • 父进程调用waitpid()等函数来接收子进程退出状态。
  • 父进程先结束,子进程则自动托管到init进程(pid = 1)。

第二种在前面已经介绍过了,现在来看第一种情况:
第一种情况使用wait/waitpid回收。
子进程先于父进程结束,当父进程回收之前,子进程将处于僵尸状态。子进程结束前,父进程阻塞在wait调用。

还是老规矩先通过man查看手册的说明。

NAME
       wait, waitpid, waitid - wait for process to change state

SYNOPSIS
       #include <sys/types.h>
       #include <sys/wait.h>

       pid_t wait(int *wstatus);

       pid_t waitpid(pid_t pid, int *wstatus, int options);

       int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
                       /* This is the glibc and POSIX interface; see
                          NOTES for information on the raw system call. */

DESCRIPTION
       All  of  these system calls are used to wait for state changes in a child of the calling process, and obtain information about the child whose state has changed.  A state
       change is considered to be: the child terminated; the child was stopped by a signal; or the child was resumed by a signal.  In the case of a terminated child,  performing
       a  wait allows the system to release the resources associated with the child; if a wait is not performed, then the terminated child remains in a "zombie" state (see NOTES
       below).

   wait() and waitpid()
       The wait() system call suspends execution of the calling process until one of its children terminates.  The call wait(&wstatus) is equivalent to:

           waitpid(-1, &wstatus, 0);

       The waitpid() system call suspends execution of the calling process until a child specified by pid argument has changed state.  By default, waitpid() waits only for  ter‐
       minated children, but this behavior is modifiable via the options argument, as described below.

       The value of pid can be:

       < -1   meaning wait for any child process whose process group ID is equal to the absolute value of pid.

       -1     meaning wait for any child process.

       0      meaning wait for any child process whose process group ID is equal to that of the calling process.

       > 0    meaning wait for the child whose process ID is equal to the value of pid.


RETURN VALUE
       wait(): on success, returns the process ID of the terminated child; on error, -1 is returned.

       waitpid(): on success, returns the process ID of the child whose state has changed; if WNOHANG was specified and one or more child(ren) specified by pid exist,  but  have
       not yet changed state, then 0 is returned.  On error, -1 is returned.

       waitid(): returns 0 on success or if WNOHANG was specified and no child(ren) specified by id has yet changed state; on error, -1 is returned.

       Each of these calls sets errno to an appropriate value in the case of an error.

程序验证

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

int main(int argc, char *argv[])
{
    pid_t pid;
    if((pid=fork())<0){//error
		printf("fork error!");
		return -1;
    }
    else if(pid == 0){//子进程
		printf("I’m child process,I will wait 2 seconds\r\n");
		sleep(2);
    }
    else{//父进程
		int stat;
		pid_t pid=wait(&stat);//阻塞回收子线程的资源
		printf("child terminate pid = %d\r\n",pid);
    }
    return 0;
}

在这里插入图片描述

wait函数调用说明

  • 如果其他所有子进程还处于运行状态,则父进程一直阻塞。
  • 如果一个子进程已经终止,正等待父进程取其终止状态,则取得该子进程的终止状态立即返回。
  • 如果它没有任何子进程,则立即出错返回。

waitpid函数

  • 函数功能
    waitpid函数作用和wait函数基本一样,waitpid函数可以指定进程pid进行等待,可以不阻塞父进程,比wait()更加灵活。

  • 头文件
    #include <sys/types.h>
    #include <sys/wait.h>

  • 函数原型
    pid_t waitpid(pid_t pid, int *wstatus, int options);

  • 函数参数

    • pid
      pid > 0:等待指定id的子进程

      pid = 0:等待调用waitpid的父进程进程之下的所有子进程

      pid = -1:等待任何子进程(与wait函数一样)

      pid < -1:回收指定进程组内的任意子进程

    • wstatus——参照上面的wait函数

    • options(常用的两个)

      • WNOHANG :如果没有子进程退出,立即返回,不会阻塞进程;如果结束了,则返回该子进程的进程号

      • WUNTRACED :如果子进程进入暂停状态,则马上返回

      • 不用可以设置为0

  • 返回值:
    成功:返回状态已更改的子进程的进程ID
    如果指定了WNOHANG,并且pid指定的一个或多个子进程存在,但尚未更改状态,则返回0。
    错误:返回-1

程序验证

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

int main(int argc, char *argv[])
{
    pid_t pid;
    if((pid=fork())<0){//error
		printf("fork error!");
		return -1;
    }
    else if(pid == 0){//子进程
		printf("I’m child process,I will wait 2 seconds\r\n");
		printf("this is child process id=%d\n", getpid());
		sleep(2);
    }
    else{//父进程
		int stat;
		pid_t pid=waitpid(-1, &stat, WNOHANG);//没有子进程退出则立即返回0
		//pid_t pid=waitpid(-1, &stat, 0);
		printf("this is parent process id=%d\n", getpid());
		printf("child terminate pid = %d\r\n",pid);
    }
    return 0;
}

这里调用的参数是WNOHANG,同时pid = -1,也就是等待任何子进程,和wait函数调用一样。因为是WNOHANG,所以没有子进程退出,立即返回,不会阻塞进程。返回值为0
在这里插入图片描述
后面再将WNOHANG参数改为 0,也就是上面被注释掉的一段,读者可以自行取消注释试着去运行。
得到的另一个输出结果。返回值为子进程的pid
在这里插入图片描述

结论说明

上述通过不同的变量在waitpid函数中的参数的传递,实现了不同的效果。当pid_t pid=-1时,此时waitpid函数和wait作用一模一样,都是等待任意子进程的退出。Option = WNOHANG时,waitpid函数不阻塞,会立即返回0.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值