三、多进程编程

服务器分类

服务器按处理方式可分为迭代服务器和并发服务器

  • 迭代服务器:一个时间段只能处理一个客户的请求
  • 并发服务器:同一时间段可以处理多个服务器的请求
    在Linux中有三种方式可实现并发服务器:多进程并发服务器、多线程并发服务器、IO复用

多进程编程

进程的概念

即正在运行的程序及其占用的资源(CPU、内存、系统资源等)叫做进程。

  • 源码:即vim编辑生成的C文件
  • 程序:即编译器编译生成的可执行程序
  • 进程:可执行程序通过命令(./a.out)开始运行,此时正在运行的程序以及占用的资源叫做进程。
    【注意:进程这个概念是针对系统而不是针对用户,对用户来说面对的概念是程序;而一个程序可以执行多次,表明多个进程可以执行同一个程序】

进程空间内存分布

Linux进程内存管理的对象都是虚拟内存,每个内存都会有0-4G的各自互不干涉的虚拟内存空间;
0-3G是用户空间执行用户自己的代码;高1GB的空间是内核空间执行Linux系统调用,这里存放着整个内核的代码和所有的内核模块;用户所看到和接触的都是该虚拟地址,而不是实际的物理内存地址。
Linux下一个进程在内存中有三部分的数据:代码段、堆栈段、数据段

  • 代码段:即存放程序代码的数据

  • 堆栈段:存放子程序的返回地址、子程序的参数、程序的局部变量和malloc()动态申请内存的地址

  • 数据段:存放程序的全局变量,静态变量及常量的内存空间
    Linux下进程的内存布局:
    Linux下进程的内存分布

  • 栈:栈内存由编译器在程序编译阶段完成,进程的栈空间位于进程用户空间的顶部并且是向下增长;每个函数的每次调用都会在栈空间中开辟自己的栈空间,函数参数、局部变量、函数返回地址等都会按照先入者为栈顶的顺序压入函数栈中;函数返回后该函数的栈空间消失,所以函数中返回局部变量的地址都是非法的

  • 堆:堆内存是在程序执行过程中分配的,存放程序运行中被动态分配的变量,大小不固定,堆位于非初始化数据段和栈之间,并且使用过程中是向栈空间靠近的。当进程调用 malloc 等函数分配内存时,新分配的内存并不是该函数的栈帧中,而是被动态添加到堆上,此时堆就向高地址扩张;当利用 free 等函数释放内存时,被释放的内存从堆中被踢出,堆就会缩减。因为动态分配的内存并不在函数栈帧中,所以即使函数返回这段内存也是不会消失。

Linux内存管理基本思想:

只有在正在访问一个地址的时候才建立该地址的物理映射。

Linux C/C++语言的分配方式(3种)

  1. 从静态存储区域分配;及数据段是内存分配,该段内存在程序编译阶段已分配好,在程序的整个运行期间都存在,例如:全局变量、static变量。
  2. 在栈上创建;在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是系统栈中分配的内存容量有限,比如大额数组就会把栈空间撑爆导致段错误。
  3. 从堆上分配,亦称动态内存分配;序在运行的时候用 malloc 或 new 申请任意多少的内存,程序员自己负责在何时用free 或 delete 释放内存。此区域内存分配称之为动态内存分配。动态内存的生存期由程序员决定,使用非常灵活,但问题也最多,比如指向某个内存块的指针取值发生了变化又没有其他指针指向这块内存,这块内存就无法访问,发生内存泄露

fork()系统调用——创建一个新的子进程

pid_t fork(void);

fork()系统调用会创建一个新的进程,此时它会有两个返回(return);一次返回是给父进程的,返回值是子进程的PID,第二次返回值给子进程,返回值是0。

  • 返回值>0说明是父进程在运行
  • 返回值=0说明是子进程在运行
  • 返回值<0说明fork()系统调用错误

·fork()系统调用失败的原因:

  1. 系统中已经有太多是进程
  2. 该实际用户ID的进程总数超过了系统限制

每个进程可以通过getpid()获取自己的进程PID,也可以通过getppid()获取父进程的PID;
每个子进程只有一个父进程,但一个进程可以创建多个子进程;故对父进程而言,它并没有一个API函数可以获取子进程的进程ID,所以父进程在通过fork()创建子进程的时候,必须通过返回值的形式告诉父进程其创建的子进程PID。这也是fork()系统调用两次返回值设计的原因。
创建子进程:

int main (int argc, char **argv)
{
	pid_t  pid;
	pid = fork();//创建子进程
	if (pid < 0)
	{
	printf("fork() create a child process failure: %s\n", strerror(errno));
	return -1;
	}
	else if (0 == pid)
	{
	printf("Child process PID[%d] start running, my parent PID is [%d]", getpid(),getppid());
	return 0;//子进程退出
	}
}

注意:在编程时任何位置的exit()函数调用都会导致本进程(程序)退出,main()函数中的return()调用也会导致进程退出,而其他任何函数中的return()都只是这个函数返回而不会导致进程退出。】
fork()系统调用创建一个新的进程,子进程相当于父进程的副本,即系统在创建新的子进程成功后,会将父进程的文本段、数据段、堆栈都复制一份给子进程,但子进程有自己独立的空间;所以,进程创建后父子进程对各自内容的修改不会影响到对方
父子进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。 如果需要确保让父进程或子进程先执行,则需要程序员在代码中通过进程间通信的机制来自己实现。

exec*()——执行另一个程序

fork()之后紧接着调用exec*()系列函数可让子进程去执行另外一个程序,exec*()是一系列函数,其原型为:

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

上述函数名中

  • l表示以列表(list)的形式传递要执行程序的命令行参数;
  • v表示以数组(vector)的形式传递要执行程序的命令行参数;
  • e表示给命令传递环境变量(environment)

在上述众多函数中,一般使用第一个函数execl()

int execl(const char *pathname, const char *arg, .../* (char  *) NULL */);
  • 参数pathname表示所要执行程序的路径
  • 参数arg表示命令及其相关选项、参数;其中,每个命令、选项、参数都用双引号(“”)括起来,并以NULL结束
    【注意:excel()并不会返回,因为它执行到下一个程序中去了,若返回了,则说明该系统调用出错】

创建子进程后执行ifconfig eth0命令参考如下:

elect("/sbin/ifconfig", "ifconfig", "eth0", NULL);

ifconfig命令(程序)的路径是:/sbin/ifconfig
【额外补充:
fork()用到了虚拟地址,当使用单片机跑Linux时,因为CPU没有MMU,所以不能跑标准Linux,只能跑UcLinux;即只能调用vfork()

  • MMU 内存管理单元:将虚拟地址映射到物理地址
  • MPU 内存保护单元】

vfork() 系统调用

在fork()之后常会紧跟着调用exec来执行另外一个程序,而exec会抛弃父进程的文本段、数据段和堆栈等并加载另外一个程序,所以现在的很多fork()实现并不执行一个父进程数据段、堆和栈的完全副本拷贝。作为替代,使用了写时复制(CopyOnWrite)技术: 这些数据区域由父子进程共享,内核将他们的访问权限改成只读, 如果父进程和子进程中的任何一个试图修改这些区域的时候,内核再为修改区域的那块内存制作一个副本。
vfork()是另外一个可以用来创建进程的函数,他与fork()的用法相同,也用于创建一个新进程。 但vfork()并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec或exit(),于是也就不会引用该地址空间了。不过子进程在调用exec()或exit()之前, 他将在父进程的空间中运行,但如果子进程想尝试修改数据域(数据段、堆、栈)都会带来未知的结果,因为他会影响了父进程空间的数据可能会导致父进程的执行异常。
此外, vfork()会保证子进程先运行,在他调用了exec或exit()之后父进程才可能被调度运行。如果子进程依赖于父进程的进一步动作,则会导致死锁

pid_t vfork(void);

wait()、waitpid()

当一个进程正常或异常退出时,内核就会向其父进程发送SIGCHLD信号。
父进程可以调用wait()或waitpid()查看子进程退出的状态。

pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);

注:wait()函数为阻塞函数。
如果一个已经终止、但其父进程尚未对其调用wait进行善后处理(获取终止子进程的有关信息如CPU时间片、释放它锁占用的资源如文件描述符等)的进程被称僵死进程(zombie),ps命令将僵死进程的状态打印为Z。
【ps aux 命令查看进程状态】
如果父进程在子进程退出之前退出了,这时候子进程就变成了孤儿进程。孤儿进程最终会被init进程“领养”,使其父进程为init进程。

system()与popen()函数

system()库函数可以快速创建一个进程执行相应的命令

int system(const char *command);

popen()可以执行一条命令,并返回一个基于管道(pipe)的文件流,若想获取结果中的某已数据事则可以从该文件流中一行行解析

FILE *popen(const char *command, const char *type);
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

园园顺顺崽

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

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

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

打赏作者

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

抵扣说明:

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

余额充值