Linux进程控制
本文主要介绍了Linux进程管理的几个核心概念:进程创建(fork)、终止、等待和程序替换。fork函数通过复制父进程创建子进程,父子进程共享代码段但数据段采用写时拷贝机制。进程终止分为正常退出(exit/_exit)和异常终止,exit会刷新缓冲区而_exit直接终止。进程等待(wait/waitpid)用于回收子进程资源并获取退出状态,避免僵尸进程。程序替换(exec系列函数)可以在不创建新进
一、进程创建
fork(创建进程)
Linux中fork函数是非常重要的函数,它从已存在进程中创建一个新进程。
新进程为子进程,而原进程为父进程
#include<unistd.h>
pid_t fork(void);
返回值:子进程返回0,父进程返回子进程pid,出错返回-1
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表当中
- fork返回,开始调度器调度
一个进程调用fork之后,会有两个二进制代码相同的进程,而且它们都运行到相同的地方
但每个进程都将可以开始属于它们自己的旅程
#include<stdio.h>
#include<unistd.h>
#include <stdlib.h>
#include<sys/types.h>
int main()
{
pid_t pid;
printf("Before: pid is %d\n", getpid());
if ( (pid=fork()) == -1 ) perror("fork()"),exit(1);
printf("After:pid is %d, fork return %d\n", getpid(), pid);
sleep(1);
return 0;
}
输出结果
进程1348063先打印before消息,然后它再打印after。另一个after消息有1348064打印的但是意到进程1348064没有打印before,为什么呢?

fork之前父进程独立执行,fork之后,父子两个执行流分别执行注意,fork之后,谁先执行完全由调度器决定
写时拷贝
父子代码共享,父子在不写入时,数据也是共享的当任意一方试图写入,便以写时拷贝的方式各自一份副本

当父进程形成子进程之后,子进程正在写入,如何进行写时拷贝?
重新申请空间,进行拷贝,修改页表(os)
原因:父进程创建子进程的时候首先将自己的读写权限,改为只读,然后再创建子进程,但是用户他是不知道的,用户可能会对某一些数据进行写入,这时页面转换会因为权限问题报错,不过不是真的出错,而是触发我们进行申请内存拷贝内容的策略机制,然后操作系统就可以介入了
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段
- 例如,父进程等待客户端请求,生成子进程来处理请求
- 一个进程要执行一个不同的程序,例如子进程从fork返回后,调用exec函数
- 系统中有太多的进程
- 实际用户的进程数超过了限制
二、进程终止
进程退出场景
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止
进程常见退出方法
- 从main返回


- 在main函数直接return
- 在其他函数中进行return,表示的是函数调用结束
- main函数返回值,叫做进程的退出码(0表示成功,非0表示失败)
- 调用exit


从上述代码看出printf没有实现可以说明任意地点调用exit,表示进程退出,不进行后续执行
参数就是进程的退出码,类似于main return n
- _exit
ctrl + c,信号终止
#include <unistd.h>
void _exit(int status);
参数:status 定义了进程的终止状态,父进程通过wait来获取该值
说明:虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现返回值
是255。
#include <unistd.h>
void exit(int status);
- 执行用户通过 atexit或on_exit定义的清理函数
- 关闭所有打开的流,所有的缓存数据均被写入
- 调用_exit

用例:
int main()
{
printf("aaaaaa");
exit(31);
}

int main()
{
printf("aaaaaa\n");
_exit(31);
}

- 1.exit是库函数,_exit是系统调用
- 2.exit终止进程的时候,会自动刷新缓冲区。_exit终止进程的时候,不会自动刷新缓冲区
三、进程等待
什么是进程等待
通过wait/waitpid的方式,让父进程(一般)对子进程进行资源回收的等待过程
为什么要进行等待
- a.解决子进程僵死问题带来的内存泄露
- b.父进程为什么要创建子进程?要让子进程来完成任务。子进程任务完成的如何,父进程要不要知道?要知道的话需要通过进程等待的方式,获取子进程退出的信息,虽然不是必须的,但是系统需要提供这样的基础功能
进程等待必要性
子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。另外,进程一旦变成僵尸状态,那就kill -9 也无法解决,因为谁也没有办法杀死一个已经死去的进程。 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行结果对还是不对,或者是否正常退出。父进程通过进程等待的方式,回收子进程资源获取子进程退出信息
进程等待的方法
wait
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
waitpid
pid_ t waitpid(pid_t pid, int *status, int options);
WNOHANG是waitpid 函数的一个选项,表示非阻塞模式
WNOHANG的本质是提供了一个非阻塞的(non-blocking)选项,使得父进程可以以“轮询”的方式检查子进程的状态,而不会因为等待子进程而停止自身的执行。 这对于提高程序的响应性和并发处理能力至关重要
返回值当正常返回的时候waitpid返回收集到的子进程的进程ID如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在
pid
- pid=-1,等待任一个子进程。与wait等效
- pid>0.等待其进程ID与pid相等的子进程
status
- WIFEXITED(status): 若正常终止子进程返回的状态则为真(查看进程是否是正常退出)
- WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码(查看进程的退出码)
optionsWNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
#include<stdio.h>
#include <stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
void work()
{
int cnt=5;
while(cnt)
{
printf("I am child process,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt);
cnt--;
}
}
int main()
{
pid_t id=fork();
if(id==0)
{
work();
exit(0);
}
else{
printf("wait before\n");
pid_t rid=wait(NULL);
printf("wait after\n");
if(rid==id)
{
printf("wait success,pid:%d\n",getpid());
}
}
sleep(10);
}
- rid>0:等待成功
- rid==0:等待是成功的,但是可能对方没有成功
- rid<0:等待失败
从结果可以知道子进程没有退出,父进程就必须在wait上进行阻塞等待,直到子进程僵尸,wait自动回收返回,也就是说wait after必须等待子进程结束才会执行父进程代码
- 如果子进程已经退出,调用wait/waitpid时,会立即返回并且释放资源获得子进程退出信息。
- 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
- 如果不存在该子进程,则立即出错返回
获取子进程status
- wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
- 如果传递NULL,表示不关心子进程的退出状态信息。
- 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
- status不能简单的当作整形来看待,可以当作位图来看待
exit sig(退出信号):status&0x7F exit code(退出码):(status->8)&0xFF
只考虑status低16比特位,次8位子进程退出的退出码,低7位代表子进程退出的推出信号
1.那为什么不用全局变量获取子进程退出信息?而是系统调用
进程具有独立性,父进程无法直接获取子进程退出信息
2.当一个进程异常了(收到信号exit sig),exit code就无意义了
如何判断有没有收到信exit sig=0
四、进程程序替换
替换原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变

替换函数
六种以exec开头的函数,都称为exec函数
#include <unistd.h>`
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ...,char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
函数解释
- 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回
- 如果调用出错则返回-1
- 所以exec函数只有出错的返回值而没有成功的返回值
命名理解

其实这六个函数所有的接口都是对execvec的封装,底层都是execvec
下面看一下exec函数的使用,拿execl来说

path:要替换哪一个程序->文件->程序文件的路径+文件名
arg:如何执行问题?命令行怎么写->就将参数怎么传


发现最后一个printf没有实现这是怎么回事?
原因:exec*这样的函数,如果当前进程执行成功,则后续代码没有机会再执行,因为被替换掉了
1.进行程序替换的时候,子进程对应的环境变量,是可以直接从父进程来的?
父进程添加环境变量会被子进程继承下去
2.环境变量被子进程继承下去是一种默认行为,不受程序替换影响,为什么呢?
通过地址空间可以让子进程继承父进程的环境变量数据
更多推荐


所有评论(0)