1.进程的基本概念
程序:磁盘上的可执行文件。
进程:内存中的指令和数据,是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
父子进程
父进程创建子进程,子进程继承父进程。
一个父进程可以创建多个子进程,每个子进程 有且仅有一个父进程,除非是根进程(PID=0,作为调度器实例)没有父进程。通过创建子进程,一级一级执行
进程树:调度进程(PID=0) --> init(PID=1) --> xinetd(监控网络) --> in.telnetd(远程登录) --> login( 用户名和口令) --> bash(Shell命令:如ls) --> ls(显示目录条目清单)父进程创建子进程以后,子进程在操作系统的调度下与其父进程同时运行。父进程在创建完子进程以后依然存在,甚至可以和子进程进行某种形式的交互,如:传参、回收、通信等。不同于有些旧进程在创建完新进程以后会被其取代,新进程沿用旧进程的PID,继续独立地存在。
孤儿进程
如果父进程先于子进程的终止而终止,子进程即成为孤儿进程,同时会被init进程收养,即成为init进程(现在系统有别的进程来替代init进程来管理孤儿进程)的子进程,因此init进程又被成为孤儿院进程。一个进程成为孤儿进程是正常的,系统中大多数守护进程都是孤儿进程。
//孤儿进程示例: #include <stdio.h> #include <unistd.h> int main(void) { printf("%d进程:我要调用fork()了...\n", getpid()); pid_t pid = fork(); if (pid == -1) { perror("fork"); return -1; } if (pid == 0) { sleep(1);//延迟,使其父进程先终止,以至于该子进程成为孤儿进程 printf("\n%d进程:我是被%d进程收养" "的孤儿进程。", getpid(), getppid()); return 0; } printf("%d进程:我是%d进程的父进程," "马上死!\n", getpid(), pid); return 0; }
僵尸进程
如果子进程先于父进程的终止而终止,但父进程由于某种原因,没有回收子进程的尸体(终止状态),子进程即成为僵尸进程。僵尸进程虽然已经不再活动,但其终止状态和PID仍然被保留,也会占用系统资源,直到其被父进程或init进程回收为止。如果父进程直到其终止都没有回收其处于僵尸状态的子进程,init进程会立即回收这些僵尸。因此一个进程不可能同时既是僵尸进程又是孤儿进程。
//僵尸进程示例: #include <stdio.h> #include <unistd.h> int main(void) { printf("%d进程:我要调用fork()了...\n", getpid()); pid_t pid = fork(); if (pid == -1) { perror("fork"); return -1; } if (pid == 0) { printf("%d进程:我是%d进程的子进程," "马上变僵尸!\n", getpid(), getppid()); return 0; } sleep(1); printf("%d进程:我是%d进程的父进程。\n", getpid(), pid); getchar();//父进程中进行阻塞,使得子进程终止时,父进程没状态去回收子进程,从而子进程成为僵尸进程 return 0; }
2.进程的分类
交互式进程:由Shell启动,借助标准I/O与用户交互。
批处理进程:在无需人工干预的条件下,自动运行一组批量任务。
守护(精灵)进程:后台服务,多数时候处于待命状态,一旦有需要可被激活完成特定的任务。
3.进程快照(通过命令获取进程信息)
ps
- 显示当前用户拥有控制终端的进程信息PID为进程ID
TTY为占用的终端
CMD为进程名
ps axuw
(BSD风格选项)a: 所有用户
x:既包括有控制终端也包括无控制终端的进程
u: 详细信息
w: 更大列宽
ps -efFl
(SVR4风格选项 )e: 所有用户的所有进程
f: 完整格式
F: 更完整格式
l: 长格式
USER/UID: 进程的实际用户ID
PID: 进程标识
%CPU/C: CPU使用率
%MEM: 内存使用率
VSZ: 占用虚拟内存大小(KB)
RSS: 占用半导体物理内存大小(KB)
TTY: 终端次设备号
ttyn – 物理终端(硬件设备)
pts/n – 虚拟终端(软件窗口)
? – 无控制终端,如后台进程STAT/S: 进程状态
O – 就绪,等待被调度
R – 运行,Linux下没有O状态,就绪状态也用R表示
S – 可唤醒睡眠。如遇到系统中断,获得资源,收到信号,都可被唤醒,转入运行状态
D – 不可唤醒的睡眠。只能被wake_up系统调用唤醒
T – 暂停,收到SIGSTOP(19)信号转入暂停状态,收到SIGCONT(18)信号转入运行状态
W – 等待内存分页(2.6内核后被废弃)
X – 终止且被回收,不可见
Z – 僵尸,已退出但未被回收
< – 高优先级
N – 低优先级
L – 有被锁定在半导体内存中的分页
s – 会话首进程(进程组中最开始的进程有此标记)
l – 多线程化
+ – 在前台进程组中。可在命令后加&,使其强制变为后台不占用终端START: 进程启动时间
TIME: 进程运行时间
COMMAND/CMD: 进程启动命令
F: 进程标志
1 - 通过fork产生的子进程,但是并没有通过exec创建新进程
4 - 拥有超级用户(root)特权PPID: 父进程的PID
NI: 进程nice值,-20~19,进程优先级的浮动量
PRI: 进程优先级=80+nice,则值范围为60~99,值越小优先级越高。
I/O消耗型进程,奖励,提高优先级,降低nice值;
处理机消耗型进程,惩罚,降低优先级,提高nice值;ADDR: 内核进程的内存地址,普通进程显示"-"
SZ: 占用虚拟内存页数
WCHAN: 进程正在等待的内核函数或事件
PSR: 进程当前正在被哪个处理器执行
top
命令可动态查看进程信息
4.进程ID
系统内核会为每个进程维护一个进程表项。一个进程的能力和权限,由其有效用户ID和有效组ID决定。其中包括如下ID:
①进程ID:系统为每个进程分配的唯一标识。内核在分配进程ID时,会持续增加,直到无法再增加了,再从头寻找被释放的ID,即延迟重用。
②父进程ID:父进程的PID,在创建子进程的过程中被初始化到子进程的进程表项中。
③实际用户ID:启动该进程的用户ID。
④实际组ID:启动该进程的用户组ID。
⑤有效用户ID:通常情况下,取自进程的实际用户ID。如果该进程的可执行文件带有设置用户ID位,那么该进程的有效用户ID就取自其可执行文件的拥有者用户ID。
⑥有效组ID:通常情况下,取自进程的实际组ID。如果该进程的可执行文件带有设置组ID位,那么该进程的有效组ID就取自其可执行文件的拥有者组ID。
getpid函数
#include <unistd.h>
pid_t getpid(void);
// 返回调用进程的PID
getppid函数
#include <unistd.h>
pid_t getppid(void);
// 返回调用进程的PPID, 即其父进程的PID
getuid函数
#include <unistd.h>
uid_t getuid(void);
// 返回调用进程的实际用户ID
getgid函数
#include <unistd.h>
uid_t getgid(void);
// 返回调用进程的实际组ID
geteuid函数
#include <unistd.h>
uid_t geteuid(void);
// 返回调用进程的有效用户ID
getegid函数
#include <unistd.h>
uid_t getegid(void);
// 返回调用进程的有效组ID
#include <stdio.h>
#include <unistd.h>
int main(void)
{
printf(" 进程ID: %d\n", getpid());
printf(" 父进程ID: %d\n", getppid());
printf("实际用户ID: %d\n", getuid());
printf(" 实际组ID: %d\n", getgid());
printf("有效用户ID: %d\n", geteuid());
printf(" 有效组ID: %d\n", getegid());
return 0;
}
5.创建子进程
fork函数
#include <unistd.h>
pid_t fork(void);
成功在父进程中返回子进程的PID而在子进程中返回0,失败返回-1。调用一次返回两次:
在父进程中返回所创建子进程的PID,而在子进程中返回0。
fork函数成功返回以后,父子进程各自独立地运行,其被调度的先后顺序并不确定,某些实现可以保证子进程先被调度。
函数的调用者往往可以根据该函数返回值的不同,分别为父子进程编写不同的处理分支,如下:pid_t pid = fork(); if (pid == -1) { perror("fork"); exit(EXIT_FAILURE); } if (pid == 0) { 子进程的处理分支 ------------------- exit(EXIT_SUCCESS); } 父进程的处理分支 ------------------- exit(EXIT_SUCCESS);
#include <stdio.h> #include <unistd.h> int main(void) { printf("%d进程:我要调用fork()了...\n", getpid()); pid_t pid = fork(); if (pid == -1) { perror("fork"); return -1; } if (pid == 0) { printf("%d进程:我是%d进程的子进程。\n", getpid(), getppid()); return 0; } printf("%d进程:我是%d进程的父进程。\n", getpid(), pid); sleep(1);//防止父进程终止,子进程未打印完成 return 0; }
子进程是父进程不完全副本,子进程的数据区、BSS区、堆栈区(包括I/O缓冲区),甚至命令行参数和全景变量区都从父进程拷贝,唯有代码区与父进程共享。
//子进程对父进程的数据区、BSS区、堆栈区的复制 #include <stdio.h> #include <stdlib.h> #include <unistd.h> int global = 100; // 数据区 int main(void) { int local = 200; // 栈区 int* heap = malloc(sizeof(int)); *heap = 300; // 堆区 printf("父进程:%d %d %d\n", global, local, *heap); pid_t pid = fork(); if (pid == -1) { perror("fork"); return -1; } if (pid == 0) { printf("子进程:%d %d %d\n", ++global, ++local, ++*heap); free(heap);//记得释放子进程中复制过来的堆区数据 return 0; } sleep(1); printf("父进程:%d %d %d\n", global, local, *heap); free(heap); return 0; }
//子进程对父进程输出缓冲区的复制 #include <stdio.h> #include <unistd.h> int main(void) { printf("ABC");//没有换行符,此时字符串"ABC"还在缓冲区中 pid_t pid = fork(); if (pid == -1) { perror("fork"); return -1; } if (pid == 0) { printf("XYZ\n"); return 0; } sleep(1); printf("\n"); return 0; }
//子进程对父进程输入缓冲区的复制 #include <stdio.h> #include <unistd.h> int main(void) { printf("父进程:"); int a, b, c; scanf("%d%d%d", &a, &b, &c);//当此时输入6个数如:1,2,3,4,5,6 //则前三个被父进程取走,后三个还在缓冲区中 pid_t pid = fork();//此时创建子进程,输入缓冲区中的后三个数字便被复制到子进程的输入缓冲区中 //但注意即使后面子进程取走输入缓冲区里的这三个数字,父进程的输入缓冲区还是有后三个数字的 if (pid == -1) { perror("fork"); return -1; } if (pid == 0) { scanf("%d%d%d", &a, &b, &c); printf("子进程:%d %d %d\n", a, b, c); return 0; } sleep(1); printf("父进程:%d %d %d\n", a, b, c); return 0; }
fork函数成功返回以后,系统内核为父进程维护的文件描述符表也被复制到子进程的进程表项中,但文件表项并不复制,即共享同一份文件表项。
#include <stdio.h> #include <string.h> #include <unistd.h> #include <fcntl.h> int main(void) { int fd = open("ftab.txt", O_RDWR | O_CREAT | O_TRUNC, 0644); if (fd == -1) { perror("open"); return -1; } char const* text = "Hello, World!"; if (write(fd, text, strlen(text) * sizeof( text[0])) == -1) { perror("write"); return -1; } pid_t pid = fork(); if (pid == -1) { perror("fork"); return -1; } if (pid == 0) { if (lseek(fd, -6, SEEK_CUR) == -1) { perror("lseek"); return -1; } close(fd); return 0; } sleep(1); text = "Linux"; if (write(fd, text, strlen(text) * sizeof( text[0])) == -1) { perror("write"); return -1; } close(fd); return 0; }
系统总线程数达到上限(
cat /proc/sys/kernel/threads-max
可查看)或用户总进程数达到上限(ulimit -u
可查看),fork函数将返回失败。一个进程如果希望创建自己的副本并执行同一份代码,或希望与另一个进程并发地运行(通过exec函数族),都可以使用fork函数。如下图:
6.创建轻量级子进程
vfork函数
#include <unistd.h>
pid_t vfork(void);
成功在父进程中返回子进程的PID而在子进程中返回0,失败返回-1。vfork函数与fork函数的功能基本相同,只有以下两点区别:
①vfork函数创建的子进程不复制父进程的物理内存,也不拥有自己独立的内存映射,而是与父进程共享全部地址空间。
②vfork函数在创建子进程的同时会挂起父进程,直到子进程终止,或通过exec函数创建新进程,再恢复父进程的运行。终止vfork函数创建的子进程,不要在使用main函数中使用return语句,也不要在任何函数中调用exit函数,而要调用_exit函数,以避免对父进程造成不利影响。
PS:使用了写时拷贝(copy-on-write)优化技术的fork结合exec的使用,其性能并不弱于典型vfork+exec的用法在(现在可以通过线程实现这种用法的效果)。
拷贝(copy-on-write)优化技术的fork:除写操作外,父子进程共享内存数据,只有发生写操作时才发生拷贝,提高了内存的利用率。#include <stdio.h> #include <stdlib.h> #include <unistd.h> int global = 100; // 数据区 int main(void) { int local = 200; // 栈区 int* heap = malloc(sizeof(int)); *heap = 300; // 堆区 printf("父进程:%d %d %d\n", global, local, *heap); pid_t pid = vfork(); if (pid == -1) { perror("fork"); return -1; } if (pid == 0) { printf("子进程:%d %d %d\n", ++global, ++local, ++*heap); //free(heap);不能free,这个数据父进程还要用 //return 0;不能用return,因为vfork函数出来的子进程共享内存数据,这里return,会对父进程造成影响 _exit(0);//这个函数只管自己进程的事,不影响全局。退出前会进行清理工作。 } //sleep(1); printf("父进程:%d %d %d\n", global, local, *heap); free(heap); return 0; }
7.进程的终止
1)正常终止
①从main函数中返回
②在任何地方调用exit / _exit / _Exit 函数
③在主线程中调用pthread_exit函数(主线程终止,其它线程也就随之终止)
进程一旦终止,被终止进程在用户空间所持有的资源会被自动释放,如代码区、数据区、堆栈区等,但内核空间中与该进程相关的资源,如进程表项、文件描述符等未必会得到释放。
main函数的返回值
main函数的返回值和exit / _exit / _Exit函数的参数一样,构成了进程的退出码,可以被终止进程的父进程通过wait或waitpid函数获得,其中只有最低8位可被获取。
宏:
退出码为 0 的宏EXIT_SUCCESS
退出码为 -1的宏EXIT_FAILURE
atexit函数和on_exit函数(注册退出处理函数)
#include <stdlib.h>
int atexit(void (* function)(void));
int on_exit(void (* function)(int, void*), void* arg);function – 退出时需处理的函数
int – 由main函数的返回值或exit函数参数构成的进程退出码提供
void* – 由后面的arg提供main函数退出时会调用这些传入的函数指针,然后进行一些清理处理工作。以栈的先进后出的方式进行工作的,先调用的函数最后执行。
exit / _exit / _Exit 函数
exit函数的执行过程(会调用_exit函数):
调用实现通过atexit/on_exit函数注册的退出处理函数
冲刷并关闭所有仍处于打开状态的标准I/O流
删除所有通过tmpfile函数创建的临时文件
调用_exit函数:
关闭所有仍处于打开状态的文件描述符
将调用进程的子进程(无论活死)托付给孤儿院进程收养
向调用进程的父进程发送SIGCHLD(17)信号
令调用进程终止_Exit与_exit的功能完全一致,唯一的区别是前者有标准库提供,被声明于stdlib.h,而后者有系统调用提供,被声明于unistd.h。
在main函数中执行return语句就相当于调用了exit函数。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> void doexit1(void) { printf("doexit1()...\n"); } void doexit2(int status, void* arg) { printf("doexit2(%d,%s)...\n", status, (char*)arg); } int foo(void) { printf("foo()...\n"); //exit(EXIT_SUCCESS);加这句时进程的退出码为0,执行注册的退出处理函数,则doexit2的status为0 //_exit(EXIT_FAILURE);加这句时进程的退出码为-1,但不执行注册的退出处理函数。 //执行注册的退出处理函数只有遇到exit函数或main函数返回时才调用执行 _Exit(EXIT_FAILURE);//加这句时进程的退出码为-1,但不执行注册的退出处理函数。 //执行注册的退出处理函数只有遇到exit函数或main函数返回时才调用执行 return 10; } int main(void) { atexit(doexit1);//先注册处理函数,只有当main函数return时或遇到exit函数时才被调用 on_exit(doexit2, "再见");//先注册处理函数,只有当main函数return时或遇到exit函数时才被调用 printf("foo()函数返回%d。\n", foo()); return 123; }
2)异常终止
①在主线程外部通过pthread_cancel将主线程取消
②通过信号杀死进程
SIGINT(2) -->Ctrl+C
, 终端中断符信号, 进程收到该信号执行默认动作——(异常)终止。
SIGQUIT(3) -->Ctrl+\
, 终端退出符信号
SIGKILL(9) – 必杀信号
SIGTERM(15) – 可选终止
SIGSEGV(11) – 内存段出错
SIGBUS(7) – 硬件错误
…
8.回收子进程
好处:
①通过等待子进程结束实现某种进程间的同步(比如:子进程进行文件下载,父进程进行读取该下载的文件,那得等到子进程结束了即文件下载完成了才可以进行读取操作,这就是进程间的同步)。
②获知子进程的退出码,根据子进程不同的退出原因采取不同的对策。
③避免过多的子进程变为僵尸进程拖垮系统。
wait函数
#include <sys/wait.h>
pid_t wait(int* status);
成功返回所回收子进程的PID,失败返回-1。status - 输出子进程的终止状态(后面通过分析这个参数查看子进程退出状态),可置NULL(不关心子进程退出情况)。
父进程在创建若干子进程以后调用wait函数,有以下情形:
①若所有子进程都在运行,则阻塞,直到所以子进程终止才返回
②若至少有一个子进程已经终止,则立即返回该子进程的PID并通过status参数(若非NULL)输出其终止状态
③若没有需要等待的活动子进程,也没有需要回收的死亡子进程,则返回 -1,同时置errno为ECHILD分析进程的终止状态(通过带参宏来分析):
WIFEXITED(status) - 非零表示进程正常终止
WEXITSTATUS(status) - 提供进程main函数的返回值或者传递给exit / _exit / _Exit函数参数的低8位
WIFSIGNALED(status) - 非零表示进程被信号杀死
WTERMSIG(status) - 提供杀死进程的信号编号#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/wait.h> int main(void) { pid_t pid = fork(); if (pid == -1) { perror("fork"); return -1; } if (pid == 0) { int retval = 0x12345678; printf("子进程:我是%d进程。" "我要以%#x返回值退出。\n", getpid(), retval); //return retval; 均返回低八位 //exit(retval); //_exit(retval); _Exit(retval); } printf("父进程:我要等待子进程...\n"); int status;//用来接收子进程终止状态,用于后续分析 pid = wait(&status);//这里要么子进程未结束阻塞,要么子进程结束变为僵尸进程直接回收了 if (WIFEXITED(status)) printf("父进程:发现%d子进程" "以%#x退出码终止。\n", pid, WEXITSTATUS(status)); return 0; }
//同步的回收子进程,缺点是需要一直循环查看,造成阻塞 #include <stdio.h> #include <errno.h> #include <unistd.h> #include <sys/wait.h> int main(void) { //新建三个子进程并打印退出 for (int i = 0; i < 3; ++i) { pid_t pid = fork(); if (pid == -1) { perror("fork"); return -1; } if (pid == 0) { printf("子进程:我是%d进程。" "我要退出了。\n", getpid()); return 0; } } //一直循环的回收子进程,直到子进程全部回收完毕 for (;;) { printf("父进程:我要等待子进程...\n"); pid_t pid = wait(NULL); if (pid == -1) { if (errno != ECHILD) {//如果pid返回-1,但errno不是ECHILD,说明出错了 perror("wait"); return -1; } printf("父进程:已经没有" "子进程可等了。\n"); break; //如果pid返回-1,且errno是ECHILD,说明进程中无活动的子进程也无终止的子进程,即回收完毕 } printf("父进程:发现%d进程退出了。\n", pid); } getchar(); return 0; }
waitpid函数
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int* status, int options);
成功返回所回收子进程的PID,失败返回-1。pid - 进程标识,可取以下值:
<-1: 等待并回收由-pid(pid 的相反数来标识进程组的组长)所标识的进程组中任意
子进程
-1: 等待并回收任意子进程,相当于wait函数
0: 等待并回收与调用进程同组的任意子进程
>0: 等待并回收由pid所标识的特定子进程(常用)status - 输出子进程的终止状态,可置NULL。
options - 选项,可取以下值:
0:阻塞模式,等不来就死等,类似于wait函数
WNOHANG:非阻塞模式,所等子进程仍在运行,则返回0#include <stdio.h> #include <unistd.h> #include <sys/wait.h> int main(void) { //存放新建的三个子进程 pid_t pids[3]; for (int i = 0; i < sizeof(pids) / sizeof( pids[0]); ++i) { if ((pids[i] = fork()) == -1) { perror("fork"); return -1; } if (pids[i] == 0) { printf("子进程:我是%d进程。" "我要退出了。\n", getpid()); return 0; } } for (int i = 0; i < sizeof(pids) / sizeof( pids[0]); ++i) { printf("父进程:我要等待%d进程...\n", pids[i]); //指定回收pid,阻塞模式 pid_t pid = waitpid(pids[i], NULL, 0); if (pid == -1) { perror("waitpid"); return -1; } printf("父进程:发现%d进程退出了。\n", pid); } return 0; }
#include <stdio.h> #include <unistd.h> #include <sys/wait.h> #include <errno.h> int main(void) { for (int i = 0; i < 3; ++i) { pid_t pid = fork(); if (pid == -1) { perror("fork"); return -1; } if (pid == 0) { printf("子进程:我是%d进程。" "我要退出了。\n", getpid()); return 0; } } for (;;) { //回收任意子进程,不阻塞 pid_t pid = waitpid(-1, NULL, WNOHANG); //判断是否回收完毕 if (pid == -1) { if (errno != ECHILD) { perror("waitpid"); return -1; } printf("父进程:子进程都死光了。\n"); break; } if (pid) printf("父进程:" "发现%d进程退出了。\n", pid); else { printf("父进程:" "暂时没有子进程可回收。\n"); // 空闲处理... //------------------ } } return 0; }
9.创建新进程
子进程:父子同在——并行。
新进程:以新换旧——取代。
exec函数族包括6个函数,根据参数的形式(变长参数或字符指针数组)和是否使用PATH环境变量进行区分。
为一个函数传递不定数量的字符串参数:
void foo(const char* arg, …); // 采用变长参数表
foo(NULL);
foo(“abc”, NULL);
foo(“abc”, “def”, NULL);
foo(“abc”, “def”, …, NULL);
void bar(const char* arg[]); // 采用字符指针数组
const char* a[] = {NULL};
bar(a);
const char* a[] = {“abc”, NULL};
bar(a);
const char* a[] = {“abc”, “def”, NULL};
bar(a);
const char* a[] = {“abc”, “def”, …, NULL};
bar(a);
exec函数族区分
exec(执行)+
l - list,以变长参数表的形式传入命令行参数
p - path,使用PATH环境变量寻找可执行文件
e - environ,以字符指针数组的形式传入环境变量
v - vector,以字符指针数组的形式传入命令行参数
execl函数
#include <unistd.h>
int execl(const char* path, const char* arg, …);(常用)
path – 可执行文件的路径
arg – 命令行参数例如: execl("/usr/bin/gcc", “gcc”, “hello.c”, “-o”, “hello”,NULL);
$ gcc hello.c -o hello
gcc -> argv[0] gcc这里只为占argv[0],与命令行规则对上
hello.c -> argv[1]
-o -> argv[2]
hello -> argv[3]
用arg, …作为命令行参数,运行path所表示的可执行文件,创建新进程,并用新进程取代调用进程。
成功不返回,失败返回-1。
execlp函数
#include <unistd.h>
int execlp(const char* file, const char* arg, …);
通过file参数传入可执行文件的名字即可,无需带路径,该函数会遍历PATH环境变量中的所有路径,寻找可执行文件。例如: execlp(“gcc”, “gcc”, “hello.c”, “-o”, “hello”, NULL);
execle函数
#include <unistd.h>
int execle(const char* path, const char* arg, …, char* const envp[]);
envp-- 环境变量(以NULL结尾的字符指针数组)
前面两个是继承调用继承的环境变量,这个可以自己指定环境变量
execv函数
#include <unistd.h>
int execv(const char* path, char* const argv[]);例如:char* const a[] = {“gcc”, “hello.c”, “-o”, “hello”, NULL}
“gcc” argv[0]
“hello.c” argv[1]
“-o” argv[2]
“hello” argv[3]
execv("/usr/bin/gcc", a);
execvp函数
#include <unistd.h>
int execvp(const char* file, char* const argv[]);
execve函数
#include <unistd.h>
int execve(const char* path, char* const argv[], char* const envp[]);
#include <stdio.h>
#include <unistd.h>
void pargv (char* argv[])
{
printf("--- 命令行参数 ---\n");
while (argv && *argv)
printf("%s\n", *argv++);
printf("------------------\n");
}
void penvp(char* envp[])
{
printf("---- 环境变量 ----\n");
while(envp && *envp)
printf("%s\n", *envp++);
printf("------------------\n");
}
int main(int argc, char* argv[], char* envp[])
{
printf("argenv的PID:%d\n", getpid());
pargv(argv);
penvp(envp);
return 0;
}
#include <stdio.h>
#include <stdio.h>
#include <unistd.h>
int main(void)
{
printf("exec的PID:%d\n", getpid());
printf("准备调用exec函数...\n");
/*
if (execl("./argenv", "argenv", "ABC",
"123", "Hello, World!", NULL) == -1) {
perror("execl");
return -1;
}
*/
char* argv[] = {"argenv", "ABC", "123",
"Hello, World!", NULL};
/*
if (execv("./argenv", argv) == -1) {
perror("execv");
return -1;
}
*/
char* envp[] = {"NAME=minwei",
"SCHOOL=tarena", NULL};
/*
if (execle("./argenv", "argenv", "ABC",
"123", "Hello, World!", NULL,
envp) == -1) {
perror("execle");
return -1;
}
*//*
if (execve("./argenv", argv, envp) == -1) {
perror("execve");
return -1;
}
*//*
if (execlp("argenv", "argenv", "ABC",
"123", "Hello, World!", NULL) == -1) {
perror("execlp");
return -1;
}
*/
if (execvp("argenv", argv) == -1) {
perror("execvp");
return -1;
}
printf("exec函数返回成功!\n");//只有失败时才能被执行到,因为exec函数族成功是不返回的
return 0;
}
exec函数与fork或vfork函数区别
与fork或vfork函数不同,exec函数不是创建调用进程的子进程,而是创建一个新的进程取代调用进程自身。新进程会用自己的全部地址空间,覆盖调用进程的地址空间,但进程的PID保持不变。调用exec函数不仅改变调用进程的地址空间和进程映像,调用进程的一些属性也发生了变化:
1)任何处于阻塞状态的信号都会丢失;
2)被设置为捕获的信号会还原为默认操作;
3)有关线程属性的设置会还原为缺省值;
4)有关进程的统计信息会复位;
5)与进程内存有关的任何数据都会丢失,包括内存映射文件;
6)标注库在用户空间维护的一切数据结构,如通过malloc函数族动态分配的堆内存,通过atexit/on_exit函数注册的退出处理函数等,都会丢失。
但有些属性会被新进程继承下来,比如PID、PPID、实际用户ID和实际组ID、优先级,以及文件描述符(除非该文件描述符带有FD_CLOEXEC标志位)等。
vfork+exec模式
调用exec函数固然可以创建出新的进程,但是新进程会取代原来的进程。如果既想创建新的进程,同时有希望原来的进程继续存在,则可以考虑使用vfork+exec模式,即在由vfork产生的子进程中调用exec函数,新进程取代了子进程,但父进程依然存在。
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
int main(void)
{
srand(time(NULL));
setbuf(stdout, NULL);
for (int i = 0; i < 100; ++i) {
printf("+");
usleep((rand() % 100) * 1000);
}
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void)
{
srand(time(NULL));
setbuf(stdout, NULL);
pid_t pid = vfork();
//pid_t pid = fork();
if (pid == -1) {
perror("vfork");
return -1;
}
if (pid == 0) {
if (execl("./ve1", "ve1", NULL) == -1) {
perror("execl");
_exit(-1);
}
/*这种也是顺序执行不是并行,因为vfork会挂起父进程,直至子进程终止或exec函数族调用才恢复
for (int i = 0; i < 100; ++i) {
printf("+");
usleep((rand() % 100) * 1000);
}
_exit(0);
*/
}
for (int i = 0; i < 100; ++i) {
printf("-");
usleep((rand() % 100) * 1000);
}
if (waitpid(pid, NULL, 0) == -1) {
perror("waitpid");
return -1;
}
return 0;
}
如果一个进程可以根据用户的输入创建不同的进程,并在所建 进程结束以后继续重复这个过程,那么这个进程就是Shell
Shell原理:显示提示符并等待用户输入 --> 输入:ls -l --> 调用vfork函数创建子进程 --> 子进程(ls)根据用户的输入调用exec函数创建新进程(/bin/ls) --> (父进程中)调用waitpid函数 --> 等待并回收子进程
系统函数,相当于在Shell进程给命令
system=vfork+exec+waitpid
#include <stdlib.h>
int system(const char* command);
成功返回command命令行进程的终止状态,失败返回-1。
command - 命令行字符串
如果调用vfork或waitpid函数出错,返回-1。
如果调用exec函数出错,返回127。
如果都成功,返回command进程的终止状态,由waitpid函数的status参数输出。
如果command参数取NULL指针,该函数返回-1表示失败,返回其它非0(真)值表示当前Shell可用,返回0(假)表示不可用。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void)
{
int status;
if ((status = system(NULL)) == -1) {
perror("system");
return -1;
}
if (status)
printf("Shell可用。\n");
else {
printf("Shell不可用。\n");
return -1;
}
if ((status = system("ls -l")) == -1) {
perror("system");
return -1;
}
printf("退出码:%d\n", WEXITSTATUS(status));
if ((status = system("ps u")) == -1) {
perror("system");
return -1;
}
printf("退出码:%d\n", WEXITSTATUS(status));
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
/*
* command: ls -l
* argv -> * -> ls
* * -> -l
* NULL
*/
char** getargv(const char* command)
{
char cmd[strlen(command) + 1];
strcpy(cmd, command);
int argc = 0;
char** argv = NULL;
char* token;
char* sep = " \t\n";
for (token = strtok(cmd, sep); token;
token = strtok(NULL, sep)) {
argv = realloc(argv, (++argc) * sizeof(
*argv));
argv[argc - 1] = malloc((strlen(token) +
1) * sizeof(*token));
strcpy(argv[argc - 1], token);
}
argv = realloc(argv, (++argc) * sizeof(
*argv));
argv[argc - 1] = NULL;
return argv;
}
void freeargv(char** argv)
{
for (int i = 0; argv[i]; ++i)
free(argv[i]);
free(argv);
}
int mysystem(const char* command)
{
char** argv = getargv(command);
pid_t pid = vfork();
if (pid == -1) {
perror("vfork");
freeargv(argv);
return -1;
}
if (pid == 0)
if (execvp(argv[0], argv) == -1) {
perror("execvp");
_exit(127);
}
int status;
if (waitpid(pid, &status, 0) == -1) {
perror("waitpid");
freeargv(argv);
return -1;
}
freeargv(argv);
return status;
}
int main(void)
{
int status;
if ((status = mysystem("ls -l")) == -1) {
perror("system");
return -1;
}
printf("退出码:%d\n", WEXITSTATUS(status));
if ((status = mysystem("ps u")) == -1) {
perror("system");
return -1;
}
printf("退出码:%d\n", WEXITSTATUS(status));
/*
char** argv = getargv(
"gcc -c hello.c -o hello.o");
char** pp = argv;
while (pp && *pp)
printf("%s\n", *pp++);
freeargv(argv);
*/
return 0;
}