Linux C语言 22-进程
本节关键字:进程、exec函数族
相关C库函数:fork、getpid、getppid、getuid、geteuid、getgid、getegid、execl、execlp、execv、execvp、execle、execvpe
什么是进程?
- 进程是程序的执行过程;
- 进程是动态的,随着程序的使用被创建,随着程序的结束而消亡;
- 进程是系统调度的独立任务;
- 进程是程序执行的独立任务;
- 进程是内存资源管理的最小任务。
注意:一个程序可以只有一个进程,也可以有多个进程(程序由多个进程动态执行);每一个程序运行时,操作系统分配给进程的是虚拟内存,意味着每一个进程所使用的空间都是虚拟内存, 虚拟内存会被单元管理模块(MMU)映射到物理内存上,如何映射是操作系统关心的事情,程序开发者不用关心。
C程序的启动和终止:
时间片
进程有多个,而CPU只有一个,假设该CPU是单核的,那么在某一时刻CPU只能处理一个进程,但是不能一直去处理这个进程,得多个进程之间轮流处理,给用户感觉这些进程在同时进行,而CPU处理一个进程的时间段即时间片。时间片是约定好CPU处理一个进程的时间段。
进程的类型
- 交互进程:完成人机交互的进程,可以在前台运行,也可以在后台运行。
- 批处理进程:与终端无关,被提交到一个作业队列中顺序执行。
- 守护进程:和终端无关,一直到后台运行。
进程的状态
- 运行态:正在占用CPU执行任务。
- 等待态:又称阻塞态或睡眠态,缺少某些资源而让出CPU。
- 就绪态:资源准备就绪,等待CPU调度。
进程的模式
- 终端:内核发送的信号。
- 系统调用:调用操作系统提供的访问硬件的一组接口。
特殊进程
特殊进程是指处于一种非常规状态的进程,在这里主要将其分为孤儿进程和僵尸进程。
孤儿进程
父进程比子进程先退出的进程称为孤儿进程,孤儿进程会被进程号为1的init进程收养。
僵尸进程
子进程比父进程先退出,但没有被父进程回收资源的进程称为僵尸进程,僵尸进程会造成空间浪费和资源泄漏等问题。
进程的状态标志
- D 不可中断的静止
- R 正在执行中
- S 阻塞状态
- T 暂停执行
- Z 不存在但暂时无法消除
- < 高优先级的进程
- N 低优先级的进程
- L 有内存分页分配并锁在内存中
父进程和子进程之间的关系
父进程和子进程之间对打开文件的共享
除了打开文件之外,父进程的很多其他属性也由子进程继承,包括:
- 实际用户ID、实际组ID、有效用户ID、有效组ID
- 附属组ID
- 进程组ID
- 会话ID
- 控制终端
- 设置用户ID标志和设置组ID标志
- 当前工作目录
- 根目录
- 文件模式创建屏蔽字
- 信号屏蔽和安排
- 对任一打开文件描述符的执行时关闭(close-cm-exec)标志
- 环境
- 连接的共享存储段
- 存储映像
- 资源限制
父进程和子进程之间的区别具体如下:
- fork的返回值不同。
- 进程ID不同。
- 这两个进程的父进程ID不同:子进程的父进程ID是创建它的进程的ID,而父进程的父进程ID则不变。
- 子进程的 tms_utime、tms_stime、tms_cutime 和 tms_ustime 的值设置为 0
- 子进程不继承父进程设置的文件锁。
- 子进程的未处理闹钟被清除。
- 子进程的未处理信号集设置为空集。
进程相关库函数
#include <unistd.h>
// 通过复制调用进程来创建一个新进程,子进程返回0,父进程返回子进程ID,出错时父进程返回-1,并设置error为错误码
// 复制的子进程是从父进程fork()调用后面的语句开始执行的
// EAGIN 无法分配足够的内存来复制父级的页表并为子级分配任务结构
// ENOMEM 由于内存紧张,fork()无法分配必要的内核结构
// SENOSYS 此平台不支持fork()(例如,没有内存管理单元的硬件)
// ERESTARTNOINTR 系统调用被信号中断,将重新启动。(这只能在跟踪过程中看到
pid_t fork(void);
// 进程标识
// 获取当前进程的ID
pid_t getpid(void);
// 获取当前进程的父进程的ID
pid_t getppid(void);
// 获取当前进程实际用户ID
uid_t getuid(void);
// 获取当前进程有效用户ID
uid_t geteuid(void);
// 获取当前进程使用用户组ID
gid_t getgid();
// 获取当前进程有效用户组ID
gid_t getegid();
// 进程退出 将status传递给父进程
#include <stdlib.h>
void exit(int status);
// 进程回收
#include <sys/wait.h>
// 阻塞等待进程号为*stat_loc的进程退出
pid_t wait(int *stat_loc);
// 等待子进程退出
// 如果pid == (pid_t)-1,options为0,则waitpid函数等效于wait函数
// 如果pid == (pid_t)-1,则会请求任何子进程的状态
// 如果pid > 0,则指定请求状态的单个子进程的进程ID
// 如果pid == 0,则会为进程组ID等于调用进程的进程组ID的任何子进程请求状态
// 如果pid < (pid_t)-1,则会为进程组ID等于pid绝对值的任何子进程请求状态
// WCONTINUED 报告pid指定的任何连续子进程的状态,该进程的状态自作业控制停止后一直没有报告
// WNOHANG 如果pid指定的某个子进程的状态不立即可用,则waitpid函数不应暂停调用线程的执行
// WUNTRACED pid指定的任何已停止的子进程的状态,以及自停止以来尚未报告其状态的子进程,也应报告给请求进程
// 如果调用进程设置了SA_NOCLDWAIT或SIGCHLD设置为SIG_IGN,并且该进程对于转换为僵尸进程的子进程没有未经访问的权限,则调用线程应阻止,直到包含调用线程的进程的所有子进程终止,wait()和waitpid()将失败并将errno设置为[ECHILD]
pid_t waitpid(pid_t pid, int *stat_loc, int options);
进程相关库函数使用示例
#include <stdio.h>
#include <unistd.h>
void test01(void)
{
printf("======= main process begin =======\n");
int res = 10;
pid_t pid;
pid = fork();
if (pid == -1)
{
perror("fork error");
return;
}
if (pid == 0) // 子进程
{
printf("i am child: %d, my parent: %d\n", getpid(), getppid());
printf("i am child: uid[%d] euid[%d] gid[%d] egid[%d]\n",
getuid(), geteuid(), getgid(), getegid());
while (res <= 20)
{
sleep(2);
res += 1;
printf("child check res: %d\n", res);
}
}
if (pid > 0) // 父进程
{
printf("i am main: %d, my child: %d, my parent: %d\n", getpid(), pid, getppid());
printf("i am main: uid[%d] euid[%d] gid[%d] egid[%d]\n",
getuid(), geteuid(), getgid(), getegid());
while (res >= 0)
{
sleep(1);
res -= 2;
printf("main check res: %d\n", res);
}
printf("i am main, i am waiting child\n");
wait(&pid); // 防止僵尸进程的出现
printf("i am main, i will exit\n");
}
printf("======= main process end =======\n");
}
void test02(void)
{
printf("======= main process begin =======\n");
pid_t pid;
pid = fork();
if (pid == -1)
{
perror("fork error");
return;
}
if (pid == 0) // 子进程
{
printf("i am child: %d, my parent: %d\n", getpid(), getppid());
while (1)
{
sleep(2);
}
}
if (pid > 0) // 父进程
{
printf("i am main: %d, my child: %d, my parent: %d\n", getpid(), pid, getppid());
while (1)
{
sleep(1);
}
printf("i am main, i am waiting child\n");
wait(&pid); // 防止僵尸进程的出现
printf("i am main, i will exit\n");
}
printf("======= main process end =======\n");
/**
孤儿进程的验证步骤:
ps -ajx 查询结果的表头:
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
1、验证编译出来的a.out程序运行了两个进程
ps -ajx | grep a.out | grep -v grep
2、查看a.out的进程号 pid
ps -ajx | grep a.out | grep -v grep
3、进一步筛选与pid相关的进程信息
ps -ajx | grep pid | grep -v grep
4、通过pid的关系可以看出:
a.out主进程的父进程是 -bash
a.out子进程的父进程是 a.out的主进程
5、先结束父进程,观察子进程的PPID变化(由pid变成了1,即init进程号)
ps -ajx | grep a.out | grep -v grep
kill 父进程PID
ps -ajx | grep a.out | grep -v grep
补充:
kill -l 查看所有信号
kill PID 结束进程号为PID的进程
*/
}
void test03(void)
{
// 本来是希望利用for循环创建5个进程,结果创建了32个
// 问题解决:在当前进程为子进程时,不执行fork即可
printf("======= main process begin =======\n");
pid_t pid;
int i;
for (i=0; i<5; i++)
{
pid = fork();
if (pid == -1)
{
perror("fork error");
return;
}
if (pid == 0) // 子进程
{
printf("i am child: %d, my parent: %d\n", getpid(), getppid());
// break; // 注释解开时创建5个进程,不解开时创建32个进程
}
if (pid > 0) // 父进程
{
printf("i am main: %d, my child: %d, my parent: %d\n", getpid(), pid, getppid());
}
}
while (1)
{
sleep(1);
}
if (pid > 0)
{
printf("i am main, i am waiting child\n");
wait(&pid); // 防止僵尸进程的出现
printf("i am main, i will exit\n");
}
printf("======= main process end =======\n");
/**
查看程序创建的进程个数:
ps -ajx | grep a.out | grep -v grep | wc -l
结束程序a.out
pkill a.out
*/
}
int main(void)
{
test01(); // 验证复制创建进程
// test02(); // 验证孤儿进程
// test03(); // 控制进程创建个数
return 0;
}
/** 运行结果:
======= main process begin =======
i am main: 17688, my child: 17689, my parent: 10139
i am main: uid[1000] euid[1000] gid[1000] egid[1000]
i am child: 17689, my parent: 17688
i am child: uid[1000] euid[1000] gid[1000] egid[1000]
main check res: 8
main check res: 6
child check res: 11
main check res: 4
child check res: 12
main check res: 2
main check res: 0
child check res: 13
main check res: -2
i am main, i am waiting child
child check res: 14
child check res: 15
child check res: 16
child check res: 17
child check res: 18
child check res: 19
child check res: 20
child check res: 21
======= main process end =======
i am main, i will exit
======= main process end =======
*/
exec函数族
exec函数族用于进程程序替换,子进程执行的是父进程的代码片段,那么当我们想让创建出来的子进程执行全新的程序时怎么办呢?这个时候我们就需要使用进程的程序替换了。
那我们为什么要进行程序替换呢?其实也不难理解,大概可以分为两点:
- 我们想让子进程执行一个全新的程序;
- 完成不同语言编写的程序间可以互相调用。
一般在进行服务器设计(Linux编程)的时候,往往需要子进程干两类事情:
- 子进程执行父进程的代码段(服务器代码)
- 子进程执行磁盘中一个全新的程序(shell让客户端执行对应的程序,通过我们的进程去执行其他人写的进程代码等,编程语言可以由 C/C++ -》 C/C++/Python/Shell/Java …)
程序替换为什么使用子进程?
注意:进行程序替换的是子进程!!!原因如下:
- 进程替换永远影响的是进程的本身,子进程的替换永远不会影响父进程,因为进程具有独立性;
- 独立性体现在内核层面,不同进程有不同的地址空间,有不同的页表替换只是加入新的代码和数据;
- 重新建立的是页表映射但并不影响内核数据结构的具体情况;
- 子进程虽然和父进程代码共享数据写实拷贝,但是一旦发生进程替换了,就认为代码和数据发生了双写实拷贝,就彻底将两个进程分开了;
- 引入子进程的原因就是,一方面把需求做到位,另一方面不影响父进程,因为父进程可能还要接收新的命令,再去执行新的程序。
exec函数族的六个进程替换函数 && system函数
// 根据PATH环境变量寻找待执行程序,成功不返回(因为去执行程序了),失败返回-1;因为只有失败才返回,错误值-1,所以通常我们直接在exec函数调用后直接调用perror(),和exit(),无需if判断
// 参数1 程序名
// 参数2 argv0
// 参数3 argv1
// ... argvN
// 最后 NULL
// 区别:execlp(),让当前进程或者子进程执行系统命令,比如:ls,cat,cp等命令,而execl()则是执行自己所有的可执行程序,比如一个c程序a.out
// 示例:execl("/bin/echo", "echo", "Hello World", NULL);
int execl(const char *path, const char *arg, ...);
// 执行程序file,成功返回0,失败返回-1
// 示例:execlp("ls", "ls", "-l", NULL);
int execlp(const char *file, const char *arg, ...);
// 与execlp()函数不同的是,execle()函数可以显式地指定新程序的环境变量数组
// 示例:char *env_init[] = {"XX=xx", "OO=oo", NULL}; execle("./echoenv", "echoenv", NULL, env_init);
int execle(const char *path, const char *arg, ..., char *const envp[]);
// 执行指定路径下可执行文件的函数。该函数会将当前进程替换为指定路径下的可执行文件,并传递给新程序一个参数列表
// path:表示要执行的可执行文件的路径名
// argv:参数列表,是一个字符串数组,其中每个元素都是一个参数。最后一个元素必须为 NULL,用于标记参数列表的结束
// 示例:char *argv[]={"ls", NULL, NULL}; execv("/bin/ls", argv);
int execv(const char *path, char *const argv[]);
// 在系统的 PATH 环境变量指定的路径中搜索可执行文件,当调用execvp 函数时,系统将自动搜索可执行文件并执行它。新程序接收到的命令行参数将由 argv 提供。可以通过遍历 argv 数组来获取传递给新程序的参数
// 示例:char *argv[]={"ls", "-l", NULL}; execvp("ls", argv);
int execvp(const char *file, char *const argv[]);
// 示例:extern char **environ; char *const argv_[]={"ls", "-l", NULL}; execvpe("ls", argv_, environ);
// 规律:l(list)表示参数采用列表
; v(vector)参数用数组
; p(path)有p自动搜索环境变量PATH
; e(env)表示自己维护环境变量
int execvpe(const char *file, char *const argv[], char *const envp[]);
// 通过调用/bin/sh-c命令执行命令中指定的命令,并在命令完成后返回。在命令执行期间SIGCHLD将被阻塞,
并且SIGINT和SIGQUIT将被忽略
// 失败返回-1,成功就执行命令;如果命令为NULL,shell可用system()返回非零,如果不可用则返回零
// system()不会影响任何其他子项的等待状态,通过源码可以看出Linux系统下,system函数是execl函数的封装版
// 示例:system("top");
#include <stdlib.h>
int system(const char *command);