文章目录

  • 1、进程
    • 1.1、创建进程
      • 1.1.1、fork()
      • 1.1.2、vfork()
    • 1.2、执行进程——exec函数族
    • 1.3、进程退出
      • 1.3.1、exit()和_exit()
    • 1.4、进程回收
      • 1.4.1、僵尸进程
      • 1.4.2、wait()
      • 1.4.3、waitpid()
  • 2、写在最后

1、进程

进程的定义:
进程是程序处于一个执行环境中在一个数据集上的一次运行过程,它是系统进行资源分配和调度的一个独立单位。每个进程都有自己独立的系统资源,一个进程中可以有多个线程,系统是系统资源分配的基本单位。

1.1、创建进程

整个Linux操作系统都是由父子进程结构组成,每个进程都有创建者,也就是父进程,但是有一个进程例外,也就是init进程,其为系统启动初始化后执行的第一个进程。

1.1.1、fork()

函数原型:

#include <unistd.h>
pid_t fork(void); //pid_t等价于有符号整型

主要作用: 创建一个子进程,这也就代表着,父进程可通过调用该函数创建一个子进程,父子进程各自独立,拥有自己的PCB,内存用户区,临时资源等,各自独立参与CPU调度
返回值:pid_t类型的变量,一共有两个返回值(父进程返回一个,子进程返回一个)

细节探究:
fork函数执行的流程:(1)调用_CREATE函数,也就是进程创建部分,子进程进行虚拟地址申请,在子进程的内核空间进行不完全拷贝(2)调用_CLONE函数,向父进程拷贝必要资源,子进程的用户空间进行完全拷贝,子进程继承所有父进程资源,如临时堆栈拷贝,代码完全拷贝(3)子进程执行fork函数剩余部分,执行最后这个语句,fork函数就会有二次返回,如果成功返回0,不成功返回1。不成功的主要原因有:
a.系统内存不够 b.进程表满(容量一般为200~400)c.用户的子进程太多(一般不超过25个)

所以fork函数的返回值情况如下:
父进程调用fork(),返回子进程pid(>0)
子进程调用fork(),子进程返回0,调用失败的话就返回-1
这也就说明了fork函数的返回值是2个

fork的应用场景:一个父进程希望复制自己,使父进程和子进程执行不同的代码段。在网络编程中常用到fork函数,例如:父进程等待客户端的服务请求,当请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求。示例程序如下所示(仅展示关键代码):

void test_fork()
{pid_t pid;int data;while(1){printf("please input command number:");scanf("%d", &data);if(1 == data){pid = fork();if(pid > 0){}else if(0 == pid){while(1){printf("do net require, pid = %d\n", getpid()); //模拟子进程处理请求sleep(3);}}else{perror("create process failed\n"); //创建进程失败exit(-1);}}else{printf("isn't an excepted number\n");}}
}

执行该段代码编译生成的可执行文件可得到如下结果:

父进程执行时一直在等待用户输入数据,当用户输入1时,创建子进程(pid为1785),并建立网络链接,父进程则继续等待用户输入下一个数据,并根据输入的具体值完成指定的操作。

总结:父子进程都执行fork函数,但执行不同的代码段,获取不同的返回值。创建出来的子进程在继承了所有的父进程资源后会从代码的fork()处继续向下执行,该特性可被如下的程序段所验证:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>int main()
{pid_t pid;printf("aaaaaa process pid = %d\n", getpid());pid = fork();printf("bbbbbb process pid = %d\n", getpid());while(1){sleep(1);}return 0;
}

执行该段代码编译生成的可执行文件可得到如下结果:

不难看出,父进程执行的代码段打印了"aaaaaa"和"bbbbbb"这两串字符串,而子进程仅打印了"bbbbbb"这一串字符串,即可验证上文所述。

1.1.2、vfork()

函数原型:

#include <unistd.h>
pid_t vfork(void); //pid_t等价于有符号整型

主要作用: 创建一个子进程,作用与fork函数一致

细节探究:
vfork函数的调用序列和返回值与fork相同,但二者的语义不同
(1)vfork直接使用父进程存储空间,不拷贝(可将存储空间理解为联合体,每个成员均为vfork函数创建的子进程,所有子进程共用同一段内存)
(2)vfork保证子进程先运行,在子进程调用exitexec函数族之后父进程才会被调度执行,如果在子进程退出之前子进程依赖父进程的进一步动作,则会导致死锁。下文程序段可验证vfork函数的这一特性(仅展示关键代码):

void test_vfork()
{pid_t pid;int cnt = 0;pid = vfork();if(pid > 0){while(1){printf("i am a parent process\n");sleep(1);}}else if(0 == pid){while(1){printf("i am a child process\n");cnt++;sleep(1); //在子进程占用一段运行时间后,主动结束子进程if(3 == cnt){cnt = 0;exit(0);}}}else{perror("create process failed\n");exit(-1);}
}

执行该段代码编译生成的可执行文件可得到如下结果:

父进程调用vfork函数创建子进程,先运行子进程,在合适的条件下子进程退出后父进程得以执行。

1.2、执行进程——exec函数族

函数原型: Linux下的exec函数族是6个以exec开头的函数,具体如下:

#include <unistd.h>int execl(const char *path, const char *arg, ...); //arg...传递给执行的程序的参数列表
int execv(const char *path, char *const argv[]); //arg...封装成指针数组的形式传递
int execle(const char *path, const char *arg, ..., char *const envp[]); //使用默认的环境变量(environment),在envp[]中指定当前进程所使用的环境变量
int execve(const char *path, char *const argv[], char *const envp[]);
int execlp(const char *file, const char *arg, ...);
int execvp(const char *file, char *const argv[]);

主要作用: 通过fork()或者vfork()函数创建子进程后,子进程几乎复制了父进程的全部内容,如果我们想要父子进程执行的内容不同,可以通过exec函数族实现。exec函数族提供了一个在进程中执行另一个程序的方法,它可以根据指定的文件名或目录名找到可执行文件,并用它来取代当前进程的数据段,代码段和堆栈段。在执行完后,当前进程除了进程号外,其它内容都被替换。

细节探究:
日常开发中,exec函数族最常被用到的函数是:

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

execl的示例程序如下所示(仅展示关键代码):

func.c

void test_exec()
{pid_t pid;pid = fork();if(pid > 0){printf("i am a parent process\n");}else if(0 == pid){printf("i am a child process\n");if(execl("/home/pi/project/process_1/subprocedure", "subprocedure", NULL) < 0){perror("execl failed\n");exit(1); //exec调用失败,主动结束子进程}}else{perror("create process failed\n");exit(-1);}printf("end mark point\n");
}

subproc.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>int main()
{int cnt = 0;while(cnt < 3){printf("child process execute subprocedure\n");cnt++;sleep(3);}exit(0);
}

执行该段代码编译生成的可执行文件可得到如下结果:

可以发现,父进程剩下的部分,子进程并没有执行。子进程在调用exec函数族后,就只执行subproc.c编译出来的可执行文件subprocedure。通过exec函数族实现了父子进程执行不同的内容。

特别注意:
(1)确保形参格式正确:如果exec函数族的第一个形参为path,则必须以“/”开头,否则将视其为文件名;path需要为完整的文件目录路径,即被执行的文件也需要被包含在path中。如果第一个形参为file则会自动在path中搜索
(2)要判断exec是否执行成功,如果执行失败应结束该子进程。一种执行失败的情况如下所示:

这种情况是path路径的末尾未包含可执行文件所导致,exec执行失败,结束子进程。

1.3、进程退出

进程常见的退出方法主要有以下三种:
(1)main函数中调用return实现进程退出,main函数对应的进程的状态信息将会自动被系统中的特定进程所回收
(2)ctrl+c中断正在运行的进程(信号机制)
(3)任何进程调用exit()_exit()实现进程退出

1.3.1、exit()和_exit()

函数原型:

#include <stdlib.h>
void exit(int status);#include <unistd.h>
void _exit(int status);

主要作用: 两个函数功能和用法是一样的,都能结束调用此函数的进程
参数:status为进程退出时的一个状态信息,ANSIC标准要求使用值0或宏EXIT_SUCCESS指示程序正常终止,使用值1或宏EXIT_FAILURE指示程序异常终止

细节探究:
exit()_exit()最主要的区别有以下几点:
(1)exit()属于标准库函数(标准c库中的函数),_exit()属于系统调用函数(Linux系统中的函数)。使用时,两个函数所包含的头文件不一样
(2)exit()在执行时,系统会检测进程打开文件情况,并将处于文件缓冲区的内容写入到文件当中再退出。而_exit()则直接退出,不会将缓冲区的内容写入文件

下文给出的程序段可以验证exit()_exit()之间的区别:
测试函数test()

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>int main()
{printf("test begin\n");printf("output string buffer");exit(0); //在main函数中等价于return 0printf("test end\n");//return 0;
}

执行该段代码编译生成的可执行文件可得到如下结果:

测试函数_test()

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>int main()
{printf("test begin\n");printf("output string buffer");_exit(0); //在main函数中等价于return 0printf("test end\n");//return 0;
}

执行该段代码编译生成的可执行文件可得到如下结果:

printf()使用缓存I/O的方式,该函数在遇到'\n时自动从缓冲区中将记录读出。而exit()也能将文件缓冲区的内容写入到文件中再退出。所以不难看出两个运行结果的差异:执行exit()代码段的第二行信息能被打印,而执行_exit()代码段的第二行信息不能被打印。如果第一行输出也没有格式化控制符\n,则_exit()也不会将其打印出来。

1.4、进程回收

1.4.1、僵尸进程

僵尸进程简介: 在Linux中,正常情况下,子进程是通过父进程创建的,子进程再创建新的进程。子进程的结束和父进程的运行是一个异步过程。即父进程永远也无法预测子进程到底什么时候结束。当一个子进程完成它的工作退出之后,它的父进程需要采用合适的手段对子进程实现资源回收,否则就会产生僵尸(Zombie)进程。

僵尸进程危害:
(1)僵尸进程会造成一定的资源浪费,占用不必要的资源。任何一个子进程(init除外)在退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。但是仍然会保留一些信息(包括进程号,退出状态,运行时间等),称为僵尸进程的数据结构。
(2)当进程id达到了最大值的时候,因为有僵尸进程占用了部分进程id,使得无法再打开新的进程。

僵尸进程产生示例:

void test_zombie()
{pid_t pid;pid = fork();if(pid > 0){sleep(1); //延时片刻,保证子进程先运行printf("i am a parent process\n");while(1); //父进程保持不退出}else if(0 == pid){printf("i am a child process\n"); //子进程直接退出}else{perror("create process failed\n");exit(-1);}printf("end mark point\n");
}

执行该段代码编译生成的可执行文件,并另一个终端使用指令ps -ajx查看,可得到如下结果:

进程状态为Z或Z+的进程即为僵尸进程

僵尸进程避免:
实际开发中有多种手段避免僵尸进程的出现,如子进程退出时向父进程发生SIGCHILD信号,多次fork(),将子进程变成孤儿进程等。下文着重讲解的,也是最常用的一种方法,是调用wait()/waitpid()函数,使子进程退出时被父进程回收。

1.4.2、wait()

函数原型:

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

主要作用: 父进程调用wait()阻塞等待子进程退出,并回收子进程的状态信息
参数: *status指向的整型对象来保存子进程结束时的状态,即exit()的形参值。此外,子进程的结束状态也可以有一些特定的宏来测定
返回值: 若成功回收子进程则返回子进程的进程号,失败则返回-1

细节探究:
使用wait()回收进程时应该注意以下几点:
(1)如果子进程没有结束,则父进程会阻塞等待,无法向前推进,直到子进程结束。如果父进程占用某个临界资源,则容易出现死锁现象
(2)wait()一次只能回收一个子进程,如果创建了多个子进程,则哪个子进程先结束就先被回收
(3)形参*status可以为NULL,表示直接释放子进程PCB,不接收返回值

wait()的示例程序如下所示(仅展示关键代码):

void test_wait()
{pid_t pid;pid = fork();if(pid > 0){pid_t retval;int status;printf("parent process is waiting...\n");retval = wait(&status); //阻塞等待子进程退出printf("parent process wait done, retval = %d status = %d\n", retval, status); //打印已回收子进程的进程号和结束状态}else if(0 == pid){int cnt = 0;while(cnt < 3){cnt++;printf("child process is running\n");sleep(3);}exit(0); //子进程退出}else{perror("create process failed\n");exit(-1);}
}

执行该段代码编译生成的可执行文件可得到如下结果:

父进程创建子进程之后阻塞等待子进程的退出,当子进程退出后父进程将回收子进程的状态信息。

1.4.3、waitpid()

函数原型:

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

主要作用:wait一致,等待子进程退出并回收子进程的状态信息
参数:
pid:主要有两种情况,pid = -1表示回收任何一个子进程,pid > 0表示回收对应pid的任一子进程
status:与wait()中的参数status作用一致
option:指定回收方式,常见的为0(阻塞等待子进程结束)或WNOHANG(非阻塞等待)
返回值: 若成功回收子进程则返回子进程的进程号,失败则返回-1,返回0表示optionWNOHANG且没有子进程退出

细节探究:
waitpid()的作用和wait()一样,但它并不一定等待第一个结束的子进程。waitpid()提供了若干选项,可以实现非阻塞的进程回收功能。事实上在Linux内部实现wait()时直接调用的就是waitpid(),可以将wait()理解成waitpid()的一个特例。下文代码展示了两个函数之间的等效关系:

//retval = wait(&status);
retval = waitpid(-1, &status, 0); //阻塞等待子进程退出

waitpid()去跑上文wait()示例程序可得到一样的结果,如下图所示:

下文给出的程序段是验证waitpid()的非阻塞回收功能(仅展示关键代码):

void test_waitpid()
{pid_t pid;pid = fork();if(pid > 0){sleep(1); //睡眠一段时间,确保子进程先运行pid_t retval;int status;printf("parent process is waiting...\n");if((retval = waitpid(-1, &status, WNOHANG)) == 0){printf("parent process didn't wait child process exit\n"); //非阻塞等待子进程退出,回收成功}else{printf("parent process wait done, retval = %d status = %d\n", retval, status); //回收失败}while(1); //父进程不能退出,否则子进程会成为孤儿进程,由init进程完成状态收集工作}else if(0 == pid){int cnt = 0;while(cnt < 4){cnt++;printf("child process is running\n");sleep(2);}exit(0);}else{perror("create process failed\n");exit(-1);}
}

执行该段代码编译生成的可执行文件可得到如下结果:

可以发现,父进程未能成功回收子进程,子进程在退出后成为僵尸进程,在另一个Linux终端输入命令ps -ajx可观察到这一僵尸进程。

若想以非阻塞方式成功回收进程,可以定期执行waitpid(),直到成功回收子进程的状态信息。示例程序如下所示(仅展示关键代码):

void test_waitpid()
{pid_t pid;pid = fork();if(pid > 0){pid_t retval;int status;printf("parent process is waiting...\n");while((retval = waitpid(-1, &status, WNOHANG)) == 0){sleep(2); //非阻塞等待子进程退出,如果未退出则睡眠一段时间}printf("parent process wait done, retval = %d status = %d\n", retval, status);}else if(0 == pid){int cnt = 0;while(cnt < 4){cnt++;printf("child process is running\n");sleep(2);}exit(0);}else{perror("create process failed\n");exit(-1);}
}

执行该段代码编译生成的可执行文件可得到如下结果:

父进程以一定的时间间隔非阻塞等待子进程退出,当子进程退出后成功回收其状态信息。

2、写在最后

Linux程序设计是一门很深的学问,由于时间关系,还有很多我想补充的内容都未能放在这上面。即将开启一段新的旅程了,日后有时间还会不断地去完善,祝好运!

Linux程序设计—多进程编程相关推荐

  1. linux下多进程编程简介

    两年前的文章,拿过来充充门面. ------------------------ linux下多进程编程简介 ( 作者:mikespook | 发布日期:2002-12-8 | 浏览次数:272 ) ...

  2. Linux程序设计-3-Linux编程准备知识

    Linux Programming Prerequisite 1. 编程原则 抽象和具体 库(API)的调用与选择:从技术角度,一般使用标准库,如果使用商业库,则会给对方平台带来一定的收益,但是对自己 ...

  3. linux编写多进程程序实验,实验7 编写多进程程序

    实验七编写多进程程序 学生姓名:李亚军学号:6100412196 专业班级:卓越计科121班 1.实验目的 通过编写多进程程序,使读者熟练掌握fork().exec().wait()和waitpid( ...

  4. socket多进程编程

    socket多进程编程 一.服务器并发访问的问题 服务器按处理方式可以分为迭代服务器和并发服务器两类.平常用C写的简单Socket客户端服务器通信,服务器每次只能处理一个客户的请求,它实现简单但效率很 ...

  5. linux线程并不真正并行,多核时代:并行程序设计探讨(3)——Windows和Linux对决(多进程多线程)...

    并行程序设计探讨(3)--Windows和Linux对决(多进程多线程) 前面的博文经过分析总结,最后得出两种并行技术:多进程多线程.多机协作.对于多进程和多线程来说,最有代表性且最常见的的莫过于Wi ...

  6. linux 面包店 多进程,Linux下的多进程编程(一)

    什么是一个进程?进程这个概念是针对系统而不是针对用户的,对用户来说,他面对的概念是程序.当用户敲入命令执行一个程序的时候,对系统而言,它将启动一个进程.但和程序不同的是,在这个进程中,系统可能需要再启 ...

  7. linux c多进程多线程,linux下的C\C++多进程多线程编程实例详解

    linux下的C\C++多进程多线程编程实例详解 1.多进程编程 #include #include #include int main() { pid_t child_pid; /* 创建一个子进程 ...

  8. Linux多进程编程(2)

    简介 IPC(Inter Process Communication,进程间通信)的方式总共有三种,分别是信号量.共享内存和消息队列,本文介绍前两种. 在Linux中,进程之间操作公共数据时,需要进行 ...

  9. Linux -- 多进程编程之 - 守护进程

    内容概要 一.守护进程概述 二.守护进程创建 2.1.创建子进程,父进程退出 2.2.在子进程中创建新会话 2.2.1.进程组和会话期 2.2.2.setsid()函数说明 2.3.改变当前工作目录 ...

最新文章

  1. python初始化函数_当你学会了Python爬虫,网上的图片素材就免费了
  2. lumen php命令,php – 如何使用命令行手动运行laravel / lumen作业
  3. java中奇偶数的判断
  4. 十分钟完成的操作系统
  5. 也谈创业企业CEO该拿多少工资
  6. 最受欢迎Java数据库访问框架大比拼,你独爱哪一款?
  7. 利用Tushare合成期货主力连续数据
  8. Photoshop CC 2019魔棒工具的抠图
  9. 温度补偿计算公式_基于温度压力补偿计算的燃气表计量方法与流程
  10. beego golang bootstrap-table做月度考勤(打卡、签到)统计表
  11. 【Day4.3】大皇宫内蹭讲解
  12. HDU CCPC网络选拔赛 6441 Find Integer(数学)
  13. Fail to allocate bitmap
  14. php【websocket】
  15. Mac无法安装第三方软件
  16. html中实现页面跳转代码怎么写,用JavaScript怎么实现页面跳转?
  17. 《前端技巧》清理微信浏览网站的缓存,Cookie
  18. 十进制转十六进制 代码
  19. verilog中将fft转换成ifft
  20. android的多开器解析和检测实现

热门文章

  1. 坐标系转换矩阵和几何转换矩阵的关系
  2. 360随身WIFI作USB无线网卡的做法
  3. python使用win32com读写excel的问题
  4. Simscape Multibody简介与入门(上) 准备工作
  5. 蓝桥杯真题:平面分割
  6. 【SRS】ATC介绍
  7. Apache Atlas 是什么?
  8. EJB框架 详细介绍和注解的使用
  9. 报名投票链接怎么做做一个投票的链接怎么做微信投票链接怎么做
  10. 【大数据】9大实战项目解决你所有烦恼(写论文、找工作)