进程控制相关函数
进程
进程相关概念解释
程序与进程的区别
- 程序: 死的,存在硬盘上,只占用磁盘空间。 — 剧本
- 进程:活动。运行在内存中的程序。占用内存、cpu等资源。 — 戏
虚拟内存与物理内存的映射关系
- PCB:进程控制块
- MMU:内存管理单元,在CPU内部
. src=”./DeepinScreenshot_select-area_20200506155851.png” style=”zoom:100%;” />
进入到系统调用实际上就是靠的MMU进行权级切换
PCB 进程控制块
每个进程在内核中都有一个进程控制块(PCB)来维护进程相关信息,linux内核的进程块是task_struct结构体
/usr/linux-headers-xx.xx.x-x/include/linux/sched.h
文件中可以查看struct task_struct
结构体定义
其内部成员有很多,重点掌握以下部分即可:
进程ID:系统中每个进程有唯一的id,在c语言中用pid_t类型表示,其实就是一个非负整数
进程的状态:有初始、就绪、运行、阻塞、挂起、停止等状态
- 其中初始态为进程准备阶段,常与就绪态结合来看
进程切换时需要保存和恢复的一些CPU寄存器
描述虚拟地址空间的信息
描述控制终端的信息
当前工作目录
umask掩码
文件描述符表:包含很多指向file结构体的指针
和信号相关的信息
用户id和组id
会话(Session)和进程组
进程可以使用的资源上限
进程组和会话
进程组(别名:作业)
- 多个进程的集合,每个进程都属于一个进程组,简化对多个进程的管理,
waitpid
函数和kill
函数的参数中用-pid
来表示一个进程组。 - 父进程创建子进程的时候默认父子进程属于同一进程组。进程组的ID==第一个进程ID(组长进程), 故组长进程标识
PGID==PID
- 只要有一个进程存在,进程组就存在,生存期与组长进程是否终止无关
- kill -SIGKILL -进程组ID(负数) 杀掉整个进程组
- 进程组生存期:进程组创建到最后一个进程离开(终止或转移到另一个进程组)
- 一个进程可以为自己或子进程设置进程组id
会话
多个进程组的集合
创建会话的注意事项:
- 调用进程不能是进程组组长,该进程变成新会话首进程(session leader)
- 该进程成为一个新进程组的组长进程。
- 新会话丢弃原有的控制终端,该会话没有控制终端
- 该调用进程是组长进程,则出错返回
- 建立新会话时,先调用
fork
, 父进程终止,子进程调用setsid()
- 部分linux需要root权限才能创建
守护进程
Daemon进程,是linux中的后台服务程序,通常独立于控制终端并且周期性地执行某种任务或等待处理
某些发生的事件。一般采用以 d 结尾的名字。
Linux 后台的一些系统服务进程,没有控制终端,不能直接和用户交互。不受用户登录、注销的影响,一直在
运行着,他们都是守护进程。如:预读入缓输出机制的实现;httpd服务器;sshd服务器等。
创建守护进程模型
创建守护进程,最关键的一步是调用
setsid
函数创建一个新的 Session,并成为Session Leader
。
创建步骤:
- 创建子进程,父进程退出
所有工作在子进程中进行形式上脱离了控制终端 - 在子进程中创建新会话
setsid()
函数
使子进程完全独立出来,脱离控制 - 改变当前目录位置
chdir()
函数
防止占用可卸载的文件系统 - 重设文件权限掩码
umask()
函数
防止继承的文件创建屏蔽字拒绝某些权限
增加守护进程灵活性 - 关闭文件描述符
继承的打开文件不会用到,浪费系统资源,无法卸载 - 开始执行守护进程核心工作守护进程退出处理程序模型
e.g. 简单实现
1 |
|
进程控制
fork函数
函数原型
1 |
|
1 | 返回值 |
例子:
1 |
|
运行结果:
循环创建子进程
1 |
|
getpid与getppid函数
函数原型
1 |
|
返回值: 该函数永远成功返回当前进程的PID或父进程的PID
getuid 与geteuid函数
获取当前进程实际用户ID
1 |
|
获取当前进程有效用户ID
1 |
|
getgid 与getegid函数
获取当前进程使用用户组ID
1 |
|
获取当前进程有效用户组ID
1 |
|
getsid函数
获取进程的会话id
函数原型
1 |
|
成功返回调用进程会话ID,失败返回-1,设置errno
setsid函数
创建一个会话,并以自己的ID设置进程组ID,同时也是新会话的ID
函数原型
1 |
|
成功返回调用进程会话ID,失败返回-1,设置errno
e.g. 简单使用
1 |
|
运行结果
1 | [parent] current process PID is 7510 |
setuid和seteuid函数
setuid()
函数用来设置调用进程的用户ID(real uid和effective uid)seteuid
函数用来设置调用进程的有效用户ID(effective uid)
函数原型
1 |
|
参数解释
1 | @param: |
注意事项setuid()
的传入参数uid
,根据进程具有的权限情况,可分为以下2种情况:
- 如果进程具有超级用户权限特权,那么就能设置任意的
effective uid
和real uid
, 注意所有与进程有关的用户ID都被设置为uid(非0)
,在这种情况发生后,程序就不可能重新获得root权限. - 无特权用户只能用将
real uid和effective uid
同时设置成real uid或effective uid
(e.g. 如果一个用户的real uid
为1000,通过设置SGID
的方式运行一个root用户的程序,即effective uid
为0, 如下图,那么此时setuid()
只能传入1000
或0
) 因此一个SGID
程序希望暂时放弃root权限,以一个无权限用户的身份出现,然后重新获得root权限, 可以使用seteuid()
来完成。
一般来说setuid
函数用于降权使用的,一般要和suid(s权限)标志同时使用, 例如apache+php的web服务器 fork
一个子进程,然后在用setuid
来降低权限,来提高web服务器的安全性。
e.g. 简单的使用示例
1 |
|
编译并给程序赋予s权限
1 | make && sudo chown 0:0 foo && sudo chmod u+s foo |
运行结果
1 | [parent] current UID is 1000 |
进程共享
- 父子进程相同之处:
- 刚fork后。代码段、data段、堆、栈、环境变量、全局变量、宿主目录位置、进程工作目录位置、信号处理方式
- 父子进程不同之处:
- 进程id、返回值、各自的父进程、进程创建时间、闹钟、未决信号集
- 对于全局变量,父子进程间遵循读时共享写时复制的原则
- 父子进程共享:
- 文件描述符(打开文件的结构体)
- mmap建立的映射区
- fork之后,父进程先执行还是子进程先执行不确定。取决于内核所使用的调度算法
gdb调试
使用gdb
调试时,gbd
只能跟踪一个进程,默认跟踪父进程
可以在调用fork
函数之前通过指令来设置gdb
跟踪父进程或子进程
set follow-fork-mode child
命令设置gdb
在fork
之后跟踪子进程set follow-fork-mode parent
设置跟踪父进程
注意一定要在fork函数调用之前
才有效
exec函数族
当进程调用exec函数时,该进程的用户空间代码段.text
与数据段.data
完全被新程序替换,然后从新的.text
第一条指令开始执行,但进程PID不变,换核不换壳
有六种以exec开头的函数,统称exec函数:
1 |
|
execlp函数
加载一个进程,借助PATH环境变量
函数原型
1 |
|
参数:
- file:要加载程序的名字,该函数需要配合
PATH
环境变量来使用,当PATH
中所以目录搜索后没有该参数的值则报错
- file:要加载程序的名字,该函数需要配合
返回:
- 成功:无返回
- 失败:-1
该函数通常调用系统程序如
cp、ls、date、cat
例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
int main(int argc, char* argv[])
{
pid_t pid = fork();
if (pid == -1) {
perror("fork error");
exit(1);
} else if (pid == 0) { // 子进程
execlp("ls", "ls", "-lh", NULL);
// 只有exec出错才有机会进入后面代码,否则已经进入别的程序中执行了
perror("exec error");
exit(1);
} else if (pid > 0) { // 父进程
sleep(1);
printf("I'm parent %d\n", getpid());
} else {
}
return 0;
}
execl函数
指定路径,加载一个进程
1
2execl("./a.out", "a.out", NULL);
execl("/usr/bin/ls", "ls", "-lah", NULL);
execvp函数
函数原型
1
2
int execvp(const char *file, char *const argv[]);用法:
1
2char *ls_argv[] = {"ls", "-l", "-h", NULL};
execvp("ls", ls_argv);
exec 函数族一般规律
- exec函数一旦调用成功即执行新的程序,不返回。只有失败才返回,错误值-1
- 所以通常直接在exec函数调用后直接调用perror()和exit(),无需if判断
函数名中的字母 | 描述 |
---|---|
l(list) | 命令行参数列表 |
p(path) | 搜索file时使用PATH环境变量 |
v(vector) | 使用命令行参数数组 |
e(environment) | 使用环境变量数组,不使用进程原有的环境变量,设置新加载程序运行的环境变量 |
实际上,只有
execve
是真正的系统调用,其他exec函数最终都调用execve
,所以execve
在man手册的第二卷,其他函数在man手册的第三卷,这些函数的关系如下图所示:
回收子进程
孤儿进程
孤儿进程:父进程先于子进程结束,则子进程成为孤儿进程,
子进程的父进程成为init进程,称为init进程领养孤儿进程。
僵尸进程
僵尸进程:进程终止,父进程尚未回收,子进程残留资源(PCB) 存在于内核中,变成僵尸进程。
注意:僵尸进程是不能用kill命令清除掉的。因为kill命令只是用来终止进程的,而僵尸进程已经终止了,这种时候只能杀死父进程来终止僵尸进程
wait函数
父进程调用wait函数可以回收子进程终止信息,该函数有三个功能:
- 阻塞等待子进程退出
- 回收子进程残留资源
- 获取子进程结束状态(退出原因)
函数原型
1 |
|
1 | 参数 |
当进程终止时,操作系统的隐式回收机制会:
- 关闭所以文件描述符
- 释放用户空间分配的内存,内核中的PCB仍存在。其中保存该进程的退出状态(正常退出 -> 退出值; 异常退出 -> 终止信号)
可使用wait函数传出参数status来保存进程的退出状态,借助宏函数来进一步判断进程终止的具体原因,宏函数可分为三组:
WIFEXITED(status):为非0 -> 进程正常结束;
WEXITSTATUS(status) :如上宏为真,使用此宏获取进程退出状态(exit的参数)
WIFSIGNALED(status) 为非0 -> 进程异常终止
WTERMSIG(status) 如上宏为真,使用此宏取得使进程终止的那个信号的编号。
WIFSTOPPED(status) 为非0 -> 进程处于暂停状态
WSTOPSIG(status) 如上宏为真,使用此宏取得使进程暂停的那个信号的编号。
WIFCONTINUED(status) 为真 进程暂停后已经继续运行
waitpid 函数
作用同wait,但可指定pid进程清理,可以不阻塞
函数原型
1 |
|
1 | 特殊参数: |
waitpid(-1, &wstatus, 0)
等价wait(&wstatus)
注意:一次wait
或waitpid
调用只能清理一个子进程,清理多个子进程应使用循环。
e.g.
1 |
|
运行结果:
1 | I'm 1th child, pid = 58902 |