【嵌入式Linux】<总览> 多进程

文章目录

前言

一、进程的概念与结构

1. 相关概念

2. 内核区中的进程结构

3. 进程的状态

4. 获取进程ID函数

5. 进程组

6. 会话

二、进程创建

1. fork和vfork函数

2. 额外注意点

3. 构建进程链

4. 构建进程扇

 三、进程终止

1. C程序的启动过程

2. 进程终止方式

四、特殊的进程

1. 僵尸进程

2. 守护进程

3. 孤儿进程

五、相关函数

1. wait函数

2. waitpid函数

3. execl函数

4. execlp函数

5. system函数

六、其它知识

1. 进程中的环境变量

2. 单例模式运行


前言

在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;
}

  • 17
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值