Linux应用编程(进程)

一、进程与程序

注册进程终止处理函数 atexit()

#include <stdlib.h>
int atexit(void (*function)(void));

使用该函数需要包含头文件<stdlib.h>。

函数参数和返回值含义如下:
function:函数指针,指向注册的函数,此函数无需传入参数、无返回值。
返回值:成功返回 0;失败返回非 0。

测试

#include <stdio.h>
#include <stdlib.h>

static void bye(void)
{
	 puts("Goodbye!");
}
int main(int argc, char *argv[])
{
	 if (atexit(bye)) 
	 {
		 fprintf(stderr, "cannot set exit function\n");
		 exit(-1);
	 }
	 exit(0);
}

在这里插入图片描述
1、何为进程?

进程是一个动态过程,而非静态文件,它是程序的一次运行过程,当应用程序被加载到内存中运行之后它就称为了一个进程,当程序运行结束后也就意味着进程终止,这就是进程的一个生命周期。

2、进程号
Linux 系统下的每一个进程都有一个进程号(processID,简称 PID),进程号是一个正数,用于唯一标识系统中的某一个进程。在 Ubuntu 系统下执行 ps 命令可以查到系统中进程相关的一些信息,包括每个进程的进程号

在应用程序中,可通过系统调用 getpid()来获取本进程的进程号,其函数原型如下所示:

#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);

使用示例

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>

int main(void)
{
	 pid_t pid = getpid();
	 printf("本进程的 PID 为: %d\n", pid);
	 exit(0);
}

在这里插入图片描述
获取父进程

#include <sys/types.h>
#include <unistd.h>
pid_t getppid(void);

使用示例
用法如上。

二、进程的环境变量

在 shell 终端下可以使用 env 命令查看到 shell 进程的所有环境变量

export LINUX_APP=123456 # 添加 LINUX_APP 环境变量

export -n LINUX_APP # 删除 LINUX_APP 环境变量

1、应用程序中获取环境变量

在我们的应用程序中只需申明它即可使用,如下所示:

extern char **environ; // 申明外部全局变量 environ

测试

#include <stdio.h>
#include <stdlib.h>

extern char **environ;

int main(int argc, char *argv[])
{
	 int i;
	 /* 打印进程的环境变量 */
	 for (i = 0; NULL != environ[i]; i++)
	  	puts(environ[i]);
	 exit(0);
}

在这里插入图片描述
获取指定环境变量 getenv()

#include <stdlib.h>
char *getenv(const char *name);
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
	 const char *str_val = NULL;
	 if (2 > argc) 
	 {
		 fprintf(stderr, "Error: 请传入环境变量名称\n");
		 exit(-1);
	 }
	 /* 获取环境变量 */
	 str_val = getenv(argv[1]);
	 if (NULL == str_val) 
	 {
		 fprintf(stderr, "Error: 不存在[%s]环境变量\n", argv[1]);
		 exit(-1);
	 }
	 /* 打印环境变量的值 */
	 printf("环境变量的值: %s\n", str_val);
	 exit(0);
}

在这里插入图片描述2、添加/删除/修改环境变量

putenv()函数

#include <stdlib.h>
int putenv(char *string);

putenv()函数将设定 environ 变量(字符串数组)中的某个元素(字符串指针)指向该 string 字符串,而不是指向它的复制副本,因此,不能随意修改参数 string 所指向的内容,这将影响进程的环境变量,出于这种原因,参数 string 不应为自动变量(即在栈中分配的字符数组)。

测试

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
	 if (2 > argc) 
	 {
		 fprintf(stderr, "Error: 传入 name=value\n");
		 exit(-1);
	 }
	 /* 添加/修改环境变量 */
	 if (putenv(argv[1])) 
	 {
		 perror("putenv error");
		 exit(-1);
	 }
	 exit(0);
}

setenv()函数

#include <stdlib.h>
int setenv(const char *name, const char *value, int overwrite);
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
	 if (3 > argc) 
	 {
		 fprintf(stderr, "Error: 传入 name value\n");
		 exit(-1);
	 }
	 /* 添加环境变量 */
	 if (setenv(argv[1], argv[2], 0)) 
	 {
		 perror("setenv error");
		 exit(-1);
	 }
	 exit(0);
}

除了上面给大家介绍的函数之外,我们还可以通过一种更简单地方式向进程环境变量表中添加环境变量,用法如下:

NAME=value ./app

在执行程序的时候,在其路径前面添加环境变量,以 name=value 的形式添加,如果是多个环境变量,则在./app 前面放置多对 name=value 即可,以空格分隔。

unsetenv()函数

#include <stdlib.h>
int unsetenv(const char *name);

3、清空环境变量

通过将全局变量 environ 赋值为 NULL来清空所有变量。

environ = NULL;

也可通过 clearenv()函数来操作,函数原型如下所示:

#include <stdlib.h>
int clearenv(void);

clearenv()函数内部的做法其实就是将environ赋值为NULL。在某些情况下,使用setenv()函数和clearenv()函数可能会导致程序内存泄漏,前面提到过,setenv()函数会为环境变量分配一块内存缓冲区,随之称为进程的一部分;而调用 clearenv()函数时没有释放该缓冲区(clearenv()调用并不知晓该缓冲区的存在,故而也无法将其释放),反复调用者两个函数的程序,会不断产生内存泄漏。

4、环境变量的作用

环境变量常见的用途之一是在 shell 中,每一个环境变量都有它所表示的含义,譬如 HOME 环境变量表示用户的家目录,USER 环境变量表示当前用户名,SHELL 环境变量表示 shell 解析器名称,PWD 环境变量表示当前所在目录等,在我们自己的应用程序当中,也可以使用进程的环境变量。

三、进程的内存布局

C 语言程序一直都是由以下几部分组成的:
1、正文段:这是 CPU 执行的机器语言指令部分,文本段具有只读属性。
2、初始化数据段:包含了显式初始化的全局变量和静态变量。
3、未初始化数据段:包含了未进行显式初始化的全局变量和静态变量。
4、栈:函数内的局部变量以及每次函数调用时所需保存的信息都放在此段中,函数传递的实参以及函数返回值等也都存放在栈中。
5、堆:可在运行时动态进行内存分配的一块区域,譬如使用 malloc()分配的内存空间。
在这里插入图片描述

四、进程的虚拟地址空间

在 Linux 系统中,每一个进程都在自己独立的地址空间中运行,在 32 位系统中,每个进程的逻辑地址空间均为 4GB,这 4GB 的内存空间按照 3:1 的比例进行分配,其中用户进程享有 3G 的空间,而内核独自享有剩下的 1G 空间。
在这里插入图片描述

在这里插入图片描述

所有应用程序运行在自己的虚拟地址空间中,使得进程的虚拟地址空间和物理地址空间隔离开来,这样做带来了很多的优点:
⚫ 进程与进程、进程与内核相互隔离。一个进程不能读取或修改另一个进程或内核的内存数据,这是因为每一个进程的虚拟地址空间映射到了不同的物理地址空间。提高了系统的安全性与稳定性。
⚫ 在某些应用场合下,两个或者更多进程能够共享内存。因为每个进程都有自己的映射表,可以让不同进程的虚拟地址空间映射到相同的物理地址空间中。通常,共享内存可用于实现进程间通信。
⚫ 便于实现内存保护机制。譬如在多个进程共享内存时,允许每个进程对内存采取不同的保护措施,例如,一个进程可能以只读方式访问内存,而另一进程则能够以可读可写的方式访问。
⚫ 编译应用程序时,无需关心链接地址。前面提到了,当程序运行时,要求链接地址与运行地址一致,在引入了虚拟地址机制后,便无需关心这个问题。

五、fork()创建子进程

使用 fork()创建子进程。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void)
{
	 pid_t pid;
	 pid = fork();
	 switch (pid) 
	 {
		 case -1:
			 perror("fork error");
			 exit(-1);
		 case 0:
			 printf("这是子进程打印信息<pid: %d, 父进程 pid: %d>\n",getpid(), getppid());
			 _exit(0); //子进程使用_exit()退出
		 default:
			 printf("这是父进程打印信息<pid: %d, 子进程 pid: %d>\n",getpid(), pid);
			 exit(0);
	 }
}

在这里插入图片描述
使用示例 2

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void)
{
	 pid_t pid;
	 pid = fork();
	 switch (pid) 
	 {
		 case -1:
			 perror("fork error");
			 exit(-1);
		 case 0:
			 printf("这是子进程打印信息\n");
			 printf("%d\n", pid);
			 _exit(0);
		 default:
			 printf("这是父进程打印信息\n");
			 printf("%d\n", pid);
			 exit(0);
	 }
}

在这里插入图片描述
在 exit()函数之前添加了打印信息,而从上图中可以知道,打印的 pid 值并不相同,0 表示子进程打印出来的,46953 表示的是父进程打印出来的,所以从这里可以证实,fork()函数调用完成之后,父进程、子进程会各自继续执行 fork()之后的指令,它们共享代码段,但并不共享数据段、堆、栈等,而是子进程拥有父进程数据段、堆、栈等副本,所以对于同一个局部变量,它们打印出来的值是不相同的,因为 fork()调用返回值不同,在父、子进程中赋予了 pid 不同的值。

六、父、子进程间的文件共享

调用 fork()函数之后,子进程会获得父进程所有文件描述符的副本,这些副本的创建方式类似于 dup(),这也意味着父、子进程对应的文件描述符均指向相同的文件表,如下图所示:
在这里插入图片描述
由此可知,子进程拷贝了父进程的文件描述符表,使得父、子进程中对应的文件描述符指向了相同的文件表,也意味着父、子进程中对应的文件描述符指向了磁盘中相同的文件,因而这些文件在父、子进程间实现了共享,譬如,如果子进程更新了文件偏移量,那么这个改变也会影响到父进程中相应文件描述符的位置偏移量。

父、子进程同时对文件进行写入操作,测试代码如下所示:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(void)
{
	 pid_t pid;
	 int fd;
	 int i;
	 
	 fd = open("./test.txt", O_RDWR | O_TRUNC);
	 if (0 > fd) 
	 {
		 perror("open error");
		 exit(-1);
	 }
	 
	 pid = fork();
	 switch (pid) 
	 {
		 case -1:
			 perror("fork error");
			 close(fd);
			 exit(-1);
		 case 0:
			 /* 子进程 */
			 for (i = 0; i < 4; i++) //循环写入 4 次
				 write(fd, "1122", 4);
			 close(fd);
			 _exit(0);
		 default:
			 /* 父进程 */
			 for (i = 0; i < 4; i++) //循环写入 4 次
				 write(fd, "AABB", 4);
			 close(fd);
			 exit(0);
	 }
} 

在这里插入图片描述
再来测试另外一种情况,父进程在调用 fork()之后,此时父进程和子进程都去打开同一个文件,然后再对文件进行写入操作,测试代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(void)
{
	 pid_t pid;
	 int fd;
	 int i;
	 pid = fork();
	 switch (pid) 
	 {
		 case -1:
			 perror("fork error");
			 exit(-1);
		 case 0:
			 /* 子进程 */
			 fd = open("./test.txt", O_WRONLY);
			 if (0 > fd) 
			 {
				 perror("open error");
				 _exit(-1);
			 }
			 for (i = 0; i < 4; i++) //循环写入 4 次
			 write(fd, "1122", 4);
			 close(fd);
			 _exit(0);
		 default:
			 /* 父进程 */
			 fd = open("./test.txt", O_WRONLY);
			 if (0 > fd) 
			 {
				 perror("open error");
				 exit(-1);
			 }
			 for (i = 0; i < 4; i++) //循环写入 4 次
				 write(fd, "AABB", 4);
			 close(fd);
			 exit(0);
	 }
}

在这里插入图片描述
这种文件共享方式实现的是一种两个进程分别各自对文件进行写入操作,因为父、子进程的这两个文件描述符分别指向的是不同的文件表,意味着它们有各自的文件偏移量,一个进程修改了文件偏移量并不会影响另一个进程的文件偏移量,所以写入的数据会出现覆盖的情况。

fork()函数使用场景

fork()函数有以下两种用法:
⚫ 父进程希望子进程复制自己,使父进程和子进程同时执行不同的代码段。这在网络服务进程中是常见的,父进程等待客户端的服务请求,当接收到客户端发送的请求事件后,调用 fork()创建一个子进程,使子进程去处理此请求、而父进程可以继续等待下一个服务请求。
⚫ 一个进程要执行不同的程序。譬如在程序 app1 中调用 fork()函数创建了子进程,此时子进程是要去执行另一个程序 app2,也就是子进程需要执行的代码是 app2 程序对应的代码,子进程将从 app2程序的 main 函数开始运行。这种情况,通常在子进程从 fork()函数返回之后立即调用 exec 族函数来实现,关于 exec 函数将在后面内容向大家介绍。

系统调用 vfork()
一般不用

七、fork()之后的竞争条件

调用 fork 之后,无法确定父、子两个进程谁将率先访问 CPU,也就是说无法确认谁先被系统调用运行(在多核处理器中,它们可能会同时各自访问一个 CPU),这将导致谁先运行、谁后运行这个顺序是不确定的。
这个时候可以通过采用采用某种同步技术来实现,譬如前面给大家介绍的信号,如果要让子进程先运行,则可使父进程被阻塞,等到子进程来唤醒它,示例代码如下所示:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>

static void sig_handler(int sig)
{
	 printf("接收到信号\n");
}

int main(void)
{
	 struct sigaction sig = {0};
	 sigset_t wait_mask;
	 /* 初始化信号集 */
	 sigemptyset(&wait_mask);
	 /* 设置信号处理方式 */
	 sig.sa_handler = sig_handler;
	 sig.sa_flags = 0;
	 if (-1 == sigaction(SIGUSR1, &sig, NULL)) 
	 {
		 perror("sigaction error");
		 exit(-1);
	 }
	 switch (fork()) 
	 {
		 case -1:
			 perror("fork error");
			 exit(-1);
		 case 0:
			 /* 子进程 */
			 printf("子进程开始执行\n");
			 printf("子进程打印信息\n");
			 printf("~~~~~~~~~~~~~~~\n");
			 sleep(2);
			 kill(getppid(), SIGUSR1); //发送信号给父进程、唤醒它
			 _exit(0);
		 default:
			 /* 父进程 */
			 if (-1 != sigsuspend(&wait_mask))//挂起、阻塞,当执行完信号处理函数后或者接收到wait_mask以外的信号后解挂,将掩码设成调用前的值
			 exit(-1);
			 printf("父进程开始执行\n");
			 printf("父进程打印信息\n");
			 exit(0);
	 }
}

在这里插入图片描述

八、监视子进程

1、wait()函数

系统调用 wait()可以等待进程的任一子进程终止,同时获取子进程的终止状态信息,其函数原型如下所示:

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);

函数参数和返回值含义如下:
status:参数 status 用于存放子进程终止时的状态信息,参数 status 可以为 NULL,表示不接收子进程终止时的状态信息。
返回值:若成功则返回终止的子进程对应的进程号;失败则返回-1。

系统调用 wait()将执行如下动作:
⚫ 调用 wait()函数,如果其所有子进程都还在运行,则 wait()会一直阻塞等待,直到某一个子进程终止;
⚫ 如果进程调用 wait(),但是该进程并没有子进程,也就意味着该进程并没有需要等待的子进程,那么 wait()将返回错误,也就是返回-1、并且会将 errno 设置为 ECHILD。
⚫ 如果进程调用 wait()之前,它的子进程当中已经有一个或多个子进程已经终止了,那么调用 wait()也不会阻塞。wait()函数的作用除了获取子进程的终止状态信息之外,更重要的一点,就是回收子进程的一些资源,俗称为子进程“收尸”。

参数 status 不为 NULL 的情况下,则 wait()会将子进程的终止时的状态信息存储在它指向的 int 变量中,可以通过以下宏来检查 status 参数:
⚫ WIFEXITED(status):如果子进程正常终止,则返回 true;
⚫ WEXITSTATUS(status):返回子进程退出状态,是一个数值,其实就是子进程调用_exit()或 exit()时指定的退出状态;wait()获取得到的 status 参数并不是调用_exit()或 exit()时指定的状态,可通过WEXITSTATUS 宏转换;
⚫ WIFSIGNALED(status):如果子进程被信号终止,则返回 true;
⚫ WTERMSIG(status):返回导致子进程终止的信号编号。如果子进程是被信号所终止,则可以通过此宏获取终止子进程的信号;
⚫ WCOREDUMP(status):如果子进程终止时产生了核心转储文件,则返回 true;

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>

int main(void)
{
	 int status;
	 int ret;
	 int i;
	 
	 /* 循环创建 3 个子进程 */
	 for (i = 1; i <= 3; i++) 
	 {
		 switch (fork()) 
		 {
			 case -1:
				 perror("fork error");
				 exit(-1);
			 case 0:
				 /* 子进程 */
				 printf("子进程<%d>被创建\n", getpid());
				 sleep(i);
				 _exit(i);
			 default:
				 /* 父进程 */
				 break;
		 }
	 }
	 sleep(1);
	 printf("~~~~~~~~~~~~~~\n");
	 for (i = 1; i <= 3; i++) 
	 {
		 ret = wait(&status);
		 if (-1 == ret) 
		 {
			 if (ECHILD == errno) 
			 {
				 printf("没有需要等待回收的子进程\n");
				 exit(0);
			 }
		 else 
		 {
			 perror("wait error");
			 exit(-1);
		 }
	 }
	 printf("回收子进程<%d>, 终止状态<%d>\n", ret,
	 WEXITSTATUS(status));
	 }
	 exit(0);
}

在这里插入图片描述

2、waitpid()函数

使用 wait()系统调用存在着一些限制,这些限制包括如下:
⚫ 如果父进程创建了多个子进程,使用 wait()将无法等待某个特定的子进程的完成,只能按照顺序等待下一个子进程的终止,一个一个来、谁先终止就先处理谁;
⚫ 如果子进程没有终止,正在运行,那么 wait()总是保持阻塞,有时我们希望执行非阻塞等待,是否有子进程终止,通过判断即可得知;
⚫ 使用 wait()只能发现那些被终止的子进程,对于子进程因某个信号(譬如 SIGSTOP 信号)而停止(注意,这里停止指的暂停运行),或是已停止的子进程收到 SIGCONT 信号后恢复执行的情况就无能为力了。

#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);

函数参数和返回值含义如下:
pid:参数 pid 用于表示需要等待的某个具体子进程,关于参数 pid 的取值范围如下:
⚫ 如果 pid 大于 0,表示等待进程号为 pid 的子进程;
⚫ 如果 pid 等于 0,则等待与调用进程(父进程)同一个进程组的所有子进程;
⚫ 如果 pid 小于-1,则会等待进程组标识符与 pid 绝对值相等的所有子进程;
⚫ 如果 pid 等于-1,则等待任意子进程。wait(&status)与 waitpid(-1, &status, 0)等价。
status:与 wait()函数的 status 参数意义相同。
options:稍后介绍。
返回值:返回值与 wait()函数的返回值意义基本相同,在参数 options 包含了 WNOHANG 标志的情况下,返回值会出现 0,稍后介绍。

参数 options 是一个位掩码,可以包括 0 个或多个如下标志:
⚫ WNOHANG:如果子进程没有发生状态改变(终止、暂停),则立即返回,也就是执行非阻塞等待,可以实现轮训 poll,通过返回值可以判断是否有子进程发生状态改变,若返回值等于 0 表示没有发生改变。
⚫ WUNTRACED:除了返回终止的子进程的状态信息外,还返回因信号而停止(暂停运行)的子进程状态信息;
⚫ WCONTINUED:返回那些因收到 SIGCONT 信号而恢复运行的子进程的状态信息。

使用示例

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>

int main(void)
{
	 int status;
	 int ret;
	 int i;
	 
	 /* 循环创建 3 个子进程 */
	 for (i = 1; i <= 3; i++) 
	 {
		 switch (fork()) 
		 {
			 case -1:
				 perror("fork error");
				 exit(-1);
			 case 0:
				 /* 子进程 */
				 printf("子进程<%d>被创建\n", getpid());
				 sleep(i);
				 _exit(i);
			 default:
				 /* 父进程 */
			 break;
		 }
	 }
	 sleep(1);
	 printf("~~~~~~~~~~~~~~\n");
	 while(1)
	 {
		 ret = waitpid(-1, &status, WNOHANG);	//轮询
		 if (0 > ret) 
		 {
			 if (ECHILD == errno)
				 exit(0);
			 else 
			 {
				 perror("wait error");
				 exit(-1);
			 }
		 }
		 else if (0 == ret)
			 continue;
		 else
	 		 printf("回收子进程<%d>, 终止状态<%d>\n", ret,WEXITSTATUS(status));
	}
	exit(0);
}

在这里插入图片描述
3、僵尸进程与孤儿进程

父进程先于子进程结束,也就是意味着,此时子进程变成了一个“孤儿”,我们把这种进程就称为孤儿进程。
如果子进程先于父进程结束,此时父进程还未来得及给子进程“收尸”,那么此时子进程就变成了一个僵尸进程。

4、SIGCHLD 信号
当发生以下两种情况时,父进程会收到该信号:
⚫ 当父进程的某个子进程终止时,父进程会收到 SIGCHLD 信号;
⚫ 当父进程的某个子进程因收到信号而停止(暂停运行)或恢复时,内核也可能向父进程发送该信号。

通过 SIGCHLD 信号实现异步方式监视子进程。

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

static void wait_child(int sig)
{
	 /* 替子进程收尸 */
	 printf("父进程回收子进程\n");
	 while(waitpid(-1, NULL, WNOHANG) > 0)// waitpid()返回 0,表明再无僵尸进程存在;或者返回-1,表明有错误发生
		 continue;
}

int main(void)
{
	 struct sigaction sig = {0};
	 /* 为 SIGCHLD 信号绑定处理函数 */
	 sigemptyset(sig.sa_mask);
	 sig.sa_handler = wait_child;
	 sig.sa_flags = 0;
	 
	 if (-1 == sigaction(SIGCHLD, &sig, NULL)) 
	 {
		 perror("sigaction error");
		 exit(-1);
	 }
	 /* 创建子进程 */
	 switch (fork()) 
	 {
		 case -1:
			 perror("fork error");
			 exit(-1);
		 case 0:
			 /* 子进程 */
			 printf("子进程<%d>被创建\n", getpid());
			 sleep(1);
			 printf("子进程结束\n");
			 _exit(0);
		 default:
			 /* 父进程 */
			 break;
	 }
	 sleep(3);
	 exit(0);
}

在这里插入图片描述

九、执行新程序

1、execve()函数

#include <unistd.h>
int execve(const char *filename, char *const argv[], char *const envp[]);

函数参数和返回值含义如下:
filename:参数 filename 指向需要载入当前进程空间的新程序的路径名,既可以是绝对路径、也可以是相对路径。
argv:参数 argv 则指定了传递给新程序的命令行参数。是一个字符串数组,该数组对应于 main(int argc, char *argv[])函数的第二个参数 argv,且格式也与之相同,是由字符串指针所组成的数组,以 NULL 结束。argv[0]对应的便是新程序自身路径名。
envp:参数 envp 也是一个字符串指针数组,指定了新程序的环境变量列表,参数 envp 其实对应于新程序的 environ 数组,同样也是以 NULL 结束,所指向的字符串格式为 name=value。
返回值:execve 调用成功将不会返回;失败将返回-1,并设置 errno。

使用示例

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
	 char *arg_arr[5];
	 char *env_arr[5] = {"NAME=app", "AGE=25","SEX=man", NULL};
	 if (2 > argc)
		 exit(-1);
	 arg_arr[0] = argv[1];
	 arg_arr[1] = "Hello";
	 arg_arr[2] = "World";
	 arg_arr[3] = NULL;
	 execve(argv[1], arg_arr, env_arr);
	 perror("execve error");
	 exit(-1);
}
#include <stdio.h>
#include <stdlib.h>

extern char **environ;

int main(int argc, char *argv[])
{
	 char **ep = NULL;
	 int j;
	 
	 for (j = 0; j < argc; j++)
		 printf("argv[%d]: %s\n", j, argv[j]);
	 puts("env:");
	 for (ep = environ; *ep != NULL; ep++)
		 printf(" %s\n", *ep);
	 exit(0);
}

在这里插入图片描述
代码中中 execve()函数的使用并不是它真正的应用场景,通常由 fork()生成的子进程对 execve()的调用最为频繁,也就是子进程执行 exec 操作。

说到这里,我们来分析一个问题,为什么需要在子进程中执行新程序?其实这个问题非常简单,虽然可以直接在子进程分支编写子进程需要运行的代码,但是不够灵活,扩展性不够好,直接将子进程需要运行的代码单独放在一个可执行文件中不是更好吗,所以就出现了 exec 操作。

父进程绑定的信号处理函数对子进程的影响
fork后子进程会继承父进程绑定的信号处理函数,若调用exec加载新程序后,就不会继承这个信号处理函数了。

父进程的信号掩码对子进程的影响
fork后子进程会继承父进程的信号掩码,执行exec后仍然会继承这个信号掩码。

2、system()函数

使用 system()函数可以很方便地在我们的程序当中执行任意 shell 命令

#include <stdlib.h>
int system(const char *command);

例子

system("ls -la")
system("echo HelloWorld")

使用示例

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
	 int ret;
	 if (2 > argc)
		 exit(-1);
	 ret = system(argv[1]);
	 if (-1 == ret)
		 fputs("system error.\n", stderr);
	 else 
	 {
		 if (WIFEXITED(ret) && (127 == WEXITSTATUS(ret)))
			 fputs("could not invoke shell.\n", stderr);
	 }
	 exit(0);
}

在这里插入图片描述

3、进程状态与进程关系

3.1、进程状态

Linux 系统下进程通常存在 6 种不同的状态,分为:就绪态、运行态、僵尸态、可中断睡眠状态(浅度睡眠)、不可中断睡眠状态(深度睡眠)以及暂停态。

在这里插入图片描述
3.2、进程关系

主要包括:无关系(相互独立)、父子进程关系、进程组以及会话。

十、守护进程

1、何为守护进程

是运行在后台的一种特殊进程,它独立于控制终端并且周期性地执行某种任务或等待处理某些事情的发生,主要表现为以下两个特点:
⚫ 长期运行。
⚫ 与控制终端脱离。
守护进程是一种很有用的进程。Linux 中大多数服务器就是用守护进程实现的,譬如,Internet 服务器inetd、Web 服务器 httpd 等。同时,守护进程完成许多系统任务,譬如作业规划进程 crond 等。

2、编写守护进程程序

编写守护进程一般包含如下几个步骤:

  1. 创建子进程、终止父进程

  2. 子进程调用 setsid 创建会话
    setsid 函数能够使子进程完全独立出来,从而脱离所有其他进程的控制。

  3. 将工作目录更改为根目录

  4. 重设文件权限掩码 umask
    设置文件权限掩码的函数是 umask,通常的使用方法为 umask(0)。

  5. 关闭不再需要的文件描述符

  6. 将文件描述符号为 0、1、2 定位到/dev/null

  7. 其它:忽略 SIGCHLD 信号

接下来,我们根据上面的介绍的步骤,来编写一个守护进程程序,示例代码如下所示:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>

int main(void)
{
	 pid_t pid;
	 int i;
	 /* 创建子进程 */
	 pid = fork();
	 if (0 > pid) 
	 {
		 perror("fork error");
		 exit(-1);
	 }
	 else if (0 < pid)//父进程
		 exit(0); //直接退出
	 /*
	 *子进程
	 */
	 /* 1.创建新的会话、脱离控制终端 */
	 if (0 > setsid()) 
	 {
		 perror("setsid error");
		 exit(-1);
	 }
	 /* 2.设置当前工作目录为根目录 */
	 if (0 > chdir("/")) 
	 {
		 perror("chdir error");
		 exit(-1);
	 }
	 /* 3.重设文件权限掩码 umask */
	 umask(0);
	 /* 4.关闭所有文件描述符 */
	 for (i = 0; i < sysconf(_SC_OPEN_MAX); i++)
		 close(i);
	 /* 5.将文件描述符号为 0、1、2 定位到/dev/null */
	 open("/dev/null", O_RDWR);
	 dup(0);
	 dup(0);
	 /* 6.忽略 SIGCHLD 信号 */
	 signal(SIGCHLD, SIG_IGN);
	 /* 正式进入到守护进程 */
	 for ( ; ; ) 
	 {
		 sleep(1);
		 puts("守护进程运行中......");
	 }
	 exit(0);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/13197.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

leetcode160. 相交链表

给你两个单链表的头节点 headA 和 headB &#xff0c;请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点&#xff0c;返回 null 。 图示两个链表在节点 c1 开始相交&#xff1a; 题目数据 保证 整个链式结构中不存在环。 注意&#xff0c;函数返回结果后&…

软件测试工程师需要达到什么水平才能顺利拿到 20k 无压力?

最近有粉丝朋友问&#xff1a;软件测试员需要达到什么水平才能顺利拿到 20k 无压力&#xff1f; 这里写一篇文章来详细说说&#xff1a; 目录 扎实的软件测试基础知识&#xff1a;具备自动化测试经验和技能&#xff1a;熟练掌握编程语言&#xff1a;具备性能测试、安全测试、全…

flv怎么无损转换成mp4格式,3大超级方法分享

flv格式是目前在视频分享媒体播放网站上广泛使用的一种视频文件格式&#xff0c;可以在网站窗口中直接播放&#xff0c;这类视频文件还能够有效保护版权。但是有些时候我们可能需要将flv格式的视频转换为其他格式&#xff0c;比如mp4。但是该怎么操作呢&#xff1f; 其实有很多…

【花雕学AI】深度挖掘ChatGPT角色扮演的一个案例—CHARACTER play : 莎士比亚

CHARACTER play : 莎士比亚 : 52岁&#xff0c;男性&#xff0c;剧作家&#xff0c;诗人&#xff0c;喜欢文学&#xff0c;戏剧&#xff0c;爱情 : 1、问他为什么写《罗密欧与朱丽叶》 AI: 你好&#xff0c;我是莎士比亚&#xff0c;一位英国的剧作家和诗人。我很高兴你对我的…

【状态估计】用于描述符 LTI 和 LPV 系统的分析、状态估计和故障检测的算法(Matlab代码实现)

&#x1f4a5; &#x1f4a5; &#x1f49e; &#x1f49e; 欢迎来到本博客 ❤️ ❤️ &#x1f4a5; &#x1f4a5; &#x1f3c6; 博主优势&#xff1a; &#x1f31e; &#x1f31e; &#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 …

一文看懂数据云平台的“可观测性”技术实践

背景 这是一家大型制造集团。为监控及预测工厂设备运行情况&#xff0c;IT部门在数据云平台DataSimba上按天执行数据作业&#xff0c;每24小时对工厂设备的日志数据进行分析&#xff0c;发现能对业务起到很好的辅助作用&#xff0c;效果不错。 “要不升级为每1个小时跑一次&am…

腾讯云轻量级云服务器Centos7防火墙开放8080端口

腾讯云轻量级云服务器Centos7防火墙开放8080端口 一、centos7防火墙打开端口 因为Centos7以上用firewalld代替了iptables,也就是说firewalld开通了8080端口应该就行了 1.查看8080是否已经放开 sudo firewall-cmd --permanent --zonepublic --list-ports2.查看防火墙状态 s…

EMQX vs NanoMQ | 2023 MQTT Broker 对比

引言 EMQX 和 NanoMQ 都是由全球领先的开源物联网数据基础设施软件供应商 EMQ 开发的开源 MQTT Broker。 EMQX 是一个高度可扩展的大规模分布式 MQTT Broker&#xff0c;能够将百万级的物联网设备连接到云端。NanoMQ 则是专为物联网边缘场景设计的轻量级 Broker。 本文中我们…

SpringCloud 项目如何方便 maven 打包以及本地开发

一、背景 springcloud-alibaba &#xff0c;使用 nacos 做配置中心&#xff0c;maven 作为构建工具。为了防止 test 、prod 环境配置文件覆盖问题&#xff0c;使用 mvn -P 命令。 二、项目 pom 文件 1. 利用 resources 标签来指定目录&#xff0c;build > resources 标签&a…

MySQL-CENTOS7下MySQL单实例安装

MySQL单实例安装 1 版本下载2 MySQL安装2.1 创建目录并解压2.2 安装数据库2.3 安装RPM包2.4 启动服务2.5 连接MYSQL 3 MYSQL卸载卸载4 FAQ 1 版本下载 mysql下载 选择对应的版本。我选择的是的8.0.31的版本。 2 MySQL安装 2.1 创建目录并解压 mkdir /mysql mkdir /mysql/s…

OpenAI-ChatGPT最新官方接口《错误代码大全》全网最详细中英文实用指南和教程,助你零基础快速轻松掌握全新技术(九)(附源码)

Error codes 错误码 前言Introduction 导言API errors API 错误401 - Invalid Authentication 401 -验证无效401 - Incorrect API key provided 401 -提供的API密钥不正确401 - You must be a member of an organization to use the API 401 -您必须是组织的成员才能使用API429…

公司招人,面试了50+的候选人,技术实在是太烂了····

前两个月&#xff0c;公司测试岗位面了 50候选人&#xff0c;面试下来发现几类过不了的情况&#xff0c;分享大家防止踩坑&#xff1a; 技术倒是掌握得挺多&#xff0c;但只是皮毛&#xff0c;基础知识却是一塌糊涂。工作多年&#xff0c;从未学习过工作之外的技术栈&#xff…

【项目】视频列表滑动,自动播放

自动播放 期望效果&#xff0c;当滑动列表结束后&#xff0c;屏幕中间的视频自动播放HTML页面data变量实践操作&#xff01;重点来了&#xff01;滚动获得的数据实现效果源码&#xff08;粘贴即可运行&#xff09; 期望效果&#xff0c;当滑动列表结束后&#xff0c;屏幕中间的…

Gartner Magic Quadrant for SD-WAN 2022 (Gartner 魔力象限:软件定义广域网 2022)

Gartner 魔力象限&#xff1a;SD-WAN 2022 请访问原文链接&#xff1a;https://sysin.org/blog/gartner-magic-quadrant-sd-wan-2022/&#xff0c;查看最新版。原创作品&#xff0c;转载请保留出处。 作者主页&#xff1a;sysin.org Gartner 魔力象限&#xff1a;SD-WAN 2022…

完美解决丨 - [SyntaxError: invalid syntax](#SyntaxError-invalid-syntax)

目录 报错名称 SyntaxError: invalid syntaxNameError: name xx is not definedIndentationError: expected an indented blockAttributeError: xx object has no attribute xxTypeError: xx object is not callableValueError: I/O operation on closed fileOSError: [Errno 2…

记一次mysql cpu 异常升高100%问题排查

此服务器为一个从库&#xff0c;用于数据的导出业务&#xff0c;服务器配置较低&#xff0c;日常的慢sql也比较多。 上午11点左右cpu异常告警&#xff0c;如下图所示&#xff0c; cpu使用率突增到50%&#xff0c;下午2点左右突增到100% &#xff0c;登录服务器top命令查看cpu升…

关于编译的重要概念总结

文章目录 什么是GNU什么是GCC / Ggcc / g编译的四个阶段gcc和g的主要区别 MinGW-w64C语言版本C 98C 11C 14C 17C 20 Makefilecmake 回想初学编程的时候&#xff0c;大部分人都是从C语言开始学起的&#xff0c;除了一些常见的语法和思想&#xff0c;一些基础知识常常被人们忽略&…

记一次从JS到内网的横向案例

前言 前段时间参加了一场攻防演练&#xff0c;使用常规漏洞尝试未果后&#xff0c;想到不少师傅分享过从JS中寻找突破的文章&#xff0c;于是硬着头皮刚起了JS&#xff0c;最终打开了内网入口获取了靶标权限和个人信息。在此分享一下过程。 声明&#xff1a;本次演练中&#xf…

ROS学习第二十四节——rosbag

1 rosbag使用_命令行 需求: ROS 内置的乌龟案例并操作&#xff0c;操作过程中使用 rosbag 录制&#xff0c;录制结束后&#xff0c;实现重放 实现: 1.准备 创建目录保存录制的文件 mkdir ./xxx cd xxx2.开始录制 -a:all&#xff0c;录制所有话题消息 -o:out&#xff0c…

Linux基础—网络设置

Linux基础—网络设置 一、查看网络配置1.查看网络接口信息 ifconfig2.查看主机名称 hostname3.查看路由表条目 route4.查看网络连接情况 netstat5.获取socket统计信息 ss 二、测试网络连接1.测试网络连接 ping2.跟踪数据包 traceroute3.域名解析 nslookup 三、使用网络配置命令…
最新文章