Linux编程入门(15)-进程(三)创建和终止

前面两篇文章学习了进程相关的理论基础,本文开始学习进程编程。

Linux编程入门(13)-进程(一)

Linux编程入门(14)-进程(二)

创建进程

系统函数 fork()

Linux 提供了系统调用 fork() 用于创建一个新进程,其函数原型为:

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

pid_t fork(void);

如果调用成功,函数 fork() 在父进程中返回子进程的 ID;在子进程中返回 0。

若调用失败,则在父进程中返回 -1,子进程不会创建。

进程调用 fork() 后,内核的处理过程为:

  • 分配新的内存块和内核数据结构。
  • 复制原来的进程到新的进程。
  • 向运行进程的集合添加新进程。
  • 将控制权返回给两个进程。

从而,子进程获得父进程的栈、数据段、堆、可执行文本段的副本。

新进程创建成功后,父子进程执行相同的程序文本段。两个进程的栈段、数段、以及堆段分别由进程自己使用,每个进程可修改属于自己的段,不会影响另一个进程。

子进程会得到父进程的代码和当前运行到的位置。新进程从 fork 返回的地方开始运行,而不是从程序的开头运行。注意一点,fork 之后父子进程谁先执行是未知的,这取决于当前内核的调度算法

调用 fork() 时,常用的处理流程代码:

pid_t chid_pid;

child_pid = fork();
if(child_pid == 0)
{
	//处理子进程代码
}
else if(child_pid == -1)
{
	//调用出错,处理异常情况	
}
//继续执行父进程代码

获取进程 ID

在使用 fork() 创建新进程时,可以通过返回值来区分父、子进程。

在父进程中,fork() 将返回新创建子进程的 PID。在子进程中,fork() 返回 0。

子进程或其他进程如果想获取进程ID,可以通过 getpid() 函数得到,调用 getppid() 获取父进程 ID。其函数原型为:

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

pid_t getpid(void);
pid_t getppid(void);

文件共享

fork() 执行成功后,子进程会获得父进程所有的文件描述符副本。这意味着,父子进程相互对应的描述符均指向相同的已打开文件句柄。

打开的文件偏移量以及文件标志会在父子进程间共享。

如果一个进程更新了文件偏移量,那么另一个进程会受到影响。这样使得父子进程同时写入一个文件时,二者不会覆盖彼此的写入内容。但是,会出现两个进程写入的内容很随意地混杂在一起。要规避这个问题,需要用到进程间的同步(后边会进行讲解)。

如果不需要文件描述符的共享,可以在调用 fork() 之后,注意两点:

  • 父、子进程使用不同的文件描述。
  • 各自关闭不再使用的描述符。

编程示例

通过编程,来演示 fork() 如何使用,以及其执行情况,示例代码如下:

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

int main()
{
	pid_t fork_new;
	/* 打印父进程 ID */
	printf("fork before, my pid is %d\n", getpid());
	/* 创建新进程 */
	fork_new = fork();
	/* 判断 fork 执行结果 */
	if(fork_new == -1)
	{
		perror("fork");
	}
	else if(fork_new == 0)
	{
		/* 子进程执行,打印自己的进程ID */
		printf("I am the child. my pid is %d\n", getpid());
	}
	else
	{
		/* 父进程执行,打印子进程ID */
		printf("I am the parent. my child is %d\n", fork_new);
	}

	return 0;
}

编译、运行结果:

$ gcc forkdemo.c
$ ./a.out 

fork before, my pid is 2521
I am the parent. my child is 2522
I am the child. my pid is 2522

由执行结果看出,进程 2521 调用 fork() 创建子进程,通过返回值判断是父进程,还是子进程。子进程 PID 为 2522。

由打印情况来看,父进程先得到了执行。fork() 不但能够创建新进程,而且能够区分原来的进程和新创建的进程。

终止进程

终止进程有两种方式:

  • 正常终止。如调用 exit()。
  • 异常终止。如接收到终止进程信号。

此处主要讲解调用系统函数正常终止进程相关的内容。

_exit() 系统函数

进程可以调用 _exit() 函数终止。_exit() 系统函数是一个内核操作,执行的动作包括:

  • 处理所有分配给这个进程的内存。
  • 关闭所有这个进程打开的文件。
  • 释放所有内核用来管理和维护这个进程的数据结构。

_exit() 函数的原型为:

#include <unistd.h>

void _exit(int status);

参数 status 表示进程的终止状态,父进程可以通过调用 wait() 来获取这个状态。该状态字仅有低 8 位为父进程使用。

终止状态为 0,表示进程正常退出;非零表示进程异常退出。

exit() 系统函数

exit() 是 fork() 的逆过程,进程通过调用 exit() 来停止运行。这是一个标准库函数。

exit() 会执行如下动作:

  • 刷新 stdio 缓冲区。
  • 调用由 atexit()on_exit() 注册的(这两个函数后面介绍),退出处理函数,执行顺序与注册顺序相反。
  • 执行当前系统定义的其他与 exit() 相关的操作。
  • 然后调用 _exit() 函数。

其函数原型为:

#include <stdlib.h>

void exit(int status);

参数 status 与 _exit() 的参数相同。

进程终止的动作

系统调用 exit 终止当前进程,需要执行一系列的清理工作。这些工作在不同版本的 Linux 中有些不同,但一般会包括以下操作:

  • 关闭所有打开的文件描述符、目录描述符。
  • 释放该进程持有的任何文件锁。
  • 向父进程发送 SIGCHLD 。
  • 如果父进程调用 wait()waitpid() 来等待子进程结束,则通知父进程。

注册退出处理程序

如果应用程序需要在进程终止时执行一些操作,可以将处理程序注册到内核,在该进程调用 exit() 正常终止时会自动执行。

如果程序调用 _exit() 或因信号而终止,则不会调用退出处理程序。

GNU C 语言函数提供了两种方式来注册退出处理程序,分别是 atexit()on_exit()。函数原型分别如下:

atexit() 函数

#include <stdlib.h>

int atexit(void (*function)(void));     

参数 function 为一个函数指针,atexit() 将其指向的函数注册到内核。

atexit() 调用成功则返回 0。返回非 0,说明注册失败。

function 指向的函数不接受任何参数,也无返回值,其一般形式如下:

void function(void)
{
}

on_exit() 函数

#include <stdlib.h>

int on_exit(void (*function)(int , void *), void *arg);

on_exit() 函数的参数 function,是一个函数指针,指向如下类型的函数:

void function(int status, void *arg)
{
}

调用时,会传递给 function() 两个参数,提供给 exit() 的 status 参数和注册时给 on_exit() 的 arg 参数副本。

参数 arg 意义可以由程序的设计者定义,可将其用作指针,也可用作整型值使用(通过强制类型转换)。

on_exit() 调用成功时,返回 0。失败时,返回非零值。

使用 atexit()on_exit() 可以注册多个退出处理程序,当应用程序调用 exit() 时,这些函数的执行顺序与注册顺序相反。

程序示例

编写实验代码,用于演示如何注册退出处理程序函数,以及处理程序函数的执行顺序。示例代码如下:

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

void atexitFunc1(void)
{
	printf("atexit function 1 called\n");
}

void atexitFunc2(void)
{
	printf("atexit function 2 called\n");
}

void onexitFunc(int status, void *arg)
{
	printf("on_exit function called, status = %d, arg = %ld\n", status, (long)arg);
}

int main(void)
{
	if(on_exit(onexitFunc, (void *)10) != 0)
	{
		perror("on_exit 1");
	}
	
	if(atexit(atexitFunc1) != 0)
	{
		perror("aexit 1");
	}

	if(atexit(atexitFunc2) != 0)
	{
		perror("aexit 2");
	}
	
	if(on_exit(onexitFunc, (void *)20) != 0)
	{
		perror("on_exit 2");
	}
}

编译运行,结果如下:

$ gcc aexit_demo.c -o aexit_demo
$ ./aexit_demo

on_exit function called, status = 0, arg = 20
atexit function 2 called
atexit function 1 called
on_exit function called, status = 0, arg = 10

对照程序代码,退出处理程序函数的执行顺序正好与其注册顺序相反。

小结

本文主要学习了如何创建进程,以及进程退出相关内容。主要有以下几点:

  • 调用系统函数 fork() 可以创建一个进程,以及创建进程的处理流程。
  • 获取进程的PID 和 PPID 的系统函数。
  • 父子进程之间共享文件的情况。
  • 终止一个进程的处理流程,系统提供正常终止进程的函数。
  • 如何注册退出处理程序,以及相应的系统函数。

好了,今天先说到这,下次继续。加油!


公众号【一起学嵌入式】,“干货”满满
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zsky_01

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值