Unix进程
文章目录
- 命令行参数
- 进程终止
- 正常结束
- 异常终止
- exit和_exit
- atexit
- 环境变量
- 环境变量性质
- 环境表
- shell中操作环境变量
- 查看环境变量
- 设置环境变量
- 环境变量接口
- 获取环境变量
- 设置环境变量
- 环境变量的继承性
- 进程资源
- shell命令查看进程的资源限制
- 进程关系
- 进程标识
- 进程组
- 会话
- 控制终端
- 控制终端ID
- 前后台进程切换命令
- jobs
- bg
- fg
- 精灵进程(守护进程)
- 闲逛进程
- 进程创建
- fork与写时复制
- 进程等待
- wait、waitpid
- statloc参数详解
- 僵尸进程与孤儿进程
- 进程替换
- exec函数解析
- exec用户ID问题
命令行参数
ISO C标准的main函数具有2个参数,分别是argc和argv,它们标识==命令行参数的个数和命令行参数数组==,所谓命令行参数,就是通过命令行启动一个程序时所传入的参数。内核启动一个程序时调用一个特殊的例程——程序入口点进行命令行参数和环境变量的初始化,随后调用main函数执行用户逻辑。
./a.out 这是执行一个可执行程序最简单的方式,命令行参数个数为1,参数即./a.out
./a.out xxx yyy 命令行参数个数为3,参数分别为./a.out xxx yyy
命令行参数需要以空格作为分割符,shell会以空格为单位解析命令行参数。当程序运行时,它可以选择读取命令行参数进行特定的任务处理。
//简单加法器的实现
#include <stdio.h>
#include <stdlib.h>
int main(int argc,char* argv[]){
if(argc<3){printf("请至少输入2个操作数\n");return 0;}
int res=0;
for(int i=1;i<argc;++i){ //注意从下标为1处开始读取
res+=atoi(argv[i]);
}
printf("求和的结果是%d\n",res);
return 0;
}
运行效果展示
ISO C标准规定argv[argc]必须为NULL标识结束,因此for(int i=1;i<argc;++i) <==> for(int i=1;argv[i]!=NULL;++i)
进程终止
程序入口点在调用main函数后等待main函数返回,并进行结果处理。在用户源代码中通过main函数return来表示进程的结束。其实想要让一个进程结束的方式不只有在main函数中return。Unix环境下一共有8种进程终止的方式,其中5种是正常结束,3种是异常终止
正常结束
- main返回
- exit调用
- _exit调用
- 最后一个线程返回
- 最后一个线程pthread_exit调用
异常终止
- 调用abort
- 收到默认处理动作为终止进程的信号
- 最后一个线程对取消请求做出相应
本文只讨论正常结束的前3种情况,请注意main函数返回和exit调用是等价的
exit和_exit
exit是ISO C标准提供的一个接口,在程序的任意处调用exit函数都会导致进程结束;_exit是POSIX标准提供的一个接口,它是一个系统调用,会直接进入内核,在程序的任意处调用也会导致进程结束。
exit是对_exit的封装,相较于其的区别在于exit在进入内核之前会先调用一系列终止处理程序,其中包括刷新所有的C标准缓冲区。
exit和_exit都有一个整型类型的参数用于标识进程退出状态,宏常量EXIT_SUCCESS(扩展到0)标识进程正常结束且结果正确,EXIT_FAILURE(扩展到1)标识进程正常结束但结果错误
int exit(int status);
int _exit(int status);
atexit
可以通过atexit函数来自定义终止处理程序,执行exit时会按照atexit注册的函数逆向逐个调用。atexit必须传入一个无参数无返回值的函数
int atexit(void (*func)());
#include <stdio.h>
#include <stdlib.h>
void my_handler1(){printf("my_handler1\n");}
void my_handler2(){printf("my_handler2\n");}
void my_handler3(){printf("my_handler3\n");}
int main(){
atexit(my_handler1);
atexit(my_handler2);
atexit(my_handler3);
return 0;
}/*调用次序3->2->1*/
环境变量
环境变量是操作系统层面的一种机制,用于向运行中的进程提供配置信息。它们是一组动态的键值对(key=value),可以在程序启动时或运行过程中被读取,从而影响程序的行为
环境变量性质
全局性:环境变量通常是全局的,意味着它们可以被任何进程访问,除非被明确地限制在一个特定的进程中。
动态性:环境变量可以在程序运行期间被更改,这使得程序可以根据环境的变化调整其行为。
继承性:当一个进程创建子进程时,子进程会继承父进程的环境变量副本,除非另有指定。
环境表
每一个进程都会收到一张环境表,它是一个字符指针数组,以NULL代表结束,全局变量environ指向环境表
我们可以通过遍历environ所管理的环境表来获取所有的环境变量
#define _GNU_SOURCE
#include <unistd.h>
#include <stdio.h>
int main(){
for(int i=0;environ[i]!=NULL;++i){
printf("%s\n",environ[i]);
}
return 0;
}
部分Unix系统对main函数做出了扩展,允许传入第三个参数标识环境表(Linux就支持)
int main(int agrc,char* argv[],char* env[]);
也可以通过遍历env来获取所有的环境变量
#include <stdio.h>
int main(int argc,char* argv[],char* env[]){
for(int i=0;env[i]!=NULL;++i){
printf("%s\n",env[i]);
}
return 0;
}
2种方式效果等价
shell中操作环境变量
以Linux为例
查看环境变量
- echo ${KEY} ::=查看指定的环境变量值
- env\printenv ::=查看所有的环境变量值
设置环境变量
- export {KEY}={VALUE} ::=设置会话级别的环境变量,当会话关闭之后失效
环境变量接口
获取环境变量
char* getenv(const char* key);
如果存在环境变量key则返回对应的值,否则返回一个空指针
设置环境变量
int putenv(const char* kv);
int setenv(const char* key,const char* value,int mode);
int unsetenv(const char* name);
- putenv无则新增,有则更新
- setenv根据mode决定对已存在的环境变量的处理方式。非0则更新,0则保留
- unsetenv删除环境变量
进程所设置的环境变量只对当前进程或者其子进程有效,并不影响其他进程
环境变量的继承性
putenv可以被子进程所继承,在CGI机制中往往父进程可以通过设置环境变量的方式向子进程传递必要的参数。
//环境变量继承性实验
int main(){
putenv("OPEARND1=1000");
putenv("OPEARND2=1000");
pid_t pid=fork(); //表示创建一个子进程
if(pid==0){
printf("计算结果%d\n",atoi(getenv("OPEARND1"))+atoi(getenv("OPEARND2")));
}
else{
printf("父进程设置环境变量完毕\n");
}
return 0;
}
进程资源
任何一个进程都具有一定的资源限制,例如占用CPU的最大时间,数据段的最大长度,文件最大字节数,打开文件上限等等,有些业务逻辑需要受限至于进程的资源限制,这时候查询和修改进程资源限制就显得很有必要。XSI扩展了2个接口用于查询和修改进程资源限制
#include <sys/time.h>
#include <sys/resource.h>
int getrlimit(int resource,struct rlimit* rlptr);
int setrlimit(int resource,const struct rlimit* rlptr);
typedef long unsigned int rlim_t;
struct rlimit{
rlim_t rlim_cur; //称为软限制,标识当前进程的限制值
rlim_t rlim_max; //称为硬限制,标识当前进程限制值的最大值
};
对于进程资源的修改操作并不是随意进行的,它必须遵守如下规则
- root权限进程可以提高硬限制
- 普通进程可以灵活调整软限制(不可超过硬限制) 和 降低硬限制(不可逆,调低了就回不去了)
eg
/*这是一个普通进程*/
#include <sys/time.h>
#include <sys/resource.h>
#include <stdio.h>
int main(){
struct rlimit rl;
getrlimit(RLIMIT_CORE,&rl); //获取当前进程核心转储文件最大值
printf("soft rlimit_core:%lu\n",rl.rlim_cur);
printf("hard rlimit_core:%lu\n",rl.rlim_max);
getrlimit(RLIMIT_CPU,&rl); //获取当前进程占用CPU时间最大值
printf("soft rlimit_cpu:%lu\n",rl.rlim_cur);
printf("hard rlimit_core:%lu\n",rl.rlim_max);
rl.rlim_cur=100;
setrlimit(RLIMIT_CPU,&rl); //ok
rl.rlim_max=101;
setrlimit(RLIMIT_CPU,&rl); //ok 普通进程可以降低硬限制
/*-----------------------------------------------------------------------*/
rl.rlim_cur=200;
setrlimit(RLIMIT_CPU,&rl); //error 软限制不可大于硬限制
rl.rlim_max=200;
setrlimit(RLIMIT_CPU,&rl); //error 普通进程不可抬高硬限制(之前降低过调不回去)
return 0;
}
shell命令查看进程的资源限制
prlimit --pid={PROCESS_ID} --output=DESCRIPTION,RESOURCE,SOFT,HARD
进程关系
进程标识
内核需要管理每一个进程,需要通过一个唯一标识符来确定进程。进程的唯一标识符为称为==进程ID(PID),进程ID是一个非负整数,同一时刻不会存在进程ID相同的2个进程;每一个进程都有唯一的父进程,通过父进程ID(PPID)==所指示*(0号进程没有父进程,它是系统启动的第一个进程,负责进程调度,属于内核级进程)*;除此之外,一个进程还有许多标识信息,例如真实用户ID(UID),有效用户ID(EID),真实组ID(GID),有效组ID(EGID)。并且都具有相应的系统调用可以用于获取调用进程的相关标识信息。
#include <unistd.h>
pid_t getpid();
pid_t getppid();
pid_t getuid();
pid_t geteuid();
pid_t getgid();
pid_t getegid();
//
pid_t getresuid();
pid_t getresgid(); //获取保留的用户ID和组ID,这2个接口不具备可移植性,但是Linux中有提供
进程组
多个进程可以构成一个进程组,通过进程组可以高效管理多个进程。每一个进程组都有一个组长进程,组长进程的进程ID等于进程组组ID==(PID=GID)==。在shell中可以通过管道连接符同时启动多个进程,这些进程会被归置为一组。
pid_t getpgrp(); //获取GID
进程组的生命周期随进程组中最后结束的进程,即只要进程组中存在一个进程,进程组就会存在。非组长进程可以加入别的进程组或者创建一个进程组自己成为组长。
int setpgid(pid_t pid,pid_t pgid);
//一个进程可以调用setpgid加入或创建一个进程组
//pid一般传入0标识进程自身
会话
多个进程组成一个会话。进程可以通过调用setsid来创建一个新的会话,但是这个进程不能是组长进程(组长进程调用setsid会失败);如果调用成功,则调用进程会成为新会话里的唯一进程(会话首进程),会话也有标识即会话ID(SID),会话ID等于会话首进程ID
int setsid();
pid_t getsid(pid_t pid);
getsid返回的是目标进程的进程组ID,而非会话ID,并且getsid函数作用域被限制在会话中,即一个进程通过getsid获得其他会话的进程组信息会失败
控制终端
一个会话可以具有一个控制终端,通过xshell远程登录Linux时其实就是建立一个带有控制终端的会话,用户可以通过控制终端与主机进行交互。
带有控制终端的会话中进程组可以被划分为前台进程组和后台进程组。其中前台进程组有且仅有一个,键盘发送信号就是发给前台进程组的(CTRL+C会强制结束当前会话的所有前台进程)
一个非组长进程调用setsid创建新会话后默认没有控制终端。
控制终端ID
ps命令中有一个字段为TPGID,即控制终端ID,一般TPGID都不固定的。我们可以通过TPGID来判断一个进程是否属于前台进程,对于前台进程它的TPGID是非负的,而对于后台进程其TPGID为-1
前后台进程切换命令
jobs
jobs命令可以查看==当前会话==的后台进程信息,结果以组为单位呈现
bg
bg命令可以将==一组==被CTRL+Z暂停的前台进程挂到后台继续运行
bg %{jobs_no}
fg
fg命令可以将==一组==后台进程挂到前台继续运行
fg %{jobs_no}
精灵进程(守护进程)
精灵进程也称为守护进程,是一种长时间运行的后台进程。它们通常在系统启动时启动,并持续运行直到系统关闭。精灵进程不与用户直接交互,而是执行特定的服务或任务。
特点
- 没有控制终端,通常在后台运行
- 生命周期长,通常从系统启动到关闭一直存在
- 通常作为系统服务运行,提供网络服务、定时任务等功能
- 创建时会调用fork()和setsid()等函数,确保自己独立于启动它的终端
- 通常会将当前工作目录更改为根目录,避免占用挂载的文件系统
- 设置文件权限掩码,确保创建的文件具有适当的权限
- 关闭不需要的文件描述符,避免占用系统资源
闲逛进程
闲逛进程也称为空闲进程或调度进程,是操作系统中的一个特殊进程。当系统中没有其他可运行的进程时,调度器会将CPU的控制权交给闲逛进程。闲逛进程的主要目的是确保CPU不会空转,从而防止CPU进入不受控制的状态。
特点
- 具有最低的优先级,只有当系统中没有其他可运行的进程时才会被调度
- 主要任务是让CPU保持忙碌状态,即使这种“忙碌”实际上是空闲的
- 通常执行一个简单的循环,使CPU进入低功耗状态
- 不消耗实际的计算资源,只是占用了CPU的时间片
- 在多处理器系统中,每个CPU核心通常都有一个对应的闲逛进程
进程创建
fork与写时复制
一个进程可以通过fork创建子进程,子进程会继承父进程的进程组ID(即子进程和父进程属于同一个进程组),内核会为fork生成的子进程生成一份父进程的地址空间快照,并且在子进程不进行数据修改操作时,子进程与父进程共享同一块物理内存;如果子进程对数据发生修改操作,则内核会建立被修改的数据的副本以确保子进程不会影响父进程,这种机制成为写时复制技术。
值得注意的是,子进程会继承父进程的文件描述符表(重定位信息也会得到继承),并且==与父进程共享一个文件表项----->共享文件偏移量==,这意味着如果父子进程同时往一个文件中写入数据可能出现数据错乱的情况;但这个特性也有其优势,如果子进程向标准输出打印了一串信息后退出,假设父进程等待子进程结束,那么它能很自然得(不需要复杂的控制)将一些子进程的退出状态输出到标准输出上(父进程的输出信息一定是在子进程打印的信息之后的)
进程等待
当一个进程正常或异常终止时,内核向其父进程发送SIGCHID信号,告诉父进程其子进程已经结束。父进程需要在合适的时机调用系统API获取子进程的退出信息并回收已经结束的子进程资源(子进程的大部分资源已经被内核回收,但是内核仍会保留子进程的进程控制块信息,因为这里面存储了子进程的退出信息)。标准为我们提供了wait和waitpid两个接口用于进程等待,进程调用wait和waitpid函数默认陷入阻塞直到子进程返回。
wait、waitpid
#include <sys/wait.h>
pid_t wait(int* statloc);
pid_t waitpid(pid_t pid,int* statloc,int options);
***wait:***阻塞等待任意子进程,如果不关心子进程的退出信息,参数写NULL,否则子进程的退出信息会被写入到statloc所指向的空间。
**waitpid:参数statloc与wait一致,默认为阻塞等待,如果将options参数设置为WNOHANG则为非阻塞等待。
waitpid的pid参数不同的值具有不同的意义
- pid==-1,等待任意子进程(如果options=0则退化为wait)
- pid>0,等待进程id等于pid的子进程
- pid==0,等待组ID为调用进程组ID的任意子进程
- pid<0,等待组ID为pid绝对值的任意子进程
statloc参数详解
statloc保存了子进程的退出状态,按照比特位划分成不同的段,每一个段具有不同的意义(正常退出码,异常结束信号,core文件是否生成)。标准提供了一组宏函数用来从statloc中获取相应的信息。
僵尸进程与孤儿进程
如果父进程始终不对子进程进行回收操作,那么子进程便会一直占用进程ID号,内核也会一直保留其进程控制块,但子进程不会占用CPU资源,此时的子进程处于一种僵死状态(Z),称为==僵尸进程==,僵尸进程应该被避免,因为它会造成内存资源的浪费。一种解决僵尸进程的方式是杀死父进程,让僵尸进程称为孤儿进程(如果父进程先于子进程结束,则子进程变成孤儿进程,但会很快被1号进程所领养),让1号进程称为其父进程,1号进程一定会调用进程等待函数回收子进程的资源。
进程替换
大部分情况下,fork函数创建新的子进程后,子进程会调用exec家族函数以执行另个程序。当进程调用一种exec函数时,该进程执行的程序完全替换为新程序,而新程序则从其main 函数开始执行。调用exec并不创建新进程,所以前后的进程ID并未改变。exec只是用磁盘上的一个新程序替换了当前进程的正文段、数据段、堆段和栈段。
exec会继承如下属性(重点关注框出来的)
exec函数解析
请注意以filename为参数的exec函数遵循如下规则:
- filename如果是绝对路径等价于pathname
- filename仅是一个文件名则从环境变量PATH和当前目录查找可执行文件
exec用户ID问题
编写程序时应该遵守==最小权限原则,进程在运行时对应的权限应该为当前能正常处理任务的最低权限==。最小权限原则可以避免进程因为权限过高而进行恶意读写的行为。exec所执行的可执行文件所需要的最小权限可能低于在调用exec之前的权限,为了能够降低权限,建议将目标可执行文件设置用户ID为,保证exec在进程替换后有效用户ID是可执行文件的用户ID。