9. 进程

9. 进程

  • 1. 进程与程序
    • 1.1 main() 函数由谁调用
    • 1.2 程序如何结束
      • 1.2.1 注册进程终止处理函数 atexit()
    • 1.3 何为进程
    • 1.4 进程号
  • 2. 进程的环境变量
    • 2.1 应用程序中获取环境变量
      • 2.1.1 获取指定环境变量
    • 2.2 添加/删除/修改环境变量
      • 2.2.1 putenv()
      • 2.2.2 setenv()
      • 2.2.3 命令行式添加
      • 2.2.4 unsetenv()
    • 2.3 清空环境变量
    • 2.4 环境变量的作用
  • 3. 进程的内存布局
  • 4. 进程的虚拟地址空间
    • 4.1 为什么要引入虚拟地址
    • 4.2 虚拟地址的优点
  • 5. fork() 创建子进程
  • 6. 父子进程间的文件共享
  • 7. 系统调用 vfork()
  • 8. fork() 之后的竞争条件
  • 9. 进程的诞生与终止
    • 9.1 进程的诞生
    • 9.2 进程的终止
  • 10. 监视子进程
    • 10.1 wait()
    • 10.2 waitpid()
    • 10.3 僵尸进程与孤儿进程
      • 10.3.1 孤儿进程
      • 10.3.2 僵尸进程
    • 10.4 SIGCHLD 信号
  • 11. 执行新程序
    • 11.1 execve()
    • 11.2 exec 库函数
    • 11.3 exec 族函数使用
    • 11.4 system()
  • 12. 进程状态与进程关系
    • 12.1 进程状态
    • 12.2 进程关系
  • 13. 守护进程 Daemon
    • 13.1 何为守护进程
    • 13.2 编写守护进程程序
    • 13.3 SIGHUP 信号
  • 14. 单例模式运行
    • 14.1 通过文件存在与否进行判断
    • 14.2 使用文件锁

1. 进程与程序

1.1 main() 函数由谁调用

事实上,操作系统下的应用程序在运行主函数之前需要执行一段引导代码,最终由这一段引导代码去调用主函数。在编译链接时,由链接器将引导代码链接到主函数中,一起构成可执行文件。程序运行需要通过操作系统的加载器来实现,加载器时操作系统中的程序,当执行程序时,加载器负责将此应用程序加载到内存中去执行。而传参是在命令行中,命令行参数由 shell 进程逐一进行解析,将这些参数传递给加载器,加载器加载应用程序时会将其传递给应用程序引导代码,当引导程序调用主函数时,最终传递给 main() 函数

1.2 程序如何结束

程序结束就是进程终止,可以分为正常终止和异常终止,正常的有 return、exit 和 _exit;异常终止可以是调用 abort() 或者进程收到一个信号

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

用于注册一个进程在正常终止时调用的函数

#include <stdlib.h>
int atexit(void (*function)(void));
// 参数是函数指针,指向注册的函数
// 调用成功返回0,失败返回-1
static void func()
{cout << "nihao"<<endl;
}
int main()
{exit(0);
}

此代码运行后,会打印出 nihao,但是如果程序中使用了 _exit() 或 _Exit() 终止,而非是 exit(),那么将不会执行注册的终止处理函数。

1.3 何为进程

进程就是一个可执行程序的实例。可执行程序就是一个可执行文件,文件存放在磁盘中,如果可执行文件没有被执行,就不会产生作用,当它被运行后,就会有影响。而进程是一个动态的概念,它是程序的一次运行过程,当应用程序被加载到内存中运行后,就被称为了一个进程,程序运行结束后,进程就会终止,这就是一个进程的生命周期

1.4 进程号

每一个进程都对应一个进程号,也就是 PID,用于唯一标识一个进程。使用ps -aux可以查看系统中的进程相关信息,包括进程号。通过进程号,可以使用kill -9 进程号就可以杀掉对应进程。还可以通过系统调用获取自己的进程号,进程号就是一个正数

#include <sys/types.h>
#include <unistd.h>
pid_t getpid();		// 获取当前进程的进程号
pid_t getppid();	// 获取当前进程的父进程的进程号

2. 进程的环境变量

每一个进程都有一组与其相关的环境变量,这些环境变量以字符串的形式存储在一个字符串数组列表中,把这个数组称为环境列表。而环境变量都是以 名称=值 形式定义,所以环境变量是 名称-值 的成对集合。使用env可以看到 shell 进程的所有环境变量。使用export DEF=111可以添加一个新的环境变量,而使用export -n DEF可以删除环境变量。

2.1 应用程序中获取环境变量

在应用程序中也可以获取当前进程的环境变量,事实上,进程的环境变量是从其父进程继承来的,新的进程在创建之前,会继承其父进程的环境变量副本。
环境变量存放在一个字符串数组中,在应用程序中,通过 environ 变量指向它,是一个全局变量,在程序中只需要声明就可以extern char **environ

// 获取进程所有环境变量
extern char **environ;
int main()
{int i=0;for(i=0;NULL!=environ[i];i++){cout << environ[i]<<endl;}
}

2.1.1 获取指定环境变量

#include <stdlib.h>
char *getenv(const char *name);
// 参数是获取的环境变量名
// 如果存在,就返回对应的字符串的指针;如果不存在就返回NULL

使用该函数,不应该去修改返回的字符串

2.2 添加/删除/修改环境变量

2.2.1 putenv()

向进程的环境变量数组中添加一个新的环境变量,或者修改一个已经存在的环境变量的值

#include <stdlib.h>
int putenv(char *string);
// string是一个字符串指针,指向name=value形式的字符串,成功返回0,失败返回非零

函数调用成功之后,string 指向的字符串就成为了进程环境变量的一部分,就是说,environ 中的某个元素就指向了该字符串,而不是副本,所以不可随意修改

2.2.2 setenv()

用于向进程的环境变量列表中添加一个新的环境变量或修改现有环境变量对应的值

#include <stdlib.h>
int setenv(const char *name, const char *value, int overwrite);
// name表示需要添加或修改的环境变量名
// value表示环境变量的值
// overwrite:若参数name标识的环境变量已经存在,如果此参数为0,那么不会修改现有环境变量的值;如果非0,并且存在,就覆盖,不存在表示添加新的环境变量

这个函数会为形如 name=value 的字符串分配一块内存空间,并将 name 和 value 所指向的字符串复制到此缓冲区中,以此来创建一个新的环境变量。

2.2.3 命令行式添加

在运行程序时name=value ./app,在路径前添加 name=value 形式,如果是多个就以空格分隔

2.2.4 unsetenv()

从环境变量表中移除 name 标识的

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

2.3 清空环境变量

可以通过 environ=NULL来清空所有环境变量,也可以使用 clearenv()

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

使用 setenv() 和 clearenv() 函数可能会导致内存泄露,因为 setenv() 会分配一块内存缓冲区,而调用这个函数时没有释放缓冲区,就会发生内存泄漏

2.4 环境变量的作用

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

3. 进程的内存布局

在这里插入图片描述

  • 正文段: 也可以称为代码段,这是 CPU 执行的机器语言指令部分,文本段具有只读属性,以防止程序由于意外而修改其指令;正文段是可以共享的,即使多个进程间也可以同时运行同一段程序
  • 初始化数据段: 通常又称为数据段,包含了显示初始化的全局变量和静态变量,当程序加载到内存中时,从可执行文件中读取这些变量的值
  • 未初始化数据段: 包含了未进行显示初始化的全局变量和静态变量,又称为 bss 段,意思是由符号开始的块。在程序开始执行之前,系统会将本段内所有内存初始化为 0,可执行文件并没有为 bss 段变量分配内存空间,在可执行文件中只需记录 bss 段的位置及其所需大小,直到程序运行时,由加载器来分配这一段内存空间、
  • 栈: 局部变量以及每次函数调用时所需保存的信息都放在此段中,函数传递的参数以及返回值等也在。栈是一个动态增长和收缩的段,由栈帧组成。系统会为每个当前调用的函数分配一个栈帧
  • 堆: 可在运行时动态分配的一块区域
    可以使用 size 文件名 查看二进制可执行文件的文本段、数据段、bss 段的大小

4. 进程的虚拟地址空间

4.1 为什么要引入虚拟地址

如果没有引入虚拟地址,那么所有应用程序访问的就是实际的计算机物理地址,就会出现一些问题

  • 当多个程序需要运行时,必须保证这些程序用到的内存总量小于计算机实际物理内存
  • 内存使用效率低。
  • 进程地址空间不隔离。
  • 无法确定程序的链接地址,因为将程序代码加载到内存中的地址是随机分配的,无法预知

4.2 虚拟地址的优点

通过逻辑地址映射到真正的物理内存上

  • 进程与进程、进程与内核相互隔离,一个进程不能读取或修改另一个进程或内核的内存数据,因为每一个进程的虚拟地址映射到了不同的物理内存空间,提高了系统的安全性和稳定性
  • 在某些应用场合下,两个或更多的进程能够共享文件。因为每个进程都有自己的映射表,可以让不同进程的虚拟地址空间映射到相同的物理内存中
  • 便于实现内存保护机制
  • 编译应用程序时,不需要关心链接地址。

5. fork() 创建子进程

#include <unistd.h>
pid_t fork();

调用成功后,将会在父进程中返回子进程的 PID,而子进程中的返回值是 0;如果调用失败,父进程返回 -1。父进程就是调用 fork() 函数的进程。
调用成功后,子进程和父进程会继续执行 fork() 之后的指令,子进程和父进程在各自的进程空间中运行。事实上,子进程是父进程的一个拷贝,比如数据段、堆、栈以及继承了父进程打开的文件描述符,但是,父进程与子进程不共享这些内存空间,而是各自维护自己的空间。之后,每个进程均可以修改各自的栈数据以及堆段中的变量,而不影响另一个进程。
虽然子进程是父进程的一个副本,但是对于程序代码段来说,两个进程执行相同的代码段,因为代码段是只读的,也就是说父子进程共享代码段,在内存中只有一份代码数据段

int main()
{pid_t pid = fork();if(pid==0){cout << "我是子进程: "<<getpid()<<"我的父进程是: "<<getppid()<<endl;}cout << "我是父进程: "<<getpid()<<endl;
}

6. 父子进程间的文件共享

子进程会继承父进程打开的所有文件描述符,也就意味着父子进程对应的文件描述符均指向相同的文件表,就可以实现磁盘中文件的共享,那么如果子进程更新了文件偏移量,这个改变也会影响到父进程中相应的文件偏移量。但是如果父子进程各自打开同一个文件,那么就不会出现相互影响的情况,而是会各自写入。继承就像追加写入,而打开就像覆盖写入。

7. 系统调用 vfork()

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

fork() 会将父进程进行拷贝,但是会消耗大量的时间,而且如果子进程需要进程替换函数去执行新的程序就不需要用到父进程拷贝来的。vfork() 函数是为了子进程立即执行 exec() 新的程序而专门设计的

  • vfork() 并不会将父进程的地址空间完全复制到子进程中,因为子进程会立即执行 exec 或 _exit,于是也就不会引用该地址空间的数据。不过在子进程调用 exec 或 _exit 之前,它在父进程的空间中运行,子进程共享父进程的内存。但是如果子进程修改了父进程的数据、进行函数调用或者没有调用 exec 或 _exit 就返回可能会有未知的结果
  • vfork() 保证子进程先运行,调用 exec 之后父进程才可能被调用运行
  • 但是这个函数可能会导致一些难以察觉的问题,所以尽量少使用,除非速度绝对重要,因为现代 Linux 已经实现了写时拷贝,解决了之前的问题

8. fork() 之后的竞争条件

调用子进程与父进程的先后顺序是不确定的,不同的机器可能有不同的结果。有些程序可能有要求谁先调用,就可以让某一个进程先阻塞,等待另一个进程将它唤醒

static void sig_handler(int sig)
{cout << "接收到信号:" << sig<<endl;
}
int main()
{struct sigaction sig={0};sigset_t wait_mask;sigemptyset(&wait_mask);sig.sa_handler=sig_handler;sig.sa_flags=0;sigaction(SIGUSR1,&sig,NULL);pid_t pid=fork();if(pid==0){cout << "子进程"<<endl;cout << "------"<<endl;sleep(2);kill(getppid(),SIGUSR1);// 发送信号给父进程唤醒_exit(0);}sigsuspend(&wait_mask);// 挂起cout << "父进程"<<endl;cout << "~~~~~~~"<<endl;return 0;
}

9. 进程的诞生与终止

9.1 进程的诞生

Linux 系统下的所有进程都是由其父进程创建而来的,使用ps -aux命令可以查看系统下所有进程的信息,可以看到一个 PID 为 1 的进程,就是 init 进程,是所有进程的父进程,由内核启动,

9.2 进程的终止

进程的终止分正常终止和异常终止,一般使用 exit() 而不是 _exit() 系统调用来终止。exit 函数会执行以下动作:

  • 如果程序中注册了进程终止函数,那么会调用终止处理函数
  • 刷新 stdio 流缓冲区,这个缓冲区是由库维护的,不是系统的
  • 执行 _exit() 系统调用
    但是父子进程不能够都调用 exit() 函数,只能有一个进程使用,而另一进程使用 _exit()。
int main()
{printf("nihao");pid_t pid=fork();if(pid==0){exit(0);}exit(0);
}

可以看到 nihao 被答应了两遍,因为没有使用换行符来刷新缓冲区,子进程将父进程缓冲区拷贝了一份,而父子进程退出时,就会刷新各自的缓冲区。可以使用以下方式来避免:

  • 对于行缓冲设备,可以加上换行符
  • 在调用 fork() 之前,可以使用 fflush() 来强制刷新缓冲区,也可以使用 setvbuf() 和 setbuf() 来关闭 stdio 流的缓冲功能
  • 子进程调用 _exit() 退出进程,就不会刷新缓冲区

10. 监视子进程

父进程需要知道子进程的终止状态等等

10.1 wait()

等待任意子进程终止,同时获取子进程的退出状态

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

statuc 就是用来存放子进程退出信息的,可以设置为 NULL,表示不接收,如果成功返回终止的子进程对应的进程号,失败返回 -1.
该函数会执行以下动作:

  • 这个函数是阻塞式等待,如果所有子进程都还在运行,就会一直等,直到有一个进程终止
  • 如果进程调用 wait() 函数,但是该进程没有子进程,也就意味着没有需要等待的,就返回错误,并且设置 errno 为 ECHILD
  • 如果调用之前,它的子进程当中已经有子进程终止,那么调用 wait() 也不会阻塞。因为该函数还可以回收子进程的一些资源
    可以使用一些宏来检查 status:
  • WIFEXITED(status): 如果子进程正常终止,返回 true
  • WEXITSTATUS(status): 返回子进程退出状态,就是 _exit() 中的值
  • WIFSIGNALED(status): 如果子进程被信号终止,返回 true
  • WTERMSIG(status): 返回导致子进程终止的信号
  • WCOREDUMP(status): 如果子进程终止时发生了核心转储就返回 true

10.2 waitpid()

wait() 不能等待指定进程退出,而且只能阻塞式等待,wait() 只能发现那些已经终止的进程,而不能发现因信号暂停的进程,或者暂停后恢复的进程

#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
/* pid:* pid>0,表示等待进程为pid的子进程* pid=0,表示等待与父进程同一个进程组的所有子进程* pid<-1,等待进程组标识符与pid绝对值相等的所有子进程* pid=-1,等待任意子进程* //* options:位掩码,包括0和以下,而0表示阻塞等待* WHOHANG:如果子进程没有发生状态改变,则立即返回,也就是执行非阻塞等待,可以实现轮询检查。如果返回值为0,表示没有发生改变* WUNTRACED:处理返回终止的子进程的状态信息,还返回因信号而停止的* WCONTINUED:返回因收到SIGCONT信号而恢复运行的子进程的状态信息* /

10.3 僵尸进程与孤儿进程

10.3.1 孤儿进程

父进程先于子进程结束,所有的孤儿进程都会被 init 进程领养

int main()
{pid_t pid=fork();if(pid==0){cout << "子进程: "<<getpid()<<" 的父进程是: "	<< getppid() <<endl;sleep(3);	// 休眠,等待父进程结束cout << "子进程: "<<getpid()<<" 的父进程是: "	<< getppid() <<endl;}sleep(1);cout << "父进程结束"<<endl;return 0;
}

10.3.2 僵尸进程

子进程结束后,父进程会通过 wait() 和 waitpid() 回收子进程占用的一些资源。如果子进程先于父进程结束,父进程来不及回收,子进程就会处于僵尸状态,直到父进程调用 wait() 或 waitpid() 回收。而如果父进程没有调用等待函数就结束,那么子进程就会被 init 进程领养,然后调用等待函数回收。

10.4 SIGCHLD 信号

  • 当父进程的某个子进程终止时,父进程会收到 SIGCHLD 信号
  • 当父进程的某个子进程因收到信号而停止或恢复时,父进程也可能会收到该信号
    所以在该信号的处理函数中,可以设置轮询检查,这样,就不会漏掉僵尸进程。
static void wait_child(int sig)
{cout << "父进程回收子进程" << endl;while(waitpid(-1, NULL, WNOHANG)>0)continue;
}
int main()
{struct sigaction sig={0};sigemptyset(sig.sa_mask);sig.sa_handler=wait_child;sig.sa_flags=0;pid_t pid=fork();if(pid==0){cout << "子进程: " << endl;sleep(1);cout << "子进程结束" <<endl;_exit(0);}sleep(3);return 0;
}

11. 执行新程序

当子进程的工作不再是运行父进程的代码段,而是运行另一个新程序,那么这个时候子进程可以通过 exec 函数来实现运行另一个新的程序。

11.1 execve()

将新程序加载到某一进程的内存空间,使用新的程序替换旧的程序,而进程的栈、数据以及堆数据会被新进程的相应部件所替换,然后从新程序的 main 函数开始执行

#include <unistd.h>
int execve(const char *filename, char *const argv[], char *const envp[]);
/* * filename:指向需要载入当前进程空间的新程序的路径名* argv:命令行参数,和main函数的第二个参数一样,以NULL结尾* envp:指定新程序的环境变量列表,对应于新程序的environ数组,也是以NULL为结尾,所指向的字符串格式为name=value* /
// 成功不会返回,失败返回-1,并设置errno

11.2 exec 库函数

关于这一部分函数可以看另一篇文章:Linux进程控制

11.3 exec 族函数使用

extern char **environ;
char *arr[5];
arr[0]="ls";
arr[1]="-a";
arr[2]="-l";
arr[3]="NULL";
execl("bin/ls","ls","-a","-l",NULL);			// 需要传递
execve("bin/ls",arr);							// 将命令行参数通过数组传递
execlp("ls","ls","-a","-l",NULL);				// 自动查找文件
execvp("ls",arr);								// 自动查找路径,并将命令行参数通过数组传递
execle("bin/ls","ls","-a","-l",NULL,environ);	// 需要传递环境变量
execvpe("ls",arr,environ);						// 自动查找文件,以数组传递,需要环境变量

11.4 system()

使用该函数可以很方便地在程序当中执行任意 shell 命令。

#include <stdlib.h>
int system(const char *command);
// command:指向需要执行的 shell 命令,以字符串形式提供,譬如"ls -al"等
/* 返回值:* 当参数为 NULL,如果 shell 可用则返回一个非 0 值,不可用返回 0。参数不为空时,如下* 如果无法创建子进程或无法获取子进程的终止状态,返回 -1* 如果子进程不能执行 shell,返回值就类似子进程通过调用 _exit(127) 终止了* 如果所有的系统调用都成功了,返回执行 command 的 shell 进程的终止状态* /

优点是使用上简单,但是会牺牲效率。使用 system() 至少要创建两个进程,一个用于运行 shell,另一个运行参数 command 中解析出来的命令。每一个命令都是一次进程替换。

12. 进程状态与进程关系

12.1 进程状态

进程状态有以下六种:

  • 就绪态:指该进程满足被 CPU 调度的所有条件但此时并没有被调度执行,只要得到 CPU 就能够直接运行;意味着已经准备好被执行,当一个进程的时间片到达,操作系统调度程序会从就绪态链表中调度一个进程。一个新创建的进程就处于就绪态
  • 运行态:指该进程当前正在被 CPU 调度运行,处于就绪态的进程得到 CPU 调度就会进入运行态
  • 僵尸态:就是指僵尸进程,该进程已经结束,但是父进程未对其收尸
  • 可中断睡眠态:不可中断睡眠称为深度睡眠,深度睡眠无法被信号唤醒,只能等待相应的条件才能结束睡眠状态。浅度睡眠和深度睡眠统称为等待态,或者叫做阻塞态。表示进程处于一种等待状态,等待某种条件成立后便会进入到就绪态。所以,处于等待态的进程无法参与进程系统调度。
  • 暂停态:暂停并不是进程的终止,表示进程暂停运行,一般可通过信号将进程暂停。处于暂停态的进程是可以恢复进入到就绪态的,譬如收到 SIGCONT 信号。
    在这里插入图片描述

12.2 进程关系

  1. 无关系
    两个进程间没有任何关系,相互独立
  2. 父子关系
  3. 进程组
    进程有一个组 ID,用于标识该进程属于哪一个进程组,进程组是一个或多个进程的集合,这些进程不是鼓励的,它们之间存在父子、兄弟关系,或者功能上有联系。设置进程组实质上是为了方便对进程进行管理。
    使用进程组有以下要注意:
    • 每个进程必定属于且只能属于一个进程组
    • 每一个进程有一个组长进程,组长进程的 ID 就是进程组 ID
    • 在组长进程的 ID 前面加上一个负号就是操作进程组
    • 组长进程不能再创建新的进程组
    • 只要进程组中还存在一个进程,则进程组就存在,与组长进程是否终止无关
    • 一个进程组可以包含多个进程,进程组的生命周期从被创建开始,到其内所有进程终止或离开
    • 默认情况下,新创建的进程会继承父进程的进程组 ID
    #include <unistd.h>
    pid_t getpgid(pid_t pid);
    pid_t getgrp(void);
    
  4. 会话
    会话是一个或多个进程组的集合
    在这里插入图片描述
    会话可以包含多个进程,但只能有一个前台进程组,其它的是后台进程组。每个会话都有一个会话首领,即创建会话的进程,一个会话可以有控制终端,也可以没有控制终端,在有终端的情况下也只能连接一个控制终端,这通常是登录到其上的终端设备(在终端登录的情况下)或伪终端设备(譬如通过 SSH 协议网络登录)。
    会话的首领进程连接一个终端后,该终端就成为会话的控制终端,与控制终端建立连接的会话首领进程被称为控制进程。产生在终端上的输入和信号将发送给会话的前台进程组中的所有进程。
    当用户在某个终端登录时,一个新的会话就开始了;当我们在 Linux 系统下打开了多个终端窗口时,实际上就是创建了多个终端会话。
    一个进程组有组长进程的 ID 标识,而对于会话来说,会话的首领进程的进程组 ID 将作为该会话的标识,也就是会话 ID(sid)。在默认情况下,新创建的进程会继承父进程的会话 ID,通过系统调用 getsid() 可获取。
    #include <unistd.h>
    pid_t getsid(pid_t pid);// 0 表示获取当前进程的会话 ID
    pid_t setsid(void);		// 创建一个会话,但是没有控制终端
    

13. 守护进程 Daemon

13.1 何为守护进程

守护进程也称为精灵进程,是运行在后台的一种特殊进程,它独立于控制终端并且周期性地执行某种任务或等待处理某些事情的发生,主要表现为以下两个特点:

  • 长期运行:守护进程是一种生存周期很长的一种进程,它们一般在系统启动时开始运行,除非强行终止,否则直到系统关机都会保持运行。而普通进程在用户登录或运行程序时创建,在运行结束或用户注销时终止,但守护进程不受用户登录注销的影响
  • 与控制终端脱离:在 Linux 中,系统与用户交互的界面称为终端,每一个从终端开始运行的进程都会依附于这个终端,也就是会话的控制终端。当控制终端被关闭的时候,该会话就会退出,有控制终端运行的所有进程都会被终止,这使得普通进程都是和运行该进程的终端相绑定的。但守护进程能突破这种限制,它脱离终端并且在后台运行,脱离终端的目的是为了避免进程在运行的过程中的信息在终端显示并且进程也不会被任何终端所产生的信息打断。
    守护进程与终端无任何关系,用户的登录与注销与守护进程无关,不受其影响,守护进程自成进程组、自成会话

13.2 编写守护进程程序

  1. 创建子进程、终止父进程
    父进程调用 fork() 创建子进程,然后父进程 exit() 退出。这样做实现了以下几点:如果该守护进程是作为一条简单的 shell 命令启动,那么父进程终止会让 shell 认为这条命令已经执行完毕;虽然子进程继承了父进程的进程组 ID,但它有自己独立的进程 ID,这保证了子进程不是一个进程组的组长进程,这是下面将要调用 setsid() 的先决条件
  2. 子进程调用 setsid() 创建会话
    用于之前子进程并不是进程组的组长进程,所以调用 setsid() 会使得子进程创建一个新的会话,子进程称为新会话的首领进程,同样也创建了新的进程组、子进程成为组长进程,此时创建的会话将没有控制终端。所以这里调用该函数有三个作用:让子进程摆脱原会话的控制,让子进程摆脱原进程组的控制和让子进程摆脱原控制终端的控制。
    在创建子进程时,子进程继承了父进程的会话、进程组、控制终端等,虽然父进程退出了,但原先的会话期、进程组、控制终端等并没有改变,因此不是真正意义上的独立。setsid() 能够使子进程完全独立出来,从而脱离所有其他进程的控制。
  3. 将工作目录更改为根目录
    子进程继承了父进程的当前工作目录,由于在进程运行中,当前目录所在的文件系统是不能卸载的,这对以后使用会造成很多的麻烦。因此通常的做法就是让根目录作为守护进程的当前目录,当然也可以指定其他。
  4. 重设文件权限掩码 umask
    文件权限掩码用于对新建文件的权限位进行屏蔽。子进程会继承父进程的文件权限掩码,因此将文件权限掩码设置为 0,可以确保子进程有最大的操作权限,增强守护进程的灵活性。
  5. 关闭不需要的文件描述符
    子进程继承了父进程的所有文件描述符,被打开的文件可能永远不会被守护进程读或写,但它们一样消耗系统资源,导致所在的文件系统无法卸载,所以需要关闭。
  6. 将文件描述符号为 0、1、2 定位到 /dev/null
    将守护进程的标准输入、标准输出以及标准错误重定向到 /dev/null,这使得守护进程的输出无处显示,也无处从交互式用户那里接受输入。
  7. 其他:忽略 SIGCHLD 信号
    让内核将僵尸进程转交给 init 进程去处理,这样既不会产生僵尸进程,也省去了服务器进程回收子进程所占用的时间。
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>int main()
{pid_t pid;int i;pid=fork();if(pid>0){exit(0);	// 父进程直接退出}// 子进程// 1. 创建新会话,脱离控制终端if(setsid()<0){perror("setsid error");exit(-1);}// 2. 设置当前工作目录为根目录if(chdir("/")<0)	// 更改当前工作目录函数,参数是字符串形式的路径{perror("chdir error");exit(-1);}// 3. 重设文件权限掩码umask(0);// 4. 关闭所有文件描述符for(i=0;i<syscong(_SC_OPEN_MAX);i++){close(i);}// 5. 将012文件定位open("/dev/null",)_RDWR);dup(0);dup(0);// 6. 忽略 SIGCHLD 信号signal(SIGCHLD,SIG_IGN);// 进入守护进程for(;;){sleep(1);puts("守护进程运行中");}exit(0);
}

运行之后,没有任何打印信息输出,原因在于守护进程已经脱离了控制终端,它的打印信息不会显示到终端,/dev/null 是一个黑洞文件,自然看不到输出信息。

13.3 SIGHUP 信号

当用户准备退出会话时,系统会向该会话发出 SIGHUP 信号,会话将 SIGHUP 信号发送给所有子进程,子进程收到 SIGHUP 信号后,便会自动终止,当所有会话中的所有进程都退出时,会话也就终止了。因为程序当中一般不会对 SIGHUP 信号进行处理,所以对应的处理方式为系统默认方式,SIGHUP 信号的系统默认处理方式就是终止进程。如果忽略这个信号,终端退出时,进程不会退出。
控制终端只是会话的一个进程,只有会话中的所有进程退出后,会话才会结束。但是忽略了该信号,导致进程不会终止,会话也依然会存在。

14. 单例模式运行

通常情况下,一个程序可以被多次执行,即程序在还没有结束的情况下,又再次执行该程序,也就是系统中同时存在多个该进程的实例化对象。
但是对于有些程序设计来说,不允许出现这种情况,程序只能被执行一次,只要该程序没有结束,就无法再次运行,叫做单例模式。

14.1 通过文件存在与否进行判断

用一个文件存在与否来做标志,在程序运行正式代码之前,先判断一个特定的文件是否存在,存在表示进程已经运行,此时应立即退出,不存在表示没有进程运行,然后创建该文件,程序结束时再删除该文件。

#include <stdio.h>
#include <stdlib.h>
#include <sys/file.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>#define LOCK_FILE "./testApp.lock"
static void delete_file(void)
{remove(LOCK_FILE);
}
int main(void)
{/* 打开文件 */int fd = open(LOCK_FILE, O_RDONLY | O_CREAT | O_EXCL, 0666);if (-1 == fd) {fputs("不能重复执行该程序!\n", stderr);exit(-1);}// 注册进程终止处理函数if (atexit(delete_file))exit(-1);puts("程序运行中...");sleep(10);puts("程序结束");close(fd); //关闭文件exit(0);
}

但是上面的程序有问题,程序中使用 _exit() 退出,那么将无法执行删除函数,也就无法删除这个特定文件;程序异常退出,也无法删除文件;计算机掉电关机,重启后文件依旧存在。

14.2 使用文件锁

也需要通过一个特定的文件来实现,当程序启动后,首先打开该文件,以 O_WRONLY|O_CREAT,当文件不存在就创建,然后尝试去获取文件锁,若是成功,则将程序的进程号写入到该文件中,写入后不要关闭或解锁,保证进程一直持有该文件锁,若是获取锁失败,代表程序已经被运行,则退出本次启动。
当程序退出或文件关闭后,文件锁会自动解锁。

#define LOCK_FILE "./testApp.pid"
int main()
{char str[20]={0};int fd=open(LOCK_FILE,O_WRONLY|O_CREAT,0666);if(fd==-1){perror("open error");exit(-1);}// 以非阻塞方式获取文件锁if(-1==flock(fd,LOCK_EX|LOCK_NB)){fputs("不能重复执行该程序\n",stderr);close(fd);exit(-1);}puts("程序运行中");ftruncate(fd,0);	// 将文件长度截断为0sprintf(str,"%d\n",getpid());write(fd,str,strlen(str));	// 写入pidfor(;;)sleep(1);exit(0);
}

Linux 系统中,/var/run 目录下有很多以 pid 为后缀的文件,就是单例模式运行而设计的。
文件锁后面再详细介绍。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

IC入门必备!数字IC中后端设计实现全流程解析(1.3万字长文)

吾爱IC社区自2018年2月份开始在公众号上开始分享数字IC后端设计实现相关基础理论和实战项目经验&#xff0c;累计输出文字超1000万字。全部是小编一个个字敲出来的&#xff0c;绝对没有复制粘贴的情况&#xff0c;此处小编自己得给自己鼓鼓掌鼓励下自己。人生不要给自己设限&am…

自激振荡电路笔记 电弧打火机

三极管相关 三极管的形象描述 二极管 简单求解&#xff08;理想&#xff09; 优先导通&#xff08;理想&#xff09; 恒压降 稳压管&#xff08;二极管plus&#xff09; 基础工作模块 理想稳压管的工作特性 晶体管之三极管(“两个二极管的组合” ) 电弧打火机电路 1.闭合开…

3D视觉-结构光测量-多线结构光测量

工作原理 多线结构光测量在测量方式上类似上述线结构光测量&#xff0c;但是两者也有着一些明显的差别。这种形式的结构光测量&#xff0c;也常常被成为面结构光测量。首先激光器发出电光源通过通过光栅的调制产生多个切片光束&#xff0c;这些切片光束照射到待测物体表面后形成…

安全生产知识竞赛活动方案

为进一步普及安全生产法律法规知识&#xff0c;增强安全意识&#xff0c;提高安全技能&#xff0c;经研究&#xff0c;决定举办以“加强安全法治、保障安全生产”为主题的新修订《安全生产法》知识竞赛活动&#xff0c;现将有关事项通知如下&#xff1a; 一、活动时间&#xf…

大模型系列:OpenAI使用技巧_自定义文本向量化embeding

文章目录 0. Imports1. 输入2. 加载和处理输入数据3. 将数据分成训练和测试集4. 生成合成的负样本5. 计算嵌入和余弦相似度6. 绘制余弦相似度的分布图7. 使用提供的训练数据优化矩阵。8. 绘制训练期间找到的最佳矩阵的前后对比图&#xff0c;展示结果 本笔记本演示了一种将Open…

yolov8实战第四天——yolov8图像分类 ResNet50图像分类(保姆式教程)

yolov8实战第一天——yolov8部署并训练自己的数据集&#xff08;保姆式教程&#xff09;_yolov8训练自己的数据集-CSDN博客在前几天&#xff0c;我们使用yolov8进行了部署&#xff0c;并在目标检测方向上进行自己数据集的训练与测试&#xff0c;今天我们训练下yolov8的图像分类…

环形锻件全自动尺寸测量法兰三维检测自动化设备-CASAIM自动化蓝光检测系统

锻造是一种利用锻压机械对金属坯料施加压力&#xff0c;使其产生塑性变形以获得具有一定机械性能、一定形状和尺寸锻件的加工方法&#xff0c;锻压&#xff08;锻造与冲压&#xff09;的两大组成部分之一。 目前客户使用专用直径千分尺、塞规、塞尺等对锻件形状尺寸误差进行测…

数模学习day05-插值算法

插值算法有什么作用呢&#xff1f; 答&#xff1a;数模比赛中&#xff0c;常常需要根据已知的函数点进行数据、模型的处理和分析&#xff0c;而有时候现有的数据是极少的&#xff0c;不足以支撑分析的进行&#xff0c;这时就需要使用一些数学的方法&#xff0c;“模拟产生”一些…

SOLIDWORKS Flow Simulation热环境分析

关于室内通风的问题&#xff0c;其实室内通风方面的与我们之前聊到的数据中心通风散热问题相类似&#xff0c;只不过本次会引入一个新的模块——人体舒适度问题&#xff0c;在Flow Simulation中有一个HVAC模块就是专门用于研究人体舒适度的&#xff0c;它可以预测人们在热环境中…

力扣题目学习笔记(OC + Swift)206. 反转链表

206. 反转链表 给你单链表的头节点 head &#xff0c;请你反转链表&#xff0c;并返回反转后的链表。 示例 方法一、迭代 在遍历链表时&#xff0c;将当前节点的 next\textit{next}next 指针改为指向前一个节点。由于节点没有引用其前一个节点&#xff0c;因此必须事先存储其…

【Proteus仿真】【51单片机】自动除湿器系统

文章目录 一、功能简介二、软件设计三、实验现象联系作者 一、功能简介 本项目使用Proteus8仿真51单片机控制器&#xff0c;使用按键、LCD1602液晶、DHT11温湿度、继电器除湿模块等。 主要功能&#xff1a; 系统运行后&#xff0c;LCD1602显示DHT11传感器检测的湿度值阈值&am…

开源radishes高仿网易云音乐完整源码,可试听和下载“灰色”歌曲,跨平台的无版权音乐平台

源码介绍 Radishes是项目名称&#xff0c;是由萝卜翻译而来。可以在这里试听和下载“灰色”歌曲&#xff0c;是一个可以跨平台的无版权音乐平台。 萝卜音乐界面和功能参考 windows 网易云音乐界面和 ios 的网易云音乐 安装依赖 cd radishes/ yarn bootstrap 运行项目 web:…

docker-compose Install TeamCity

前言 TeamCity 是一个通用的 CI/CD 软件平台,可实现灵活的工作流程、协作和开发实践。允许在您的 DevOps 流程中成功实现持续集成、持续交付和持续部署。 系统支持 docker download TeamCity TeamCity 文档参考项目离线包百度网盘获取

PPT录制视频的方法,轻松提升演示效果!

在现代工作和学习中&#xff0c;ppt是一种常见的演示工具&#xff0c;而将ppt转化为视频形式更能方便分享和传播。本文将介绍两种ppt录制视频的方法&#xff0c;每一种方法都将有详细的步骤和简要的介绍&#xff0c;通过这些方法&#xff0c;你可以轻松将ppt制作成视频&#xf…

Servlet入门

目录 1.Servlet介绍 1.1什么是Servlet 1.2Servlet的使用方法 1.3Servlet接口的继承结构 2.Servlet快速入门 2.1创建javaweb项目 2.1.1创建maven工程 2.1.2添加webapp目录 2.2添加依赖 2.3创建servlet实例 2.4配置servlet 2.5设置打包方式 2.6部署web项目 3.servl…

SSH -L:安全、便捷、无边界的网络通行证

欢迎来到我的博客&#xff0c;代码的世界里&#xff0c;每一行都是一个故事 SSH -L&#xff1a;安全、便捷、无边界的网络通行证 前言1. SSH -L基础概念SSH -L 的基本语法&#xff1a;端口转发的原理和作用&#xff1a; 2. SSH -L的基本用法远程访问本地示例&#xff1a;访问本…

Linux 账号的权限管理

文章目录 Linux 账号的权限管理一、用户账号和组账号的概述1、Linux基于用户身份对资源访问进行控制1.1 用户账号1.2 组账号1.3 UID和GID系统如何区别用户身份 二、用户账号文件1、用户账号文件/etc/passwd保存用户名称&#xff0c;宿主目录、登录shell等基本信息 2、用户账号文…

【北亚服务器数据恢复】san环境下LUN Mapping出错导致文件系统一致性出错的数据恢复案例

服务器数据恢复环境&#xff1a; san环境下的存储上一组由6块硬盘组建的RAID6&#xff0c;划分为若干LUN&#xff0c;MAP到跑不同业务的服务器上&#xff0c;服务器上层是SOLARIS操作系统UFS文件系统。 服务器故障&#xff1a; 业务需求需要增加一台服务器跑新增的应用&#xf…

java注解和反射

java注解和反射 内置注解 Override 重写生命 Deprecated 已过时的方法&#xff0c;不推荐使用&#xff0c;可以使用 SuppressWarning 镇压警告&#xff0c;懂的都懂 元注解 作用&#xff1a;负责注解其他的注解 Target 描述注解的使用范围 Retention 描述注解的生命周期 Docu…

蚂蚁实习一面面经

蚂蚁实习一面面经 希望可以帮助到大家 tcp建立连接为什么要三次握手&#xff1f; 三次握手的过程 注意&#xff1a;三次握手的最主要目的是保证连接是双工的&#xff0c;可靠更多的是通过重传机制来保证的 所谓三次握手&#xff0c;即建立TCP连接&#xff0c;需要客户端和…