本节关键字:进程、exec函数族
相关C库函数:fork、getpid、getppid、getuid、geteuid、getgid、getegid、execl、execlp、execv、execvp、execle、execvpe
进程有多个,而CPU只有一个,假设该CPU是单核的,那么在某一时刻CPU只能处理一个进程,但是不能一直去处理这个进程,得多个进程之间轮流处理,给用户感觉这些进程在同时进行,而CPU处理一个进程的时间段即时间片。时间片是约定好CPU处理一个进程的时间段。
特殊进程是指处于一种非常规状态的进程,在这里主要将其分为孤儿进程和僵尸进程。
父进程比子进程先退出的进程称为孤儿进程,孤儿进程会被进程号为1的init进程收养。
子进程比父进程先退出,但没有被父进程回收资源的进程称为僵尸进程,僵尸进程会造成空间浪费和资源泄漏等问题。
除了打开文件之外,父进程的很多其他属性也由子进程继承,包括:
父进程和子进程之间的区别具体如下:
#include
// 通过复制调用进程来创建一个新进程,子进程返回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
void exit(int status);
// 进程回收
#include
// 阻塞等待进程号为*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
#include
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函数族用于进程程序替换,子进程执行的是父进程的代码片段,那么当我们想让创建出来的子进程执行全新的程序时怎么办呢?这个时候我们就需要使用进程的程序替换了。
那我们为什么要进行程序替换呢?其实也不难理解,大概可以分为两点:
一般在进行服务器设计(Linux编程)的时候,往往需要子进程干两类事情:
注意:进行程序替换的是子进程!!!原因如下:
// 根据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
int system(const char *command);