文章目录
前言
在Linux中程序的运行涉及进程的相关知识,熟悉并掌握其相关知识在嵌入式Linux应用开发中至关重要。本篇记录进程的具体知识,若涉及版权问题,请联系本人删除!
一、进程的概念与结构
1. 相关概念
- 程序:存放在硬盘的可执行文件。
- 进程:是程序运行的实例,每个进程都有一个虚拟地址空间。进程之间相互独立,同时也存在相关机制来进行进程的通信。每个Linux进程都有唯一的进程ID(PID),其都是正整数。
- 并发:虚假的同时运行多个进程,是单CPU切换速度极快的结果。
- 并行:真实的同时运行多个进程,有多个CPU。
- 命令:①如下图,通过命令"ps -aux"可以查看进程信息。②用kill -9可以强制退出进程。
2. 内核区中的进程结构
每启动一个进程,在虚拟地址空间的内核区中就会对应一个task_struct结构体(进程控制块PCB),如下图所示。其中包含了进程的ID、状态、优先级、调度策略、文件结构体指针(指向文件描述符表)等等。
3. 进程的状态
有五种常见状态:创建态、就绪态、运行态、阻塞态(挂起态)和退出态(终止态)。
- 创建态:进程在创建时就是该状态,时间很短。
- 就绪态:创建后就处于该状态,等待抢夺CPU时间片。
- 运行态:获得CPU资源使得该进程运行,当时间片用完后重新回到就绪态。
- 阻塞态:进程强制放弃CPU,无法抢夺CPU时间片(例如sleep在休眠期间)。同时,阻塞态又分为不可中断和可中断类型。(执行中按下Ctrl+C能中断的是可中断类型)
- 退出态:进程的终止,占用的系统资源被释放。(任何状态都可以直接转换为退出态)
僵尸状态:进程已经终止了,用户区资源已经被释放了,但是内核区中的task_struct仍有信息,ps的命令中STAT值为Z。
4. 获取进程ID函数
#include <unistd.h>
#include <sys/types.h>
当前进程ID: pid_t getpid(void);
当前进程的父进程ID: pid_t getppid(void);
当前进程的实际用户ID: uid_t getuid(void);
当前进程的有效用户ID: uid_t geteuid(void);
当前进程的用户组ID: gid_t getgid(void);
当前进程的进程组ID: pid_t getpgrp(void);
进程ID为pid的进程组ID: pid_t getpgid(pid_t pid);
【注】实际用户是当前环境下的用户,有效用户是真正开启进程的用户
5. 进程组
【1】介绍:进程组就是多个进程的集合。每个进程组都有个组长,组长就是进程组中第一个进程。组长的PID等于进程组的ID。只有当进程组中的所有进程都退出或者转移了,这个进程组才会消失。
【2】创建进程组/转移进程到某个进程组:
int setpgid(pid_t pid, pid_t pgid);
//参数说明:
//pid表示进程的ID号
//pgid表示进程组的ID号,若pgid不存在则创建进程组
//返回值:成功返回0,失败返回-1
【3】注意事项:
- 父进程fork出的子进程所在的进程组与父进程是同一个。
- 组长进程不能再创建新的进程组。
- 在组长进程ID前加上负号就是操作进程组。
- 一个进程只能为它自己或它的子进程设置进程组ID。
6. 会话
【1】介绍:会话(session)是由多个进程组所构成的。一个普通进程可以调用setsid函数使自己成为新会话的领头进程(会长),同时这个领头进程还会被放入到一个新的进程组中。
【2】相关函数:
#include <unistd.h>
//获取进程所属的会话ID
//当参数为0时,获取当前的会话ID
pid_t getsid(pid_t pid);
//将某个进程变成会话,得到守护进程
//哪个进程调用,哪个进程就会变成一个会话
//返回值为新会话的ID,失败为-1
pid_t setsid(void);
【3】注意事项:
- 调用setsid函数不能是进程组的组长,如果是则调用失败。为了保证调用成功,可以调用fork函数创建子进程,然后终止父进程,让子进程来调用setsid函数。
- 若调用的不是进程组的组长,那么调用会成功。当前进程就脱离了控制终端,因此不会阻塞终端。
- 当我们关闭终端(会话中的控制进程)时,系统会向该会话发送SIGHUP信号,会话将该信号发送给所有的子进程。子进程收到该信号后,就会终止进程。因此,关闭终端时所有的子进程均会终止。
二、进程创建
1. fork和vfork函数
【1】头文件:#include <sys/types.h>、#include <unistd.h>
【2】函数原型:①pid_t fork(void); ②pid_t vfork(void);
【3】功能:
- fork创建子进程,且子进程复制父进程的内存空间。子、父进程谁先运行看进程调度。
- vfork创建子进程,子进程先运行且不复制父进程空间。
2. 额外注意点
- fork和vfork被调用一次,会返回两次:子进程中的返回值为0,在父进程中的返回值则是子进程的PID。可以根据返回值不同来区分是父进程还是子进程。
- 失败返回值:创建子进程失败会返回-1。
- 执行位置:父进程是从main函数代码体首部开始执行,子进程是从fork函数之后开始执行。
- 虚拟地址空间的用户空间:子进程中代码段与环境变量的物理空间和父进程是同一个。而其他的物理空间不是同一个(而是将父进程的复制一份给子进程),即使它们的虚拟地址是一样的。
- 虚拟地址空间的内核空间:①子进程只复制父进程的文件描述符表,不复制但共享文件表项和inode。②父进程创建一个子进程后,文件表项中的引用计数器加1,当父进程close后计数器减1,子进程还是可以使用文件表项,只有当计数器为0时才会释放文件表项。
实验程序1:创建子进程,打印子、父进程中的pid信息。
#include <stdio.h> #include <sys/types.h> #include <unistd.h> int main(int argc, char **argv) { //fork创建子进程,复制父进程空间 pid_t pid = fork(); //子、父进程中打印pid if (pid < 0) { perror("创建子进程失败"); } else if (pid == 0) {//子进程 printf("I am child process. PID: %d, PPID: %d, 返回的PID: %d\n", getpid(), getppid(), pid); } else {//父进程 printf("I am parent process. PID: %d, PPID: %d, 返回的PID: %d\n", getpid(), getppid(), pid); } return 0; }
实验程序2:父进程将文件指针定位到文件尾部,子进程写入内容。原有目录下有文件1.txt,原有内容为123
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <string.h> int main(int argc, char **argv) { //命令行参数判定 if (argc != 2) { printf("Command: %s <filename>\n", argv[0]); return -1; } //文件操作 int fd = open(argv[1], O_WRONLY); if (fd < 0) { perror("文件打开错误"); return -1; } //父进程改变文件指针到文件尾部 //子进程等待父进程定位好后写入内容 pid_t pid = fork(); if (pid < 0) { perror("创建子进程错误"); close(fd); return -1; } else if (pid > 0) {//父进程 if (lseek(fd, 0, SEEK_END) < 0) { perror("文件指针定位错误"); close(fd); return -1; } } else {//子进程 sleep(2);//确保父进程先运行 const char * content = "Hello, Can!\n"; int contentSize = strlen(content); if (write(fd, content, contentSize) < contentSize) { printf("写入错误\n"); close(fd); return -1; } } printf("--------pid: %d完成工作---------\n", getpid()); //关闭文件:父子进程都会关闭,使得引用计数减为0 close(fd); return 0; }
3. 构建进程链
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char **argv)
{
//创建3个子进程,形成进程链
for (int i = 0; i < 3; ++i) {
pid_t pid = fork();
if (pid < 0) {
perror("创建失败");
return -1;
}
if (pid > 0) { //若为父进程则退出
break;
}
}
printf("PID: %d, PPID: %d\n", getpid(), getppid());
sleep(1);
return 0;
}
4. 构建进程扇
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char **argv)
{
//创建3个子进程,形成进程扇
for (int i = 0; i < 3; ++i) {
pid_t pid = fork();
if (pid < 0) {
perror("创建失败");
return -1;
}
if (pid == 0) {//若为子进程则退出
break;
}
}
printf("PID: %d, PPID: %d\n", getpid(), getppid());
sleep(1);
return 0;
}
三、进程终止
1. C程序的启动过程
在main函数执行前,Linux内核会启动一个特殊例程,将命令行中的参数传给argc和argv。若主函数中有三个形参,那么该例程还会将环境信息构建成环境表传给第三个形参。最后,该例程还会登记进程的终止函数(进程终止前会调用)。
终止函数说明:
- 每个进程都默认登记了一个标准的终止函数。
- 终止函数在进程终止时释放一些资源。
- 登记的多个终止函数的执行顺序按照栈的方式执行。
- 用户自定义终止函数(无参无返回值),需要调用atexit函数向内核登记。
atexit函数:
【1】头文件:#include <stdlib.h>
【2】功能:向内核登记一个终止函数,该函数会在正常进程终止时被调用。
【3】函数原型:int atexit(void (*function)(void));
【4】返回值:成功返回0,否则返回非零值。
2. 进程终止方式
- 正常终止:
- ①main函数中return返回 会刷新标准IO缓存,会执行自定义的终止函数
- ②调用库函数exit(0) 会刷新标准IO缓存,会执行自定义的终止函数
- ③调用系统调用函数_exit(0)或_Exit(0) 不会刷新标准IO缓存,不会执行自定义的终止函数
- ④最后一个线程从其启动例程返回
- ⑤最后一个线程调用库函数pthread_exit
- 异常终止:
- ①调用库函数abort
- ②接收到信号并终止(例如段错误会产生一个信号,然后终止进程)
- ③最后一个线程对取消请求做处理响应
实验程序:运行下列代码,若参数指定为exit或return,文件中有写入的字符串,并且会执行自定义的终止函数;若参数指定为_exit,文件中没有任何内容,并且没有执行终止函数。
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> //自定义终止函数 void fun1() { printf("Terminate: fun1\n"); } void fun2() { printf("Terminate: fun2\n"); } void fun3() { printf("Terminate: fun3\n"); } //主函数 int main(int argc, char **argv) { //命令行参数判定 if (argc != 3) { printf("commnd: %s <filename> <exit | return | _exit>\n", argv[0]); return -1; } //登记自定义终止函数 atexit(fun1); atexit(fun2); atexit(fun3); //文件操作,忽视健壮性判定 FILE *fd = fopen(argv[1], "w");//文件不存在则创建,调用失败返回NULL fprintf(fd, "Hello, world!\n");//向文件缓冲区写入字符串,若没有刷新或fclose则不会写入硬盘 //根据参数选择退出方式 if (!strcmp(argv[2], "exit")) { exit(0); } else if (!strcmp(argv[2], "return")) { return 0; } else { _exit(0); } }
四、特殊的进程
1. 僵尸进程
- 概念:子进程的虚拟地址空间中的用户区资源已经释放,但内核区中的task_struct没有被释放,那么该进程就是僵尸进程。
- 释放僵尸进程的方式:
- ①结束或kill僵尸进程的父进程,那么僵尸进程就会成为孤儿进程,然后会被init进程(1号进程)领养,最终会被回收。
- ②让僵尸进程的父进程来回收。父进程每隔一段时间就查询子进程是否结束并回收,调用wait函数或waitpid函数,通过内核来释放僵尸进程。
- ③采用信号SIGCHLD通知处理,在信号处理函数中调用wait函数。
程序示例:运行如下程序,就会生成僵尸进程。
#include <stdio.h> #include <sys/types.h> #include <unistd.h> int main(int argc, char **argv) { //创建子进程 pid_t pid = fork(); if (pid < 0) { perror("创建子进程失败"); return -1; } //子进程退出,成为僵尸进程 if (pid == 0) { printf("PID: %d, PPID: %d\n", getpid(), getppid()); return -1; } //父进程循环,便于观察 while(1) { sleep(1); } return 0; }
2. 守护进程
【1】概念:是一种生存期很长的进程。从操作系统启动开始,在操作系统关闭时终止。
- 所有守护进程都以root(用户ID为0)的优先权运行。
- 守护进程没有控制终端,一直在后台运行。
- 守护进程的父进程都是init进程。
- 一般进程名后面带有 d 就表示它是一个守护进程。
【2】创建守护进程的步骤:见守护进程 | 爱编程的大丙 (subingwen.cn)。
3. 孤儿进程
- 概念:父进程结束了,但是子进程还在运行,那么此时子进程就是孤儿进程。孤儿进程由init进程(1号进程)来回收。
- 领养机制引入:进程的用户区资源可以自己释放,但是内核区资源需要由父进程释放。而孤儿进程的父进程已经结束。因此,为了释放孤儿进程的内核区资源,让1号进程来领养它,进而释放其内核区的task_struct结构体。
程序示例:通过fork创建子进程,同时让父进程退出,那么子进程就是孤儿进程。
#include <stdio.h> #include <sys/types.h> #include <unistd.h> int main(int argc, char **argv) { //创建子进程 pid_t pid = fork(); if (pid < 0) { perror("创建子进程失败"); return -1; } //父进程退出 if (pid > 0) { printf("PID: %d, PPID: %d\n", getpid(), getppid()); return -1; } //子进程成为孤儿进程 if (pid == 0) { sleep(2); printf("PID: %d, PPID: %d\n", getpid(), getppid()); return -1; } return 0; }
五、相关函数
1. wait函数
【1】头文件:#include <sys/types.h>、#include <sys/wait.h>
【2】函数原型:pid_t wait(int *wstatus);
【3】参数说明:wstatus是传出的参数,存放子进程退出时的信息。例如:wait(&status);
取出整形变量status中的数据需要使用一些宏函数:
- WIFEXITED(status)用于判定是否是正常结束,是的话返回真;WEXITSTATUS(status)取出对应的进程退出码。
- WIFSIGNALED(status)用于判定是否是异常结束,是的话返回真;WTERMSIG(status)取出对应的进程退出码。
- WIFSTOPPED(status)用于判定是否是暂停子进程的返回,是的话返回真;WSTOPSIG(status)取出对应的进程退出码。
【4】功能:父进程等待子进程退出并回收,避免僵尸进程和孤儿进程产生。
【5】返回值:成功则返回子进程的PID,失败返回-1。
【6】注意:wait函数等待所有的子进程退出。
示例程序:演示子进程异常退出,父进程对退出码进行处理。
#include <stdio.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <stdlib.h> int main(int argc, char **argv) { //创建子进程 pid_t pid = fork(); if (pid < 0) { perror("创建子进程失败"); return -1; } //子进程:打印信息,异常退出 if (pid == 0) { printf("PID: %d, PPID: %d\n", getpid(), getppid()); int i = 3, j = 0, k = i/j;//由于除0异常退出 } //父进程:阻塞等待子进程退出,将退出码保存 int status; pid_t ret = wait(&status); if (ret < 0) { printf("回收失败\n"); return 0; } else { printf("回收成功,子进程PID:%d\n", ret); } //父进程:处理退出码 if(WIFEXITED(status)) { printf("正常退出:%d\n", WEXITSTATUS(status)); } else if (WIFSIGNALED(status)) { printf("异常退出:%d\n", WTERMSIG(status)); } else if (WIFSTOPPED(status)) { printf("暂停退出:%d\n", WSTOPSIG(status)); } else { printf("未知退出\n"); } return 0; }
2. waitpid函数
【1】头文件:#include <sys/types.h>、#include <sys/wait.h>
【2】函数原型:pid_t waitpid(pid_t pid, int *wstatus, int options);
【3】参数说明:
- pid:
- -1:回收所有的子进程资源, 和wait()是一样的。
- >0:指定回收某一个进程的资源。
- 0:回收当前进程组的所有子进程。
- <-1:pid 的绝对值代表进程组ID,表示要回收这个进程组的所有子进程资源。
- wstatus:与wait一样。
- options:控制函数是阻塞还是非阻塞。
- 0:函数的行为是阻塞的。
- WNOHANG:函数的行为是非阻塞的。
- WUNTRACED:若某个pid子进程已暂停,并且其状态自从暂停以来没有报告过,就返回真。
【4】返回值:①若函数是非阻塞的,并且子进程还在运行就返回0;②成功返回子进程PID,失败返回-1.
【5】功能:是wait函数的升级版,可以指定为阻塞或非阻塞,可以等待一个进程或多个进程。
阻塞示例:可以达到与wait相同的效果。
#include <stdio.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <stdlib.h> int main(int argc, char **argv) { //创建子进程 pid_t pid = fork(); if (pid < 0) { perror("创建子进程失败"); return -1; } //子进程:打印信息,异常退出 if (pid == 0) { printf("PID: %d, PPID: %d\n", getpid(), getppid()); int i = 3, j = 0, k = i/j;//由于除0异常退出 } //父进程:阻塞等待子进程退出,将退出码保存 int status; pid_t ret = waitpid(-1, &status ,0); if (ret < 0) { printf("回收失败\n"); return 0; } else { printf("回收成功,子进程PID:%d\n", ret); } //父进程:处理退出码 if(WIFEXITED(status)) { printf("正常退出:%d\n", WEXITSTATUS(status)); } else if (WIFSIGNALED(status)) { printf("异常退出:%d\n", WTERMSIG(status)); } else if (WIFSTOPPED(status)) { printf("暂停退出:%d\n", WSTOPSIG(status)); } else { printf("未知退出\n"); } return 0; }
非阻塞示例:创建3个子进程形成进程扇,调用waitpid的非阻塞方式来回收子进程。
#include <stdio.h> #include <sys/types.h> #include <unistd.h> #include <sys/wait.h> int main(int argc, char **argv) { //创建3个子进程,形成进程扇 pid_t pid = getpid();//默认赋值为当前pid for (int i = 0; i < 3; ++i) { pid = fork(); if (pid == 0) { break; } } //父进程非阻塞释放所有子进程 if (pid== 0) {//子进程 printf("子进程PID: %d, PPID: %d", getpid(), getppid()); } else {//父进程 while (1) { int status; pid_t ret = waitpid(-1, &status, WNOHANG); if (ret < 0) { printf("回收失败,或者所有子进程已经被回收\n"); break; } else if (ret == 0) { printf("子进程运行中,继续等待\n"); } else { printf("回收成功!子进程PID: %d\n", ret); if (WIFEXITED(status)) { printf("子进程正常退出,退出状态码:%d\n", WEXITSTATUS(status)); } else if (WIFSIGNALED(status)) { printf("子进程异常退出,退出状态码:%d\n", WTERMSIG(status)); } else if (WIFSTOPPED(status)) { printf("子进程暂停退出,退出状态码:%d\n", WSTOPSIG(status)); } else { printf("未知退出\n"); } } } } return 0; }
3. execl函数
【1】背景:当我们想要在子进程中运行系统中其它的可执行程序,那么就可以调用exec族的函数。调用后,当前子进程的用户区数据全部被目标可执行程序所覆盖,只有内核区还残留之前的数据。(子进程算是一个外壳,运行的可执行程序才算是实体)
【2】头文件:#include <unistd.h>
【3】函数原型:int execl(const char *path, const char *arg, ...);
【4】参数说明:
- path:要启动的可执行程序的路径, 推荐使用绝对路径。
- arg:ps -aux查看进程时启动的进程的名字,一般和要启动的可执行程序名相同。
- ...:执行命令行所需要的参数,最后以NULL结尾表示结束。
【5】返回值:成功没有返回值,失败返回-1.
4. execlp函数
【1】功能:该函数常用于执行已经设置了环境变量的可执行程序。即该函数会自动搜索系统的环境变量PATH,因此该函数执行可执行程序不需要指定具体路径,只需指出名字。
【2】函数原型:int execlp(const char *file, const char *arg, ...);
【3】参数说明:file表示可执行程序的名字。其他参数与execl函数相同。
程序示例:创建子进程来执行ps -aux命令。
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> int main(int argc, char **argv) { //创建一个子进程 pid_t pid = fork(); if (pid < 0) { perror("创建子进程失败"); return -1; } //父进程执行 if (pid > 0) { printf("父进程执行中...\n"); pid_t ret = wait(NULL); if (ret < 0) { printf("回收子进程失败\n"); } else { printf("回收子进程成功,子进程PID: %d\n", ret); } } //子进程执行ps if (pid == 0) { execl("/bin/ps", "ps", NULL); printf("若执行该语句,则说明执行ps程序失败\n"); } return 0; }
5. system函数
【1】功能:简化execl函数的调用,不需要手动创建子进程。
【2】原理:system函数内部会构建一个子进程,由子进程调用exec族函数。实际上,使用 system()运行 shell命令需要至少创建两个进程,一个进程用于运行 shell、另外一个或多个进程则用于运行参数 command 中解析出来的命令,每一个命令都会调用一次 exec 函数来执行。
【3】头文件:#include <stdlib.h>
【4】函数原型:int system(const char *command);
【5】返回值:
- -1:无法创建子进程或无法获取子进程的终止状态
- 如果子进程不能执行 shell,则 system()的返回值就类似子进程调用_exit(127)终止
- 如果所有的系统调用都成功,返回执行command的shell进程的终止状态。
示例程序:调用system函数执行ps命令。
#include <stdlib.h> int main(int argc, char **argv) { system("ps -aux"); return 0; }
六、其它知识
1. 进程中的环境变量
进程的环境变量是从其父进程中继承过来的,譬如在 shell 终端下执行一个应用程序,那么该进程的环境变量就是从其父进程(shell 进程)中继承过来的。新的进程在创建之前,会继承其父进程的环境变量副本。环境变量存放在一个字符串数组中,在应用程序中,通过 environ 变量指向它, environ 是一个全局变量,在我们的应用程序中只需申明它即可使用。
程序实例:打印当前进程所有的环境变量。
extern char** environ;//声明环境变量数组 int main(int argc, char** argv) { for (int i = 0; environ[i] != NULL; ++i) { puts(environ[i]); } return 0; }
【1】获取指定环境变量:getenv函数
#include <stdlib.h>
char *getenv(const char *name);
//name: 环境变量名称
//返回值:若存在则返回对应的字符串指针,不存在返回NULL
//注意:不应该直接修改返回的字符串指针
/*******************************************************/
//程序实例:打印指定环境变量
int main(int argc, char** argv)
{
//命令行参数判定
if (argc != 2) {
printf("command: %s <variable>\n", argv[0]);
return -1;
}
//查看argv[1]环境变量的值
char* ret = getenv(argv[1]);
if (ret == NULL) {
printf("不存在该环境变量\n");
}
else {
printf("%s环境变量: %s\n", argv[1], ret);
}
return 0;
}
【2】新增环境变量:putenv函数
#include <stdlib.h>
int putenv(char *string);
//string: 新增的环境变量,形式为"name=value"
//返回值:成功返回0,失败返回非0值
//注意:调用成功后,environ数组中某个元素指向该变量本体
/*******************************************************/
//程序实例:新增指定环境变量
int main(int argc, char** argv)
{
//命令行参数判定
if (argc != 2) {
printf("command: %s <variable>\n", argv[0]);
return -1;
}
//新增环境变量argv[1]
if (putenv(argv[1]) != 0) {
printf("add fail\n");
return -1;
}
return 0;
}
【3】新增/修改环境变量:setenv函数(推荐)
#include <stdlib.h>
int setenv(const char *name, const char *value, int overwrite);
//name: 环境变量名称
//value: 环境变量的值
//overwrite: 若name已存在且该参数为0,则不覆盖;若name存在且该参数不为0,则覆盖。
若name不存在则添加环境变量。
//返回值:成功返回0,失败返回-1
//注意:该函数会开辟缓冲区,将name和value复制到该区域。
/*******************************************************/
//程序实例:新增指定环境变量
int main(int argc, char** argv)
{
//命令行参数判定
if (argc != 3) {
printf("command: %s <variable> <value>\n", argv[0]);
return -1;
}
//新增环境变量argv[1]
if (setenv(argv[1], argv[2], 0) != 0) {
printf("add fail\n");
return -1;
}
return 0;
}
【4】移除指定的环境变量:int unsetenv(const char *name);
【5】清空所有环境变量:int clearenv(void); 该函数是将environ赋值为NULL,若配合setenv函数新增环境变量就会造成内存泄漏(因为没有释放setenv里的缓冲区)。
2. 单例模式运行
若我们只想让某些程序在同一时间只能执行一次,那么该情况就需要单例模式运行。
方法一:(不靠谱)通过文件是否存在来判定。当第一次执行该程序时,创建一个标志文件,当程序执行完毕时再删除该文件。在该程序运行期间,再次执行该程序就会打开失败,从而形成单例模式运行。缺点:无法应对程序异常终止的情况。
方法二:(靠谱)文件锁。当程序启动之后,首先打开该文件,调用 open 时一般使用O_WRONLY | O_CREAT 标志,当文件不存在则创建该文件,然后尝试去获取文件锁,若是成功,则将程序的进程号PID写入到该文件中,写入后不要关闭文件或解锁(释放文件锁) ,保证进程一直持有该文件锁;若是程序获取锁失败,代表程序已经被运行、则退出本次启动。当程序退出或文件关闭之后,文件锁会自动解锁!
程序实例:
#include <stdio.h> #include <sys/types.h> #include <unistd.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/file.h> int main(int argc, char** argv) { //打开特定文件,不存在则创建 int fd = open("./test.pid", O_WRONLY | O_CREAT, 0664); if (fd < 0) { perror("open error"); exit(-1); } //尝试获取文件锁,获取失败(不能多次运行)则提示 if (flock(fd, LOCK_EX | LOCK_NB) == -1) { perror("Operation not permitted"); close(fd); exit(-1); } //程序执行 printf("running...\n"); //将进程PID写入文件中 char str[20] = {0}; sprintf(str, "%d\n", getpid()); write(fd, str, strlen(str) + 1); while (1) { sleep(10); } return 0; }