【Linux系统编程学习】Linux进程控制原语(fork、exec函数族、wait)

此为牛客Linux C++和黑马Linux系统编程课程笔记。

1. fork函数

1.1 fork创建单个子进程

#include<unistd.h>
pid_t fork(void);

作用:创建一个子进程。

pid_t类型表示进程ID,但为了表示-1,它是有符号整型。(0不是有效进程ID,init最小,为1)

返回值:失败返回-1;成功返回:① 父进程返回子进程的ID(非负) ②子进程返回 0

注意返回值,不是fork函数能返回两个值,而是fork后,fork函数变为两个,父子需【各自】返回一个。

创建失败主要有以下两个原因:

  • 当前系统的进程数已经达到了系统规定的上限,这时 errno 的值被设置为 EAGAIN
  • 系统内存不足,这时 errno 的值被设置为 ENOMEM

我们常常利用fork()的返回值来判断当前在父进程还是在子进程中。

示例程序:

#include <unistd.h>
#include <stdio.h>int main()
{pid_t pid = fork();if(pid > 0) {// 父进程printf("This is parent process, pid is %d\n", getpid());} else if(pid == 0) {// 子进程printf("This is child process, pid is %d, my parent's pid is %d\n", getpid(), getppid());}return 0;
}

运行结果为:
在这里插入图片描述

1.2 循环创建多个子进程

如果现在想要使用fork()编写一个能够创建多个子进程的程序,该如何编写?直观的想法是直接循环:

#include <unistd.h>
#include <stdio.h>int main()
{int i;for(i = 0; i < 3; ++i) {pid_t pid = fork();}printf("im a process, my pid is %d\n", getpid());return 0;
}

执行发现,输出了8个语句,说明一共有8个进程。
这是因为fork出的子进程也在执行当前程序,也就是说当前趟循环创建出的子进程在也会执行下一次循环的fork(),创建出子进程的子进程,最后一共创建了1+2+4=7个子进程,故一共有8个进程。

要想只创建当前父进程的3个子进程,需要如此编写:

#include <unistd.h>
#include <stdio.h>int main()
{int i;for(i = 0; i < 3; ++i) {pid_t pid = fork();if(pid == 0) {break;}}printf("im a process, my pid is %d, my ppid is %d\n", getpid(), getppid());return 0;
}

再执行,发现输出了4个语句:
在这里插入图片描述
其中前三个都是父进程22000的子进程。

1.3 fork父子进程的虚拟内存空间

父子进程之间在fork后。有哪些相同,那些相异之处呢?

刚fork之后:
父子相同处: 全局变量、.data、.text、栈、堆、环境变量、用户ID、宿主目录、进程工作目录、信号处理方式…
父子不同处: 1.进程ID 2.fork返回值 3.父进程ID 4.进程运行时间 5.闹钟(定时器) 6.未决信号集

似乎,子进程复制了父进程0-3G用户空间内容,以及父进程的PCB,但pid不同。真的每fork一个子进程都要将父进程的0-3G地址空间完全拷贝一份,然后在映射至物理内存吗?

当然不是!父子进程间遵循读时共享写时复制的原则。这样设计,无论子进程执行父进程的逻辑还是执行自己的逻辑都能节省内存开销。

以全局变量为例,一旦父进程或子进程要对一全局变量做修改,其对应的子进程或父进程就把该全局变量复制一份到自己的虚拟内存空间中,映射至新的物理内存,看如下示例代码:

#include <unistd.h>
#include <stdio.h>int var = 1;int main()
{int i;pid_t pid = fork();if(pid > 0) {var = 2;printf("parent var = %d\n", var);}else if(pid == 0) {var = 3;printf("child var = %d\n", var);}return 0;
}

执行结果为:
在这里插入图片描述
可见全局变量是读时共享写时复制。

【重点】:父子进程共享:1. 文件描述符(打开文件的结构体) 2. mmap建立的映射区 (进程间通信详解)

特别的,fork之后父进程先执行还是子进程先执行不确定。取决于内核所使用的调度算法。

2. exec函数族

2.1 介绍

exec 函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。

fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的pid并未改变。

exec 函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程 ID 等一些表面上的信息仍保持原样,颇有些神似“三十六计”中的“金蝉脱壳”。看上去还是旧的躯壳,却已经注入了新的灵魂。只有调用失败了,它们才会返回 -1,从原程序的调用点接着往下执行,如图:
在这里插入图片描述
左边是某进程的虚拟地址空间及内容,该进程内部使用exec执行了a.out可执行文件,右边红色的是a.out文件虚拟地址空间中的用户区,则调用结束以后该进程如图:
在这里插入图片描述
替换原进程用户区内容,内核区不变。

exec函数族如下:

int execl(const char *path, const char *arg, ...)/* (char *) NULL */);
int execlp(const char *file, const char *arg, ...) /* (char *) NULL */);
int execle(const char *path, const char *arg, ...)/*, (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
int execve(const char *filename, char *const argv[], char *const envp[]);

前六个函数是标准C库函数,最后一个是linux系统函数。前六个函数是调用最后一个函数实现的。
各个函数名都已exec开头,后面为以下字母组合,分别代表不同功能:

  • l(list) 参数地址列表,以空指针结尾
  • v(vector) 存有各参数地址的指针数组的地址
  • p(path) 按 PATH 环境变量指定的目录搜索可执行文件
  • e(environment) 存有环境变量字符串地址的指针数组的地址

最常用函数为execl函数。

2.2 execl

int execl(const char *path, const char *arg, ...)

参数:

  • path:需要指定的执行的文件的路径或者名称,推荐使用绝对路径。

  • arg:是执行可执行文件所需要的参数列表,需要注意:
    第一个参数一般没有什么作用,为了方便,一般写的是执行的程序的名称,
    从第二个参数开始往后,就是程序执行所需要的的参数列表。
    参数最后需要以NULL结束(哨兵)

返回值:只有当调用失败,才会有返回值,返回-1,并且设置errno;如果调用成功,没有返回值。

示例程序:

主程序execl.c:

#include <unistd.h>
#include <stdio.h>int var = 1;int main()
{pid_t pid = fork();if(pid > 0) {printf("im parent, pid is %d\n", getpid());sleep(1);}else if(pid == 0) {execl("child", "child", NULL);printf("its execl.c program\n");}return 0;
}

调用的子程序child.c:

#include <unistd.h>
#include <stdio.h>int main()
{printf("im child, pid is %d, ppid is %d\n", getpid(), getppid());return 0;
}

执行主程序,结果如下:
在这里插入图片描述
印证了之前的说法,子进程调用execl执行child程序后,进程号不变。
而主程序中的printf("its execl.c program\n");没有被执行,说明子进程调用execl后程序段已经被替换。

3. wait和waitpid函数

学习wait函数之前,我们最好先要了解什么是孤儿进程,什么是僵尸进程。

3.1 孤儿进程

在这里插入图片描述
看下面的示例程序:

#include <unistd.h>
#include <stdio.h>int main()
{pid_t pid = fork();if(pid > 0) {printf("im parent, pid is %d\n", getpid());}else if(pid == 0) {sleep(1);printf("im child, my parent pid is %d\n", getppid());}return 0;
}

fork出子进程后,让子进程睡眠1秒后再执行printf语句,此时父进程已经运行结束,该子进程便成了孤儿进程,运行结果如下:
在这里插入图片描述
一秒后输出:
在这里插入图片描述
可以看到子进程的父进程的pid为1,在linux中正是init进程对应的进程号,可见当出现孤儿进程时,init进程会”收养“该进程。

3.2 僵尸进程在这里插入图片描述

如果父进程调用了wait( )或者waitpid( ),父进程将会释放已经执行完的子进程的PCB资源;如果父进程没有调用了wait( )或者waitpid( ),父进程结束后,init进程收养了子进程后,init进程也将负责释放子进程的PCB资源;但是,如果父进程是一个循环,或者一直在执行,那父进程结束之前它的已经执行完的子进程就成为了僵尸进程。

看如下示例程序:

#include <unistd.h>
#include <stdio.h>int main()
{pid_t pid = fork();if(pid > 0) {while(1) {printf("im parent, pid is %d\n", getpid());sleep(1);}} else if(pid == 0) {printf("im child, pid is %d\n", getpid());}return 0;
}

父进程循环,子进程变成了僵尸进程,使用ps -aux查看进程状态,可以看到子进程的状态为Z+,意思时僵尸进程。

接下来继续介绍wait和waitpid函数。

3.3 wait函数

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *wstatus);

功能:等待当前进程的任意一个子进程结束,如果任意一个子进程结束了,此函数会回收该子进程的资源。

参数:int *wstatus 为进程退出时的状态信息,传入的是一个int指针类型的变量,传出参数。

返回值:

  • 成功:返回被回收的子进程的id

  • 失败:-1 (代表所有的子进程都是结束的,调用函数失败)

调用wait函数的进程会被挂起(阻塞),直到它的一个子进程退出或者收到一个不能被忽略的信号时才被唤醒(相当于继续往下执行)

如果没有子进程了,函数立刻返回,返回-1;如果子进程都已经结束了,也会立即返回,返回-1.

示例程序如下:

#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>int main()
{pid_t pid = fork();if(pid > 0) {int ret = wait(NULL);if(ret == -1) {printf("no child");} else {printf("child %d is dead", ret);}sleep(1);} else if(pid == 0) {while(1) {printf("im child, pid is %d\n", getpid());sleep(1);}}return 0;
}

程序运行后,子进程无限循环,父进程由于调用了wait函数,在等待子进程退出而处于阻塞态,现在我们使用kill -9 杀死子进程后,输出如下:
在这里插入图片描述
可见子进程结束后,父进程从wait处开始继续执行,并且返回了子进程的pid。

3.4 waitpid函数

与wait相近,只不过能够指定回收的进程pid。

#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *wstatus, int options);

功能:回收指定进程号的子进程,可以设置是否阻塞。
参数:
pid:

  • pid > 0 : 某个子进程的pid
  • pid = 0 : 回收当前进程组的所有子进程
  • pid = -1 : 回收任意子进程,相当于 wait()
  • pid < -1 : 某个进程组的组id的绝对值,回收指定进程组中的子进程

options:设置阻塞或者非阻塞

  • 0 : 阻塞
  • WNOHANG : 非阻塞

返回值:

  • > 0 返回子进程的id
  • 0 : options=WNOHANG, 且子进程正在运行。
  • -1 :错误,或者没有子进程了

注意:一次wait或waitpid调用只能清理一个子进程,清理多个子进程应使用循环。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/384563.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Linux系统编程学习】匿名管道pipe与有名管道fifo

此为牛客Linux C和黑马Linux系统编程课程笔记。 0. 关于进程通信 Linux环境下&#xff0c;进程地址空间相互独立&#xff0c;每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到&#xff0c;所以进程和进程之间不能相互访问&#xff0c;要交换…

【Linux系统编程学习】信号、信号集以其相关函数

此为牛客Linux C和黑马Linux系统编程课程笔记。 文章目录0. 信号的概念1. Linux信号一览表2. 信号相关函数3. kill函数4. raise函数5. abort函数6. alarm函数7. setitimer函数8. signal函数9. 信号集10. 自定义信号集相关函数11. sigprocmask函数12. sigpending函数13. sigacti…

【Linux系统编程学习】父进程捕获SIGCHLD信号以处理僵尸进程

配合之前说过的sigaction函数和waitpid函数&#xff0c;我们可以解决子进程变成僵尸进程的问题。 先看如下示例程序&#xff1a; #include <sys/time.h> #include <sys/types.h> #include <stdio.h> #include <stdlib.h> #include <signal.h> …

【Linux系统编程学习】Linux线程控制原语

此为牛客Linux C课程笔记。 0. 关于线程 注意&#xff1a;LWP号和线程id不同&#xff0c; LWP号是CPU分配时间片的依据&#xff0c;线程id是用于在进程内部区分线程的。 1. 线程与进程的区别 对于进程来说&#xff0c;相同的地址(同一个虚拟地址)在不同的进程中&#xff0c;反…

【Linux网络编程学习】预备知识(网络字节序、IP地址转换函数、sockaddr数据结构)

此为牛客Linux C课程和黑马Linux系统编程笔记。 1. 网络字节序 我们已经知道&#xff0c;内存中的多字节数据相对于内存地址有大端和小端之分。 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分。网络数据流同样有大端小端之分&#xff0c;那么如何定义网络数…

【Linux网络编程学习】socket API(socket、bind、listen、accept、connect)及简单应用

此为牛客Linux C课程和黑马Linux系统编程笔记。 1. 什么是socket 所谓 socket&#xff08;套接字&#xff09;&#xff0c;就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。 一个套接字就是网络上进程通信的一端&#xff0c;提供了应用层进程利用网络协议交换…

【Linux网络编程学习】使用socket实现简单服务器——多进程多线程版本

此为牛客Linux C课程和黑马Linux系统编程笔记。 1. 多进程版 1.1 思路 大体思路与上一篇的单进程版服务器–客户端类似&#xff0c;都是遵循下图&#xff1a; 多进程版本有以下几点需要注意&#xff1a; 由于TCP是点对点连接&#xff0c;服务器主进程连接了一个客户端以后…

【Linux网络编程学习】I/O多路复用——select和poll

此为牛客Linux C课程和黑马Linux系统编程笔记。 0. I/O多路复用 所谓I/O就是对socket提供的内存缓冲区的写入和读出。 多路复用就是指程序能同时监听多个文件描述符。 之前的学习中写了多进程和多线程版的简单服务器模型&#xff0c;但是有个问题&#xff1a;每次新来一个客…

【Linux网络编程学习】阻塞、非阻塞、同步、异步以及五种I/O模型

文章目录1. 基本概念1.1 阻塞与非阻塞1.2 同步与异步1.3 为什么没有“异步阻塞”2. 五种IO模型2.1 阻塞 blocking2.2 非阻塞 non-blocking2.3. IO复用&#xff08;IO multiplexing&#xff09;2.4 信号驱动&#xff08;signal-driven&#xff09;2.5 异步&#xff08;asynchron…

STM32时钟树解析

本人之前其实也用STM32做过一些小东西&#xff0c;但因为时钟的初始化一般是直接在SystemInit时钟系统初始化函数里直接配置为72MHz&#xff0c;所以对于STM32的时钟框图并没有怎么理会&#xff0c;今天刚好有空就重新看了一下并写一篇博客记录一下吧&#xff0c;以免以后又忘了…

S3C2440时钟体系

S3C2440在默认情况下&#xff0c;整个系统全靠一个12MHz的外部晶振提供频率来工作运行的&#xff0c;也就是说CPU、内存、UART、ADC等所有需要用到时钟频率的硬件都工作在12MHz下&#xff0c;但是通过查阅芯片手册我们知道CPU时钟最高可为400MHZ&#xff0c;那么怎么设置时钟让…

关于MCU、CPU扩展SDRAM的一个小知识

像上图这种硬件电路图上的16个数据位和我们在初始化SDRAM的时候设置的16位数据位宽是指我们读写SDRAM的时候可以同时读写16个数据位&#xff0c;数据线越多肯定越快&#xff0c;但是数据线也不可能无限增加&#xff0c;我们在程序里是可以读写8位&#xff0c;16位&#xff0c;3…

S3C2440扩展SDRAM

本文主要目的是记录一下S3C2440扩展SDRAM的一些知识&#xff0c;方便以后查阅。 通过查阅手册我们知道&#xff0c;2440有8个可以用来扩展内存的BANK&#xff0c;其中第6和第7还可用来扩展SDRAM 下面我们来看一下2440扩展SDRAM需要设置哪些寄存器。 一、BWSCON寄存器 该寄存器…

汇编语言的相对跳转和绝对跳转以及反汇编代码解析

上图第一行的b1 main为相对跳转&#xff0c;即跳转到pcoffset,其中pc为当前pc值&#xff0c;offset可以理解为偏移地址&#xff0c;也就是根据当前所在地址加上偏移地址实现跳转&#xff0c;为相对跳转。 我们来看看它的反汇编代码 上图清除完bss区后使用b1指令跳转到30000668…

韦东山嵌入式第一期14课第004节_und异常模示程序示例_P笔记

本节课的第一个程序韦老师是想让大家见识一下未定义异常&#xff0c;而第二个程序是对第一个程序进行改进&#xff0c;防止在某些条件下执行不了&#xff0c;下面就来讲一下第2个程序改进了哪些地方并且有什么用。 程序在此路径中&#xff1a;源码文档图片\源码\源码_20180321…

关于NOR FLASH地址左右移的问题

问题引入&#xff1a;不知道你会不会有这样的疑问&#xff1a;为什么在发送解锁命令时&#xff0c;我们不用右移一位&#xff0c;而发送扇区地址时却要右移一位&#xff08;nor_cmd函数内部已经左移一位&#xff09;&#xff0c;这里先补充说明一下说明是cpu角度和nor角度&…

在linux下利用ls命令进行模糊查找

如上图&#xff0c;我们当前路径下有三个文件&#xff0c;分别为helloworld.c以及helloworld和1.c&#xff0c;直接输入命令ls则显示所有文件&#xff0c;我们可以利用ls 加*的方向进行模糊查找。 输入ls 目录名 形式的命令行&#xff0c;则是对该目录名下的文件全部进行显示&a…

Linux下没有包含头文件(不知是哪个)导致编译无法通过的解决心得

最近写程序的时候编译出错了&#xff0c;提示信息为&#xff1a;invalid use of undefined type fb_var_screeninfo。显示根据英文知道是没有定义 fb_var_screeninfo这个类型&#xff0c;明显是缺少了某个头文件&#xff0c;但是缺少哪个头文件以及有什么又快又好的解决方法呢&…

Linux编译程序时加-I指定头文件位置

Linux下编译出现以下错误&#xff0c;错误的原因是在/usr/local/arm/arm-2009q3/bin/../arm-none-linux-gnueabi/libc/usr/include/freetype/config/下找不到ftheader.h&#xff0c;而我到该目录下看&#xff0c;发现路径是这样的rootubuntu:/usr/local/arm/arm-2009q3/arm-non…

关于源文件用不同的编码方式编写,会导致执行结果不一样的现象及解决方法

如果我们编写以下程序&#xff0c;并分别另存为ANSI和UTF-8两种不同的编码方式保存&#xff0c;放到Linux下编译并运行如下图&#xff0c;两端相同的程序以不同的编码方式保存编译后的运行结果不一样&#xff0c;./ansi采用ANSI编码方式&#xff0c;会自动采用GBK方式来保存中文…