在Unix及其衍生系统(如Linux)中,fork()函数是一个非常基础且重要的系统调用,它用于创建一个与调用进程(父进程)几乎完全相同的子进程。以下是关于fork()函数的一些关键点:
-
功能:
fork()的主要作用是创建一个新的进程,这个新进程是父进程的一个副本,包括父进程的代码、数据段、堆、栈以及打开的文件描述符等资源。然而,每个进程都有其独立的进程标识符(PID)、内存地址空间(尽管初始内容相同,但可后续修改而不影响对方)以及某些其他资源。 -
返回值:
fork()调用在父进程中返回新创建子进程的PID,在子进程中返回0。如果发生错误(如资源不足),则在两者中均返回-1。 -
并发模型:通过
fork()创建子进程是实现多进程并发模型的一种方式。子进程可以继续执行父进程的代码,也可以通过exec()系列函数替换自身为另一个程序,这在服务器程序中尤为常见,用于处理并发客户端请求。 -
资源共享与独立:虽然子进程继承了父进程的大多数资源,但一旦
fork()完成后,父子进程间对于可写资源(如内存、文件描述符)的修改将是独立的,除非使用特殊机制如共享内存进行通信。 -
孤儿进程与僵尸进程:如果父进程终止而子进程还在运行,则子进程成为孤儿进程,由init进程接管。当子进程先于父进程退出时,它会变成僵尸进程,直到父进程通过
wait()或waitpid()系统调用来回收其状态信息。 -
使用场景:除了用于实现并发服务器,
fork()还常用于需要并行处理任务、创建守护进程、执行不同程序或在新进程中运行相同程序的不同实例等情况。
总之,fork()是Unix/Linux系统编程中用于创建新进程的核心机制,对于理解操作系统中的进程管理和并发执行至关重要
为什么子进程先于父进程退出,会变成僵尸进程
当子进程先于父进程退出时,它会进入一种称为“终止状态”。在这个状态下,子进程的大部分资源会被操作系统回收,但是它的进程控制块(PCB,Process Control Block)中的一些信息,如进程ID、退出状态和资源使用统计等,仍需要保留,以便父进程能够查询这些信息。这部分残留的信息就构成了所谓的“僵尸进程”。
具体来说,僵尸进程产生的原因在于以下几点:
- 资源回收:当子进程终止时,其占用的内存、打开的文件等资源会被操作系统回收,但为了告知父进程子进程是如何终止的(正常退出、异常退出及退出状态码等信息),操作系统需要保留一些基本信息。
- 信息传递机制:操作系统通过保留子进程的少量信息(即僵尸状态),使得父进程有机会通过
wait()或waitpid()系统调用收集这些信息。只有当父进程执行了这些系统调用后,操作系统才会彻底释放子进程的最后残留资源,结束僵尸状态。 - 默认行为:如果不显式调用
wait()或waitpid(),父进程可能忙于其他任务,或者因为设计上的疏忽,没有及时处理子进程的终止信息。在这种情况下,子进程就会保持僵尸状态,直到父进程最终调用上述系统调用来回收信息。
为了避免僵尸进程占用系统资源,父进程应当负责任地通过wait()或waitpid()系统调用来定期检查并清理其终止的子进程。另外,父进程也可以通过设置信号处理器忽略SIGCHLD信号(使用signal(SIGCHLD, SIG_IGN)),这样子进程退出后,内核会直接释放其资源,不会形成僵尸进程。不过,这样做的同时也会导致父进程无法获取子进程的退出状态。