前面两篇文章学习了进程相关的理论基础,本文开始学习进程编程。
创建进程
系统函数 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 的系统函数。
- 父子进程之间共享文件的情况。
- 终止一个进程的处理流程,系统提供正常终止进程的函数。
- 如何注册退出处理程序,以及相应的系统函数。
好了,今天先说到这,下次继续。加油!
公众号【一起学嵌入式】,“干货”满满