C语言进程的相关操作
进程简介
- 每个进程都有一个非负整数形式到的唯一编号,即PID(Process Identification,进程标识)
- PID在任何时刻都是唯一的,但是可以重用,当进程终止并被回收以后,其PID就可以为其他进程使用
- 进程的PID由系统内核根据延迟重用算法生成,以确保新进程的PID不同于最近终止进程到的PID
- 其中0号进程,叫做交换进程,系统内核中的一部分,所有进程的根进程,磁盘上没有它的可执行文件
- 1号进程是init进程,在系统自举过程结束时由调度进程创建,读写与系统相关的初始化文件,引导系统至一个特定状态,以超级用户特权运行的普通进程,永不终止
- 除去调度进程以外,系统中的每个进程都有一个唯一的父进程,对任何一个子进程而言,其父进程的PID即是它的PPID
- 下面这些函数都包含在
unistd.h
头文件中 pid_t getpid(void);
返回调用进程的PIDpid_t getppid(void);
返回调用进程的父进程的PIDuid_t getuid(void);
返回调用进程的实际用户IDgid_t getgid(void);
返回调用进程的实际组IDuid_t geteuid(void);
返回调用进程的有效用户IDgid_t getegid(void);
返回调用进程的有效组ID
创建子进程
-
创建子进程的函数包含在
unistd.h
头文件中 -
fork
函数pid_t fork(void);
- 功能:创建调用进程的子进程
- 返回值:失败返回-1,成功情况下返回的变量在父进程中是PID,在子进程中是0
- 可以通过这个返回值来执行父进程和子进程
- 当系统中的总的线程数达到了上限,或者用户的总进程达到了上限,
fork
函数会失败。
-
创建子进程示例代码
#include <stdio.h> #include <unistd.h> int main(void) { printf("haha\n"); // 创建子进程 int pid = fork(); printf("heihei\n"); return 0; } /* haha heihei heihei */
父子进程间的关系
-
以下是父子进程中数据相关
copy
的示例图
-
验证上图
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <time.h> int global = 100; // 父进程全局变量->数据区 int main(void) { int local = 200; // 父进程局部变量->栈区 int *heap = malloc(sizeof(int)); // 动态分配内存->堆区 *heap = 3; printf("父进程第一次打印: PID->%d %p->%d %p->%d %p->%d\n", getpid(), &global, global, &local, local, heap, *heap); // 创建子进程 pid_t pid = fork(); if(pid == 0) { // 子进程操作,数据会从父进程copy一份过来,这里执行++操作 printf("子进程打印: PID->%d PPID->%d %p->%d %p->%d %p->%d\n", getpid(), getppid(), &global, ++global, &local, ++local, heap, ++*heap); return 0; } sleep(1); // 这里等1s,让子进程++ printf("父进程第二次打印: PID->%d %p->%d %p->%d %p->%d\n", getpid(), &global, global, &local, local, heap, *heap); return 0; } /* 父进程第一次打印: PID->1674604 0x5577e1acc010->100 0x7ffd4d4bfaa8->200 0x5577e23422a0->3 子进程打印: PID->1674605 PPID->1674604 0x5577e1acc010->101 0x7ffd4d4bfaa8->201 0x5577e23422a0->4 父进程第二次打印: PID->1674604 0x5577e1acc010->100 0x7ffd4d4bfaa8->200 0x5577e23422a0->3 这里的父进程和子进程地址一样是虚拟地址里面一样,因为每个进程都有一个独立的虚拟地址池,相互不影响的 发现子进程跟父进程互相不影响,验证了上图的案例 */
-
父子进程操作文件,其实是共享一个文件表项的
-
验证上图
#include <stdio.h> #include <string.h> #include <fcntl.h> #include <unistd.h> #include <time.h> int main(void) { // 父进程打开文件 int fd = open("./test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0664); if(fd == -1) { perror("open"); return -1; } // 父进程写入数据 char *data = "hello bhlu!"; if(write(fd, data, strlen(data)) == -1) { perror("write"); return -1; } // 创建子进程 pid_t pid = fork(); if(pid == 0) { // 子进程修改文件读写位置 if(lseek(fd, -5, SEEK_END) == -1) { perror("lseek"); return -1; } return 0; } // 再次插入数据,验证子进程修改的读写位置是否生效 sleep(1); // 先等1s,让子进程执行完 data = "linux\n"; if(write(fd, data, strlen(data)) == -1) { perror("write"); return -1; } // 关闭文件 close(fd); return 0; } /* cat test.txt hello linux 发现是修改成功的,说明上图是对的,子进程和父进程共用一个文件表项 */
进程的终止
以下内容只是简单的介绍进程的终止,以便理解
-
进程的终止分为两种
-
正常终止:分为三种情况
-
main函数中正常返回
-
使用
exit
函数终止:exit
函数可以在任何函数中执行令进程结束,return
语句只有在main
函数中执行才能令进程结束#include <stdlib.h> void exit(int status); /* 功能: 令进程终止 参数: status 进程的退出码,相当于main函数的返回值 无返回值 */ /* exit函数在终止前会做以下几件收尾工作 1. 调用实现通过atexit或on_exit函数注册的函数退出函数 2. 冲刷并关闭所有仍处于打开状态的标准I/O流 3. 删除所有通过tmpfile函数创建的临时文件 4. 执行_exit(status); 使用exit函数令进程终止,通常使用EXIT_SUCCESS和EXIT_FAILUR两个宏 EXIT_SUCCESS -> 1; EXIT_FAILUR -> 0; */
-
调用
_exit/_Exit
函数令进程终止// _exit函数 #include <unistd.h> void _exit(int status); /* 参数: status 进程的退出码,相当于main函数的返回值 无返回值 */ // _Exit函数 #include <stdlib.h> void _Exit(int status); /* 参数: status 进程的退出码,相当于main函数的返回值 无返回值 */ /* _exit函数在终止前会做以下几件收尾工作 1. 关闭所有仍处于打开状态的文件描述符 2. 将调用进程的所有子进程托付过init进程 3. 向调用进程的父进程发送SIGCHLD(7)信号 4. 令调用进程终止运行,将status的低八位作为退出码保存在其终止状态中 */
-
-
异常终止
-
进程执行了系统认为具有危险性的操作时,或者系统本身发生故障或意外,内核会向进程发送特定的信号
SIGILL(4) -> 进程试图执行非法指令 SIGBUS(7) -> 硬件或对齐错误 SIGEPE(8) -> 浮点异常 SIGSEGV(11) -> 无效内存访问 SIGPWR(30) -> 系统供电不足
-
人为触发信号
SIGINT(2) -> Ctrl+c SIGQUIT(3) -> Ctrl+\ SIGKILL(9) -> 不能被捕获或忽略的进程终止信号 SIGTERM(15) -> 可以被捕获或忽略的进程终止编号
-
向进程自己发送信号
#include <stdlib.h> void abort(void); /* 功能: 想进城发送SIGABRT(6)信号,该信号默认情况下可以使进程结束 无返回值 */
-
-
在使用
exit
函数或main
函数正常退出时,如果注册了atexit
或on_exit
,那就会触发退出函数,以下是示例代码#include <stdio.h> #include <stdlib.h> void func(void) { exit(6); } void goto1(void) { printf("goto1\n"); } void goto2(int status, void *arg) { printf("status = %d\n", status); printf("arg = %s\n", (char *)arg); } int main(void) { atexit(goto1); // 退出之前执行goto1 on_exit(goto2, "heihei"); // 退出之前执行goto2,可以传参 func(); return 0; } /* 相当于钩子函数,在退出之前执行,可以进行一些回收操作 status = 6 arg = heihei goto1 */
回收子进程
- 如果不回收子进程的话,会导致有很多僵尸进程的存在,从而消耗更多的系统资源。
- 父进程需要等待子进程到的终止,以继续后续工作
- 父进程需要了解子进程终止的原因,是正常终止,还是异常终止
阻塞回收
-
wait
函数是用于回收子进程的一个函数,它使用的是阻塞回收,使用它必须包含sys/wait.h
头文件 -
wait
函数-
pid_t wait(int *status);
-
功能:等待和回收任意子进程
-
参数:
status
用于输出子进程的终止状态,可置NULL
-
补充:可以使用以下工具宏分析子进程的终止状态
if(WIFEXITED(status)) // 真 printf("正常终止: 进程退出码是%d\n", WEXITSTATUS(status)); else // 假 printf("异常终止: 终止进程的信号是%d\n", WTERMSIG(status)); // 下面跟上面判断条件相反 if(WIFSIGNALED(status)) // 真 printf("异常终止: 终止进程的信号是%d\n", WTERMSIG(status)); else // 假 printf("正常终止: 进程退出码是%d\n", WEXITSTATUS(status));
-
-
返回值:成功返回回收的子进程PID,失败返回-1
-
-
-
简单代码示例
// 子进程的回收 #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) { printf("%d进程: 我是子进程!\n", getpid()); // sleep(5); // exit(3); // _exit(5); // return 2; // abort(); // 向进程发送信号异常结束 // 以下两句会造成内存无效访问,会返回11 char *p = NULL; *p = 123; } // 父进程等待回收子进程 printf("%d进程: 我是父进程!\n", getpid()); int s; // 用来输出所回收的子进程终止状态 pid_t childpid = wait(&s); if(childpid == -1) { perror("wait"); return -1; } printf("父进程回收了%d进程的僵尸!\n", childpid); // 根据返回值判断子进程是否是正常结束 if(WIFEXITED(s)) printf("正常结束: %d\n", WEXITSTATUS(s)); else printf("异常结束: %d\n", WTERMSIG(s)); return 0; }
-
以下代码是一个循环创建5个进程,然后父进程挨个回收
#include <stdio.h> #include <unistd.h> #include <sys/wait.h> #include <errno.h> int main(void) { printf("%d进程: 我是父进程!\n--------------------------\n", getpid()); sleep(1); // 创建子进程 for(int i = 0; i < 5; i++) { pid_t pid = fork(); if(pid == -1) { perror("fork"); return -1; } // 子进程操作 if(pid == 0) { printf("%d进程: 我是子进程!\n", getpid()); sleep(i+1); return i+1; } } // 父进程操作: 回收子进程 while(1) { int s; // 用户接收子进程的终止状态 pid_t childpid = wait(&s); if(childpid == -1) { if(errno == ECHILD) { printf("没有子进程可以回收了!\n"); break; } else { perror("wait"); return -1; } } // 判断子进程的终止状态 if(WIFEXITED(s)) printf("正常结束: %d\n", WEXITSTATUS(s)); else printf("异常终止: %d\n", WTERMSIG(s)); } return 0; }
非阻塞回收
-
waitpid
函数一般用于非阻塞回收子进程,还可以回收特定子进程,使用这个函数需要引用sys/wait.h
头文件 -
waitpid
函数pid_t waitpid(pid_t pid, int *status, int options);
- 功能:等待并回收任意或特定子进程
- 参数
pid
:取-1等待并回收任意子进程,相当于wait
函数,>0等待回收特定子进程status
:用于输出子进程的终止状态,可置NULL
option
:0代表阻塞模式,WNOHANG
代表非阻塞模式,如果等待的进程还在运行,则返回0
- 返回值:成功返回回收子进程的PID或者0,失败返回-1
-
以下是使用非阻塞回收的方法回收子进程
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/wait.h> #include <errno.h> int main(void) { printf("%d进程: 我是父进程!\n-------------------------\n", getpid()); // 创建子进程 for(int i = 0; i < 5; i++) { pid_t pid = fork(); if(pid == -1) { perror("fork"); return -1; } // 子进程相关操作 if(pid == 0) { printf("%d进程: 我是子进程!\n", getpid()); sleep(i+1); // 三种效果 if(i == 3) { abort(); } else if(i == 4) { char *p = NULL; *p = 123; } else { return i+1; } } } // 父进程回收子进程 sleep(1); while(1) { int s; // 用于保存进程的终止状态 pid_t childpid = waitpid(-1, &s, WNOHANG); // 这里使用的是非阻塞模式 if(childpid == -1) { // 报错或者没有子进程了 if(errno == ECHILD) { printf("没有子进程了!\n"); break; } else { perror("waitpid"); return -1; } } else if(childpid == 0) { // 子进程还在运行 printf("子进程在运行,无法回收,先睡会!\n"); sleep(2); } else { // 回收成功并判断是否正常终止 printf("%d子进程回收成功!\n", childpid); if(WIFEXITED(s)) printf("%d进程正常终止, 进程退出码: %d\n\n", childpid, WEXITSTATUS(s)); else printf("%d进程异常终止, 终止进程信号: %d\n\n", childpid, WTERMSIG(s)); } } return 0; } /* 代码执行效果 1761797进程: 我是父进程! ------------------------- 1761798进程: 我是子进程! 1761799进程: 我是子进程! 1761800进程: 我是子进程! 1761801进程: 我是子进程! 1761802进程: 我是子进程! 子进程在运行,无法回收,先睡会! 1761798子进程回收成功! 1761798进程正常终止, 进程退出码: 1 1761799子进程回收成功! 1761799进程正常终止, 进程退出码: 2 子进程在运行,无法回收,先睡会! 1761800子进程回收成功! 1761800进程正常终止, 进程退出码: 3 1761801子进程回收成功! 1761801进程异常终止, 终止进程信号: 6 子进程在运行,无法回收,先睡会! 1761802子进程回收成功! 1761802进程异常终止, 终止进程信号: 11 没有子进程了! */
补充
- 实际情况下,无论进程是正常终止还是异常终止,都会通过系统内核向其父进程发送一个SIGCHLD(17)信号,我们可以提供一个针对该信号的处理函数,在信号处理函数中异步的方式回收子进程,这样不仅流程简单,回收效率还高,僵尸进程的存活时间也会很短。
创建新进程
与fork函数不同,这里使用的exec
函数是创建一个新的进程,新进程会取代调用自身的进程,新进程覆盖之前的进程地址空间,进程的PID不会改变。
-
exec
不是一个函数,而是一堆函数,功能一样,用法相似 -
#include <unistd.h>
-
int execl(const char *path, const char *arg, ...);
execl("/bin/ls", "ls", "-a", "-l", NULL); /* path使用的是路径名 使用NULL作为arg的结尾 失败返回-1,成功不返回 */
-
int execlp(const char *file, const char *arg, ...);
execlp("ls", "ls", "-a", "-l", NULL); /* file使用的是文件名,会从环境变量中一个个的找 使用NULL作为arg的结尾 失败返回-1,成功不返回 */
-
int execle(const char *path, const char *arg, ..., char *const envp[]);
char *envp[] = {"NAME=bhlu", "AGE=25", NULL}; execle("/usr/bin/env", "env", NULL, envp); /* 比excel多一个envp,用于设置环境变量,它设置什么,新进程的环境变量就只有什么 失败返回-1,成功不返回 环境变量输出: NAME=bhlu AGE=25 */
-
int execv(const char *path, char *const argv[]);
char *argv[] = {"ls", "-a", "-l", NULL}; execv("/bin/ls", argv); /* execv系列使用的都是字符指针数组,字符数组是以NULL结尾 失败返回-1,成功不返回 */
-
int execvp(const char *file, char *const argv[]);
char *argv[] = {"ls", "-a", "-l", NULL}; execvp("ls", argv); /* 跟execv差不多,就第一个参数是文件名 失败返回-1,成功不返回 */
-
int execve(const char *path, char *const argv[], char *const envp[]);
char *argv[] = {"env", NULL}; char *envp[] = {"NAME=bhlu", "AGE=25", NULL}; execve("/usr/bin/env", argv, envp); /* 跟execle函数差不多,就是这里的第二个参数是字符指针数组 失败返回-1,成功不返回 */
-
-
后缀不同,代码的含义也不同
l
:即list
,新进程的命令以字符指针列表形式传入,列表以空指针结束p
:即path
:第一个参数,不包含/
,就根据PATH
环境变量搜索文件e
:即environment
:设定环境变量,不指定则从调用进程复制v
:即vector
:新进程的命令行参数以字符指针数组的形式传入,数组以空指针结束- 实际底层最后使用的都是
execve
函数
-
使用
exec
函数基本会将原进程的所有信号、属性、数据等都丢失或者恢复初识状态,只有PID
、PPID
、UID
等会被继承下来。 -
一般都会先创建一个子进程,然后在子进程中使用
exec
函数,以下是相关示例#include <stdio.h> #include <unistd.h> int main(void) { // 创建子进程 pid_t pid = fork(); if(pid == -1) { perror("fork"); return -1; } // 子进程相关操作 if(pid == 0) { char *argv[] = {"env", NULL}; if(execvp("/bin/env", argv) == -1) { perror("execvp"); return -1; } } // 父进程操作 printf("父进程PID: %d\n", getpid()); return 0; }
system
-
下面介绍的是
c
语言执行shell
命令的函数 -
#include <stdlib.h>
int system(const char *command);
- 功能:执行
shell
命令 - 参数:
shell
命令,如果参数取NULL
,返回非0表示Shell
可用,返回0表示不可用 - 返回值:成功返回
command
进程的终止状态, 失败返回-1
- 功能:执行
-
代码实例
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(void) { int s = system("echo $PATH"); if(s == -1) { perror("system"); return -1; } printf("父进程PID: %d\n", getpid()); return 0; }
-
system
函数内部调用了vfork
、exec
和waitpid
等函数,而且它是标准库函数,可以跨平台使用- 如果调用
vfork
或waitpid
函数出错,则返回-1 - 如果调用
exec
函数出错,则在子进程中执行exit(127)
- 如果都成功,会从
waitpid
获取command
进程的终止状态
- 如果调用