Linux进程控制

Posted 北川_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux进程控制相关的知识,希望对你有一定的参考价值。

目录

进程创建

进程创建的两种最常见的场景:
1.命令行启动命令(程序、指令等)
2.通过程序自身,fork出来子进程

fork函数初识

在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。

#include <unistd.h>
pid_t fork(void);	
//返回值:自进程中返回0,父进程返回子进程id,出错返回-1

所有的fork出来的子进程,以父进程为模板(很多数据,代码继承父进程)
进程调用fork,当控制转移到内核中的fork代码后,内核做:

  • 分配新的内存块和内核数据结构给子进程
  • 将父进程部分数据结构内容拷贝至子进程
  • 添加子进程到系统进程列表当中
  • fork返回,开始调度器调度

创建子进程,本质是多了一个进程,多了一个进程本质是多了一套进程相关的数据结构。而这套数据结构初始化以父进程为模板。

fork之前父进程独立执行,fork之后,父子两个执行流分别执行。fork之后,谁先执行完全由调度器决定。

fork函数返回值

子进程返回0
父进程返回的是子进程的pid
出错返回-1

#include<stdio.h>
#include<unistd.h>

int main()

    int a = 10;
    int n = 3;
    while(n)
    
        printf("father, pid:%d, ppid:%d, a:%d\\n", getpid(), getppid(), a);
        sleep(1);
        --n;
    
    pid_t pid = fork();
    if(pid == 0)
    
        a = 20;
        while(1)
        
            printf("child,  pid:%d, ppid:%d, a:%d\\n", getpid(), getppid(), a);
            sleep(1);
        
    
    else if(pid > 0)
    
        while(1)
        
            printf("father, pid:%d, ppid:%d, a:%d\\n", getpid(), getppid(), a);
            sleep(1);
        
    
    else
    
        perror("fork");
    
    return 0;


上面代码让父进程打印三次,然后fork创建子进程,子进程将a的值改为20,并且父子进程同时打印。
运行结果:

一个pid变量里面,怎么会有两个不同的值?从而让父子进入不同的业务逻辑
pid是父进程创建的一个变量,父进程执行fork完成创建子进程的一系列动作。fork函数最终将返回值return给pid变量,返回的时候本质是把返回值写入变量,这时候子进程已经存在了,此时必定会发生写时拷贝。一个变量名,内容是不同的,本质是父子页表映射数据到了不同的物理内存区域。
为什么要发生写时拷贝 → \\rightarrow 保证父子进程的“独立性”

写时拷贝

上面代码的运行结果可以看到,一个变量a竟然有两个值,子进程修改数据并没有影响父进程,这个过程是怎样实现的呢?

数据修改之前,父子进程的代码和数据都映射到物理内存的同一位置。当父进程或子进程想要修改内存区域的某些数据时,发现页表里设置的父子的读写权限都是只读的,这时候会出现错误,这个错误是系统层面的,os在发现错误后,重新在物理内存当中开辟一段空间,把老的数据拷贝过来,修改页表的映射关系,并且去掉刚刚的只读权限而设置成默认的既有读又有写,此时父和子就完成了读写分离,这就是写时拷贝的过程。
为什么要有写时拷贝?父子进程创建的时候,直接把数据各自拷贝一份不就完了?
1.父进程和子进程所有的数据,并不是父和子都需要写入,甚至子进程压根就不会对父进程的数据做任何修改。不需要写入修改的数据(只读),这样的数据拷贝是没有意义的,拷贝了会浪费系统空间和资源。
2.fork时,创建与进程相关的数据结构,如果还要将数据拷贝一份,会导致fork效率的降低。
3.fork创建子进程本身就是向系统要资源,比如说内存资源,要更多的资源更容易导致fork失败。

fork常规用法

1.一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
2.一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。

fork调用失败的原因

1.系统中有太多的进程
2.实际用户的进程数超过了限制

进程终止

进程退出的情况分类

为什么main函数中,总是return 0?
main函数的返回值代表进程退出,结果是否运行正确,main函数的返回值专业的说法叫做进程的退出码

  1. 代码跑完,结果正确。退出码为0
  2. 代码跑完,结果不正确。逻辑有问题,但是没有导致程序崩溃,退出码 !0
  3. 代码没有运行完毕,程序崩溃了,比如野指针、除0错误,越界,退出码没有意义。

使用echo $?可以查看最近一次执行的程序的退出码

退出码

退出码,可以人为的定义,也可以使用系统的错误码list。
当程序运行失败的时候,我们最关心:为什么失败?失败原因。计算机擅长处理整数类型的数据,所以才有了0、1、2…这样的退出码。可是人擅长处理字符串描述的具有文字含义的英文或中文组合。所以需要将整数转换成错误码描述信息。
打印错误码对应的错误描述信息:

#include<stdio.h>
#include<string.h>

int main()

    for(int i = 0; i < 134; ++i)
    
        printf("%d : %s\\n", i, strerror(i));
    
    return 0;

打印结果(部分):

进程常见退出方法

正常终止进程操作:
1.main函数return
2.任何函数exit或_exit
非main函数的return不是终止进程,而是结束函数

_exit函数

#include <unistd.h>
void _exit(int status);
//参数:status 定义了进程的终止状态,父进程通过wait来获取该值

说明:虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现返回值是255。

exit函数

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

exit最后也会调用_exit, 但在调用_exit之前,还做了其他工作:

  1. 执行用户通过 atexit或on_exit定义的清理函数。
  2. 关闭所有打开的流,所有的缓存数据均被写入
  3. 调用_exit

_exit和exit的区别

exit:在退出的时候,会进行后资源处理,包括刷新缓冲区
_exit:在退出的时候,不会进行后序的资源处理,直接终止进程

站在OS角度,如何理解进程终止?
核心思想:归还资源
1.“释放” 曾经为管理进程所维护的所有的数据结构对象
2.释放程序代码和数据占用的内存空间(不是代码和数据清空,而是把内存设置为无效就可以了)
3.取消曾经该进程的连接关系
释放:不是真的把数据结构对象销毁,而是设置为不用状态,然后保存起来,如果不用的对象多了,就有一个“数据结构的池”。

进程等待

进程等待必要性

1.子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
2.另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
3.父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
4.父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。
为什么要等待?
1.回收僵尸,解决内存泄漏
2.需要获取子进程的运行结束状态 - 不是必须的
3.尽量保证父进程要晚于子进程退出,可以规范化进行资源回收 - 编码

进程等待的方法

wait方法

等待任意一个子进程,当子进程退出,wait就可以返回。

#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
//返回值:成功返回被等待进程pid,失败返回-1
//参数:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

waitpid方法

pid_ t waitpid(pid_t pid, int *status, int options);
/*
返回值:
	当正常返回的时候waitpid返回收集到的子进程的进程ID;
	如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
	如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
	pid:
		Pid=-1,等待任一个子进程。与wait等效。
		Pid>0.等待其进程ID与pid相等的子进程。
	status:
		WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
		WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
	options:
		WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
		options参数填0表示阻塞等待,填WNOHANG表示非阻塞等待
*/
  • 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
  • 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
  • 如果不存在该子进程,则立即出错返回。

获取子进程status

  • wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
  • 如果传递NULL,表示不关心子进程的退出状态信息。
  • 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
  • status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图:


waitpid 拿到的status的值,是从哪里拿到的呢?子进程已经结束了啊
子进程退出时,它会进入僵尸z状态,相关的部分资源可以被释放,但它的pcb结构必须被保存,因为要将自己的退出信息暂时保存至pcb也就是task_struct结构体中供父进程通过wait或waitpid来进行读取。
内核源代码进程pcb关于进程退出码的信息:

进程替换

替换原理

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。一句话概括:通过exec*,让特定进程去加载磁盘中的其他程序,以达到运行的目的,期间不创建新的进程。

替换函数

有六种以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[]);

execl举例:

execl("/usr/bin/ls", "ls", "-a", "-l", NULL);

第一个参数表示要执行程序的路径,第二个参数是可变参数列表,想怎样执行程序,命令行上怎么写,这里就怎么写,最后必须以NULL结尾,告知exec*参数传入完毕。
execv举例:

char* const my_argv[] = 
		"ls",
		"-a",
		"-l",   
		NULL	                                                                          		  	    
	;
execv("/usr/bin/ls", my_argv);

函数解释

  • 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
  • 如果调用出错则返回-1
  • 所以exec函数只有出错的返回值而没有成功的返回值。

命名理解

这些函数原型看起来很容易混,但只要掌握了规律就很好记。

  • l(list) : 表示参数采用列表
  • v(vector) : 参数用数组
  • p(path) : 有p自动搜索环境变量PATH
  • e(env) : 表示自己维护环境变量
函数名参数格式是否带路径是否使用当前环境变量
execl列表
execlp列表
execle列表否,需自己组装环境变量
execv数组
execvp数组
execve数组否,需自己组装环境变量

调用关系

事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册 第2节,其它函数在man手册第3节。
下图exec函数族之间调用关系:

以上是关于Linux进程控制的主要内容,如果未能解决你的问题,请参考以下文章

Linux进程控制

<Linux>进程控制

Linux --进程控制

Linux进程控制

Linux进程控制

进程控制(Linux)