一文讲透Linux应用编程—进程原理

文章目录

  • 程序的开始和结束
    • main函数由谁调用?
    • 程序是如何结束的?
    • atexit注册进程终止处理函数
    • return、 exit、_exit三者区别
  • 进程环境
    • 环境变量
    • 进程运行的虚拟空间
  • 进程的正式引入
    • 什么是进程?
    • 进程ID
    • 多进程调度原理
  • fork创建子进程
    • 为什么要创建子进程?
    • fork的内部原理
    • 父子进程对文件的操作
  • 进程的诞生和消亡
    • 进程的诞生
    • 进程的消亡
    • 僵尸进程
    • 孤儿进程
  • waitpid介绍
    • waitpid和wait的区别?
    • waitpid原型介绍
  • exec族函数
    • 为什么需要exec函数?
    • exec函数介绍
    • exec实际应用
  • 进程状态
    • 进程的五种状态
  • system函数简介
    • system()
    • 原子操作
  • 进程之间的关系
  • 守护进程的引入
    • 进程查看命令ps
    • 向进程发送信号指令kill
    • 守护进程 daemon
    • 创建一个守护进程
    • 使用syslog 记录调试信息
      • syslog原理
  • 禁止程序多次运行

程序的开始和结束

main函数由谁调用?

  • 编译链接时的引导代码。操作系统下的应用程序其实在main执行之前也需要执行一段引导代码才能去执行main,我们写应用程序时,不用考虑引导代码的问题,编译链接时,由链接器将编译器中事先准备好的引导代码给链接进去和我们的应用程序一起构造成为最终的程序。比如编译C文件,做了很多链接工作
    一文讲透Linux应用编程—进程原理_第1张图片
  • 运行的加载器
    加载器是操作系统中的程序,当我们去执行一个程序(譬如./a/out,譬如代码中exec去加载代码)加载器负责将这个程序加载到内存中去执行这个程序。
  • -argc和argv传参如何实现?
    argc 是一个整数,表示命令行参数的数量(包括程序名称本身)。argv 是一个指向字符指针数组的指针,每个指针指向一个命令行参数的字符串。
    int main(int argc, char *argv[]) {
    // argc 表示参数数量,包括程序名本身
    printf("Argument count: %d\n", argc);

    // argv 是一个指针数组,指向参数的字符串
    for (int i = 0; i < argc; ++i) {
        printf("Argument %d: %s\n", i, argv[i]);
    }

    return 0;
}

程序是如何结束的?

  • 正常终止:return、 exit、_exit
  • 非正常终止:自己或者他人发信号终止进程

atexit注册进程终止处理函数

#include
#include
void func(){
    printf("我被终止了 我是善后函数\n");
}
int main(){
    printf("hello\n");
    //注册进程终止函数 系统会自动调用这里执行的函数 一定是最后被执行的
    atexit(func);
    printf("我是倒数第二行代码,但是我不是最后被执行的\n");
    return 0;

}
  • 注册atexit函数之后,一定是最后执行的,如果注册多个atexit函数之后,谁先注册,谁后执行 ,呈堆栈,先进后出

return、 exit、_exit三者区别

#include
#include
#include 
//用_exit()退出的话 不会执行善后函数
void func(){
    printf("我被终止了 我是善后函数\n");
}
void func2(){
    printf("我被终止了 我是第二个善后函数\n");
}

int main(){
    printf("hello\n");
    //注册进程终止函数 系统会自动调用这里执行的函数 一定是最后被执行的
    atexit(func);
    printf("我是倒数第二行代码,但是我不是最后被执行的\n");
    atexit(func2);
    _exit(0);

}
  • _exit() 是一个系统调用,位于 头文件中。它会立即终止程序的执行,不会执行任何清理工作,包括不刷新流缓冲、不调用 atexit() 注册的函数等。它也可以返回整数退出码。
  • exit() 是一个标准库函数,位于 头文件中。它允许程序立即退出,并且可以返回一个整数退出码。它执行一系列的清理工作(比如调用 atexit() 注册的函数、刷新流缓冲等),然后终止程序执行并返回给操作系统。
  • return 是在函数中使用的语句,用于从函数中返回值。在 main() 函数中使用 return 可以退出程序,但它只能返回整数值,并且只能在 main() 函数中使用。return 会执行一些清理工作(例如调用析构函数等)然后返回值给调用者。

进程环境

环境变量

  • export命令查看环境变量
    一文讲透Linux应用编程—进程原理_第2张图片
  • 进程环境表
    每一个进程中都有一份所有环境变量构成的一个表格,也就是说我们当前进程中可以直接使用这些环境变量,环境变量表本质上是一个字符串
  • 程序中通过environ全局变量使用环境变量,我们写的程序可以无条件直接使用系统中的环境变量,所以程序一旦用到了环境变量,那么操作系统就和操作系统的环境有关了。
#include
int main(){
    extern char **environ; //申明就能用
    int start = 0;
    printf("环境变量表如下:\n");
    while(NULL != environ[start]){
        printf("%s\n",environ[start]);
        start++;
    }
    return 0; 
}

一文讲透Linux应用编程—进程原理_第3张图片

  • 获取指定环境变量函数getenv
    char* res =getenv("HOME");
    printf("HOME is %s\n",res);
    return 0; 

进程运行的虚拟空间

  • 操作系统中的每个进程在独立地址空间中运行的
  • 每个进程的逻辑空间均为4GB(32位系统) 0-1G为OS 1-4G为应用
    在 32 位系统中,一个内存地址是由 32 位二进制数表示的。这意味着在这样的系统中,最多能够表示 2的32次方
    个不同的地址,也就是 4GB(以字节为单位)的内存空间。
  • 虚拟地址到物理地址空间的映射
    在 Linux 中,进程的虚拟地址空间到物理地址空间的映射是通过页表机制来实现的。这个机制允许操作系统在不同的地址空间(虚拟地址空间和物理地址空间)之间建立映射关系,从而实现了虚拟内存的概念。

简单来说,每个进程都有自己的虚拟地址空间,这个空间通常是连续的、从 0 开始的地址范围。而物理内存则是实际的硬件内存,由 RAM 组成。

当一个进程访问其虚拟地址空间时,CPU 会根据页表进行转换,将虚拟地址转换为对应的物理地址。页表包含了虚拟地址到物理地址的映射关系。这个映射关系是以页面(通常大小为 4KB 或者 4MB)为单位来管理的。

当进程访问一个虚拟地址时,CPU 首先将虚拟地址拆分成页号和页内偏移。然后,它使用页表将页号转换为相应的物理页框号,最后将页内偏移添加到物理页的基地址上,得到最终的物理地址。

在 Linux 中,当进程创建时,会分配一个页表用于管理其虚拟地址空间。操作系统负责维护页表,并根据进程的需求进行页面的加载和替换。这种分页机制允许操作系统有效地管理内存,实现了虚拟内存的概念,允许多个进程共享有限的物理内存,并且为每个进程提供了独立的地址空间,增加了系统的安全性和稳定性。

进程的正式引入

什么是进程?

  • 是一个动态的过程,不是静态实物
  • 进程就是程序的一次运行过程,一个静态的可执行程序a.out的一次运行过程(/a.out去运行到结束)就是一个进程
  • 进程控制块PCB(process control block) ,内核专门用来管理一个进程的数据结构。

进程ID

进程ID是用来表示这个进程。在Linux系统中ps命令可以查看当前进程ID
在这里插入图片描述

  • getpid,getppid ,getuid ,geteuid, getgid, getegid
    getpid():返回调用进程的进程ID(PID)。每个进程都有唯一的PID,用于标识系统中的进程。
    getppid():返回调用进程的父进程的PID。每个进程(除了初始进程)都有一个父进程,这个函数可以用来获取父进程的PID。
    getuid():返回调用进程的实际用户ID(UID)
    geteuid():
    返回调用进程的有效用户ID(EUID)。
    EUID用于控制进程对系统资源的访问权限,一般情况下等于UID,但在某些情况下可以被修改,比如设置了程序的Set-UID位。
    getgid():返回调用进程的实际组ID(GID)。
    getegid():返回调用进程的有效组ID(EGID)。
  • 相关C语言Demo
#include 
#include 
#include 

int main() {
    // 使用 getpid() 获取当前进程的 PID
    pid_t pid = getpid();
    printf("当前进程的 PID 是: %d\n", pid);
     // 使用 getpid() 获取当前进程的爹 PID
    pid_t ppid = getppid();
    printf("父进程的 PID 是: %d\n", ppid);

    return 0;
}

如图
一文讲透Linux应用编程—进程原理_第4张图片

多进程调度原理

  • 操作系统同时运行多个进程
  • 宏观上的并行和微观上的串行
  • 现在操作系统最小的调度单元是线程而不是进程

fork创建子进程

为什么要创建子进程?

  • 子进程允许程序在多个同时运行的独立流中执行任务,提高系统的并发性能。这种并发性对于处理多个任务或同时执行多个操作是至关重要的。
  • 多进程实现宏观上的并行

fork的内部原理

  • 进程的分裂生长模式——如果操作系统需要一个新进程来运行一个程序,那么操作系统会复制当前进程(称为父进程),创建一个新的进程(称为子进程)。这个子进程是父进程的副本,包括父进程的内存、数据和代码。一旦进程被分裂,新的子进程开始生长。这意味着子进程开始独立执行,有自己的程序计数器和堆栈空间。它可以通过改变其状态、执行其他程序或与其他进程进行通信来发展成为一个独立的实体。
  • fork演示
#include 
#include 
#include 

int main() {
    pid_t pid; // 存储进程ID

    pid = fork(); // 创建子进程  pid 会返回两次

    if (pid < 0) {
        // 错误处理
        fprintf(stderr, "Fork failed\n");
        return 1;
    } else if (pid == 0) {
        // 子进程代码 执行一次
        printf("This is the child process. PID: %d\n", getpid());
    } else {
        // 父进程代码 执行一次
        printf("This is the parent process. Child PID: %d\n", pid);
    }
    printf("hello world, pid  = %d \n",getpid());
    // 父子进程都会执行这里的代码  所以一定会打印两次
    return 0;
}

一文讲透Linux应用编程—进程原理_第5张图片
典型的使用fork使用方法 就是判断pid 返回值

父子进程对文件的操作

#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main() {
    pid_t pid; // 存储进程ID
    int fd = -1;
    pid = fork(); // 创建子进程  pid 会返回两次
    
    if (pid < 0) {
        // 错误处理
        fprintf(stderr, "Fork failed\n");
        return 1;
    } else if (pid == 0) {
        // 子进程代码
        fd = open("1.txt", O_RDWR | O_APPEND | O_CREAT, 0644);
        if (fd == -1) {
            fprintf(stderr, "Failed to open file\n");
            return 1;
        }
        printf("child\n");
        write(fd, "world\n", 6);
     
        sleep(1);
    } else {
        // 父进程代码
        fd = open("1.txt", O_RDWR | O_APPEND | O_CREAT, 0644);
        if (fd == -1) {
            fprintf(stderr, "Failed to open file\n");
            return 1;
        }
        write(fd, "hello\n", 6);

        sleep(1);
    }
    close(fd);
    return 0;
}

  • 子进程继承父进程中打开的文件
    上下文:父进程先open打开一个文件得到fd,然后在fork创建子进程,之后在父子进程中各自write向fd写入内容。测试结果表明:接续写。实际上本质原因就是父子进程之间的fd对应的文件指针是彼此关联的。实际测试的时候,有时候只写入了一个,实际上是某个进程完成之后直接Close了fd,还没有来得及写入。

进程的诞生和消亡

进程的诞生

一般使用fork 生成新的进程

进程的消亡

进程在运行时需要消耗系统资源(内存和IO),进程终止时理应完全释放这些资源(如果进程消亡后仍然没有释放资源,那么这些资源就丢失了。Linux系统设计时规定:每一个进程退出时候,操作系统会自动回收这个进程涉及到的所有的资源(譬如malloc申请的内容没有free时,当前进程结束时这个内存就会被释放,譬如open打开的文件没有close的在程序终止的时候也会被关闭。)但是OS只是回收了这个进程的工作时消耗的内存和IO,而并没有回收这个进程本身占用的内存(主要是task_strcuct和栈内存)。因为进程本身的内存,OS不能回收。需要别人来辅助回收,需要父进程来回收、

僵尸进程

  • 僵尸进程是已经完成了执行任务,但其父进程尚未对其进行善后处理(回收其占用的系统资源和内存)的一种进程状态。当一个进程完成任务后,通常会向其父进程发送一个信号来告知自己已经完成。父进程在收到这个信号后,应该执行一些清理工作,包括回收子进程的资源。
    如果父进程没有及时处理这个信号,子进程就会变成僵尸进程。尽管僵尸进程本身不再执行任何代码,但它仍然存在于系统进程表中,占用着系统资源(如进程ID等)。
  • 操作系统会为每个僵尸进程保留一些信息,以便父进程在需要时查询子进程的退出状态。父进程最终会使用一些系统调用(如wait()或waitpid())来获取子进程的退出状态,并彻底释放僵尸进程所占用的资源,从而结束僵尸进程的生命周期。
  • 父进程也可以不使用wait 或者 waitpid 来回收子进程,此时父进程结束时候一样回收子进程的剩余待回收内存资源。

孤儿进程

  • 孤儿进程是指其父进程提前结束或意外终止,导致操作系统将孤儿进程的父进程改为系统进程(通常是进程ID为1的init进程),这样孤儿进程就成为了一个没有父进程的进程。
  • 当一个进程的父进程结束时,操作系统会将孤儿进程的父进程设置为init进程,这个过程被称为"父进程失效"或"父进程放弃"。孤儿进程会继续在系统中运行,但它的父进程已经不再存在于系统进程表中。
  • 孤儿进程通常会被init进程(或其他接管它的父进程)接管。当孤儿进程结束时,操作系统会将其状态保存为僵尸进程,直到接收到init进程或新的父进程请求并进行处理,完成资源的释放和清理。

waitpid介绍

waitpid和wait的区别?

1.waitpid 允许你指定要等待的子进程,可以根据子进程的进程ID(PID)来等待特定的子进程,也可以使用一些特殊的参数值来指定等待不同条件的子进程。
wait 只会等待第一个终止的子进程,而且没有办法选择等待哪个子进程,它会等待任何一个子进程结束并返回其退出状态。
2.waitpid 可以通过设置选项来控制是否阻塞当前进程,例如,使用WNOHANG选项可以使 waitpid 变为非阻塞,即如果没有已终止的子进程,它会立即返回而不会阻塞父进程。而wait默认情况下会一直阻塞等待
3.waitpid 在出错时可以提供更精细的错误处理和更多的错误信息,因为它允许指定不同的选项和处理方式。

waitpid原型介绍

waitpid 是一个系统调用,用于等待特定子进程结束并获取其退出状态。它允许指定要等待的子进程以及提供不同的选项来控制等待的行为。

#include 
#include 

pid_t waitpid(pid_t pid, int *status, int options);

pid 参数指定要等待的子进程的进程ID:

  • 如果 pid 大于 0,则表示等待具有该进程ID的子进程。
  • 如果 pid 等于 -1,则表示等待任何子进程。
  • 如果 pid 等于 0,则表示等待与调用进程属于同一个进程组的任何子进程。
  • 如果 pid 小于 -1,则表示等待其进程组ID等于 pid 绝对值的任何子进程。
    status 参数是一个指向整数的指针,用于存储子进程的退出状态。
    options 参数是一组位掩码选项,用于控制 waitpid 的行为,例如可以使用 WNOHANG 选项实现非阻塞等待。
#include 
#include 
#include 
#include 

int main() {
    pid_t pid = fork();

    if (pid < 0) {
        perror("Fork failed");
        return 1;
    } else if (pid == 0) {
        // 子进程
        printf("Child process running\n");
        sleep(2); // 模拟子进程执行一些任务
        printf("Child process completed\n");
        return 0;
    } else {
        // 父进程
        printf("Parent process waiting for child...\n");
        int status;
        // 等待特定子进程(这里是子进程pid)结束并获取其状态
        waitpid(pid, &status, 0);
        if (WIFEXITED(status)) {
            printf("Child process exited with status: %d\n", WEXITSTATUS(status));
        }
        printf("Parent process done\n");
    }

    return 0;
}

exec族函数

为什么需要exec函数?

  • fork子进程是为了执行新的程序(fork创建了子进程,子进程和父进程同时被OS调度执行,因此子进程可以单独执行一个程序。这个程序宏观上将会和父进程同时进行)
  • 可以直接在子进程的if中写入新程序的代码。这样可以,但是不够灵活还得把子进程程序的源代码贴过来执行。(还得知道源代码,太长了也不合适)
  • 使用exec族函数运行新的可执行程序(exec组函数可以直接把一个编译好的可执行程序直接加载运行)
  • 有了exec族函数之后,子进程需要运行的程序被单独编写、单独编译链接成为一个可执行的程序,主程序为父进程,fork创建了子进程用exec执行可执行程序,达到父子进程运行不同程序同时在宏观上运行的效果。

exec函数介绍

  • execl和execv
    都可以用来执行一个程序,区别是传参的格式不同,execl 这个函数接受一系列的参数,参数列表是一个可变参数列表。execl() 函数要求你在函数调用中明确地列出每个参数,并以 NULL 结尾。每个参数都需要单独列出,并用逗号分隔。这种方法通常比较直观,但当参数数量未知时不太方便,因为你需要在代码中明确指定每个参数。
    execv(): 这个函数接受一个参数数组,而不是可变参数列表。execv() 函数接受一个参数数组 argv[],其中 argv[0] 是要执行的程序的名称,后续的元素是传递给新程序的参数。数组最后一个元素必须是空指针 NULL。这种方式更加灵活,可以通过数组直接传递参数,特别适用于参数数量未知或者在运行时动态生成参数的情况。
  • execlp和execvp
    在上述两组函数上基础上加了p ,上面两个执行程序必须指定可执行程序的全路径,如果exec没有找到path 这个文件则直接报错,而加了p的传递可以是File也可以是path,兼容了file 加了p的这两个函数首先去找file,如果找到则执行,如果没有找到会去环境变量Path所制定的目录下去找,如果找到则执行,没有找到报错。
  • execle和execvpe
    允许你在执行新程序时指定环境变量。

exec实际应用

  • 使用execl运行 ls - l -a
exec族函数
为什么需要exec函数?
- fork子进程是为了执行新的程序(fork创建了子进程,子进程和父进程同时被OS调度执行,因此子进程可以单独执行一个程序。这个程序宏观上将会和父进程同时进行)
- 可以直接在子进程的if中写入新程序的代码。这样可以,但是不够灵活还得把子进程程序的源代码贴过来执行。(还得知道源代码,太长了也不合适)
- 使用exec族函数运行新的可执行程序(exec组函数可以直接把一个编译好的可执行程序直接加载运行)
- 有了exec族函数之后,子进程需要运行的程序被单独编写、单独编译链接成为一个可执行的程序,主程序为父进程,fork创建了子进程用exec执行可执行程序,达到父子进程运行不同程序同时在宏观上运行的效果。
exec函数介绍
- execl和execv 
都可以用来执行一个程序,区别是传参的格式不同,execl 这个函数接受一系列的参数,参数列表是一个可变参数列表。execl() 函数要求你在函数调用中明确地列出每个参数,并以 NULL 结尾。每个参数都需要单独列出,并用逗号分隔。这种方法通常比较直观,但当参数数量未知时不太方便,因为你需要在代码中明确指定每个参数。
execv(): 这个函数接受一个参数数组,而不是可变参数列表。execv() 函数接受一个参数数组 argv[],其中 argv[0] 是要执行的程序的名称,后续的元素是传递给新程序的参数。数组最后一个元素必须是空指针 NULL。这种方式更加灵活,可以通过数组直接传递参数,特别适用于参数数量未知或者在运行时动态生成参数的情况。
- execlp和execvp
在上述两组函数上基础上加了p ,上面两个执行程序必须指定可执行程序的全路径,如果exec没有找到path 这个文件则直接报错,而加了p的传递可以是File也可以是path,兼容了file 加了p的这两个函数首先去找file,如果找到则执行,如果没有找到会去环境变量Path所制定的目录下去找,如果找到则执行,没有找到报错。
- execle和execvpe
允许你在执行新程序时指定环境变量。
exec实际应用
- 使用execl运行  ls - l -a
  • 使用execv运行 ls - l -a
    注意参数传递方式不同
#include 
#include 
#include 
#include 
int main() {
    pid_t pid = fork();
    if (pid < 0) {
        perror("Fork failed");
        return 1;
    } else if (pid == 0) {
        printf("执行ls -l -a");
        char * const arg[] ={"lsDemo","-l","-a",NULL};
        execv("/usr/bin/ls",arg);
        return 0;
    } else {
        printf("Parent process done\n");
    }
    return 0;
}

一文讲透Linux应用编程—进程原理_第6张图片

  • 使用excel 运行特定程序
#include 
#include 
#include 
#include 
int main() {
    pid_t pid = fork();
    if (pid < 0) {
        perror("Fork failed");
        return 1;
    } else if (pid == 0) {
        printf("执行time demo");
        char * const arg[] ={"time demo",NULL};
        execv("/home/marxist/Desktop/linux_study/3.3/3.3.1",arg);
        return 0;
    } else {
        printf("Parent process done\n");
    }
    return 0;
}

指定路径 和名称即可
一文讲透Linux应用编程—进程原理_第7张图片

  • execlp和execvp
    加p和不加p的区别是:不加p时需要全部路径加文件名,如果找不到就报错,如果加p 在传入的路径找不到的时候,可以去path变量去找,找不到报错
#include 
#include 
#include 
#include 
int main() {
    pid_t pid = fork();
    if (pid < 0) {
        perror("Fork failed");
        return 1;
    } else if (pid == 0) {
        printf("执行ls");
        char * const arg[] ={"ls demo","-l","-a",NULL};
        //path 直接传 ls 找不到ls 之后 在path 发现了ls 正常执行
        int ret = execvp("ls",arg);
        if(ret== -1){
            perror("找不到可执行程序");
        }
        return 0;
    } else {
        printf("Parent process done\n");
    }
    return 0;
}

效果如图
一文讲透Linux应用编程—进程原理_第8张图片
假设传入一个找不到的程序 可以根据exec返回值来判断 如果为-1 代表执行出错
一文讲透Linux应用编程—进程原理_第9张图片

进程状态

进程的五种状态

  • 就绪态
    进程已经准备好执行,但还没有被调度到处理器上运行。在这种状态下,进程已经获得了所有需要的资源,只等待系统分配处理器资源给它。
  • 运行态
    得到CPU资源 正在运行
  • 僵尸态
    进程已经结束,但是父进程还没有回收
  • 等待态 (深度睡眠和浅度睡眠)
    进程处于深度睡眠状态时,它在等待某些条件满足或某些事件发生时,会完全暂停执行并释放掉其占用的系统资源。这种状态下,进程不会被调度执行,直到等待的条件得到满足。通常需要外部触发或特定事件发生才能唤醒进程。进程处于浅度睡眠状态时,虽然也在等待某些条件或事件,但它仍然保持一定的活跃性。这种状态下,进程可能周期性地检查等待的条件是否满足,或者响应来自系统的某些信号,从而可能更快地恢复到就绪状态。
  • 暂停态
    并不是进程的终止,只是被信号暂时停止了,还可以恢复。

system函数简介

system()

#include 

int system(const char *command);

它允许你从程序中执行外部命令或者程序。使用该函数,你可以在你的程序中调用像 shell 命令一样的系统命令。

原子操作

原子操作是指不可分割的操作,即在执行过程中不会被中断的操作。在并发编程中,原子操作对于保证多线程或多进程之间的数据一致性非常重要。坏处是自己单独连续占用CPU时间。尽量避免不必要的原子操作,就算不得不原子操作,尽量缩短时间。

#include 

int main() {
    printf("ls -l\n");
    // 执行一个命令(在这个例子中是打印当前目录下的文件列表)
    system("ls -l");
    return 0;
}

一文讲透Linux应用编程—进程原理_第10张图片

进程之间的关系

  • 父子进程关系
    子进程会继承父进程的很多特性,包括文件描述符、环境变量、工作目录等。父子进程之间通过系统调用来建立关系,通常是 fork() 调用。子进程具有与父进程相同的代码和数据空间的副本。
  • 进程组
    进程组是一个或多个进程的集合。它们通常是由同一个父进程创建的,并且具有相同的进程组ID。每个进程组都有一个进程组ID(PGID),第一个进程的ID通常也是该进程组的ID。进程组的存在允许一组进程共享同一个终端并且可以接收同一信号。
  • 会话
    会话是一个或多个进程组的集合。一个会话通常是由一个用户启动的,它可以包含多个进程组。会话拥有一个会话ID(SID),第一个进程的ID通常也是会话ID。会话对于终端的控制非常重要,当一个终端连接被建立时,会话与之关联,断开连接时,会话也会结束。

守护进程的引入

进程查看命令ps

  • ps -ajx
    -a: 显示所有用户的进程,而不仅仅是当前用户的进程。
    -j: 显示作业控制的信息。这会显示作业控制相关的列,比如进程的进程组ID、会话ID等。
    -x: 显示没有控制终端的进程。
    这个命令会显示系统中所有用户的进程信息,并包括作业控制的相关信息,同时显示没有控制终端的进程。这个命令能够提供更详细和全面的进程信息,包括进程组ID、会话ID等,
  • ps -aux
    -a: 显示所有用户的进程。
    -u: 显示详细的用户及其他与进程相关的列。
    -x: 显示没有控制终端的进程。
    一般情况下,ps -aux 会显示所有用户的进程信息,并提供更详细的列,如用户、PID、CPU利用率、内存使用情况、启动时间等等。这个命令通常用于查看系统中运行的所有进程以及它们的详细信息,对于系统监控和故障排查很有用。
    一文讲透Linux应用编程—进程原理_第11张图片-ajx 偏向于显示各种有关的ID号, -aux偏向显示进程的各种占用资源

向进程发送信号指令kill

kill 命令用于向进程发送信号,通知进程执行某些动作。尽管名字是 “kill”,但它并不仅仅用于终止进程,而是可以发送各种不同的信号,其中最常见的是终止信号 SIGKILL。
基本语法是:kill [信号选项] 进程ID
一些常用的信号选项包括:

  • -9 或 -SIGKILL:终止进程。这个信号是无法被进程捕获或忽略的,是强制终止的信号。一般用于结束不响应其他信号的进程。
  • -15 或 -SIGTERM:默认信号,请求进程正常终止。进程可以捕获这个信号并执行清理工作,然后优雅地关闭。
  • -HUP 或 -1:挂起信号,通常用于通知进程重新加载配置文件或重新启动。
  • -INT 或 -2:中断信号,通常用于中断进程的运行。
  • -STOP 或 -19:停止进程的运行。进程会暂时停止,但仍保持在内存中等待继续执行的信号。
  • -CONT 或 -18:继续进程的执行。用于让暂停的进程继续运行。
    举例:kill -9 1234 将会强制终止进程ID为 1234 的进程。而 kill -15 5678 则是发送一个请求正常终止的信号给进程ID为 5678 的进程。
    发送信号给进程允许在管理和控制进程的时候进行各种操作,但请注意,强制终止进程可能会导致数据丢失或系统不稳定,所以应谨慎使用。

守护进程 daemon

守护进程(Daemon Process)是在计算机系统后台运行的一种特殊进程,它在系统启动时启动,以服务于系统或其他应用程序,并在系统关闭时自动关闭。它通常不与任何用户交互,以无人值守的方式运行。进程名后面加了d基本上是守护进程
一文讲透Linux应用编程—进程原理_第12张图片
它们通常具有以下特点:
1.后台运行:守护进程在系统启动时启动,并且在系统关闭时关闭,而不依赖于用户的直接操作。
2.无交互性:它们不与用户交互,通常没有控制终端。
3.独立于用户登录会话:它们通常不受特定用户登录或注销的影响,而是作为系统服务运行。
4.执行特定任务:通常被设计用来执行特定的任务或服务,比如网络服务、日志记录、定时任务等。
5.脱离终端控制:通常它们会脱离任何终端控制,这意味着它们不会受到用户登录或注销的影响。
常见的守护进程包括网络服务器(如HTTP服务器)、数据库服务、系统监控服务、定时任务调度服务等。它们为系统提供稳定、持续的服务,并且在系统运行期间处于后台运行状态,不需要用户交互或干预。是应用程序,不是内核程序也不是驱动,只是很难被关闭。

创建一个守护进程

任何进程都可以将自己实现成为一个守护进程。
创建一个守护进程 需要以下几点
创建子进程:通过 fork() 系统调用来创建子进程,并在父进程中退出,让子进程继续执行。

脱离控制终端:调用 setsid() 来创建新的会话,并将子进程设置为新会话的组长和领头进程,使其脱离控制终端的影响。

切换工作目录(可选):为了避免程序中途卸载的问题,可以将当前工作目录切换到根目录或其他安全目录,比如 chdir(“/”)。

重设文件权限掩码(可选):可以使用 umask(0) 来重新设置文件创建掩码,确保守护进程可以创建文件和目录的权限不受限制。

关闭文件描述符:关闭不需要的文件描述符,以避免它们在守护进程运行期间浪费系统资源。

执行核心任务:执行守护进程的核心任务,比如运行服务、监控系统等。

处理信号(可选):根据需要注册信号处理程序,处理守护进程可能接收到的信号。

永久运行:守护进程通常需要保持运行,所以在主循环中使用适当的延迟机制(如 sleep() 或事件驱动机制)来确保守护进程持续运行。

#include 
#include 
#include 
#include 
#include 
#include 
#define MAX_BUFFER_SIZE 1024
void main() {
    pid_t pid = fork();
    if (pid < 0) {
        perror("Fork failed");
        exit(EXIT_FAILURE);
    }
    if (pid > 0) {
        // 父进程退出
        exit(EXIT_SUCCESS);
    }

    // 子进程继续执行

    //  脱离终端  将当前进程设置为一个新的会话期
    setsid();

    // 更改工作目录
    chdir("/");

    // 关闭不必要的文件描述符
    close(STDIN_FILENO);
    close(STDOUT_FILENO);
    close(STDERR_FILENO);
    //将自身设置为最大的操作权限
    umask(0);

    // 核心执行后台功能
    openlog("MyDaemon", LOG_PID, LOG_DAEMON); // Open the syslog for the daemon
    while (1) {
        //打印内核信息
      
        syslog(LOG_INFO, "This is a kernel message from my daemon."); // Log a message
        sleep(1); 
    }
    closelog();
    exit(EXIT_SUCCESS);
}

编译执行后没有任何输出 但是可以通过ps -ajx 查看到进程ID,而且关闭终端之后,也不会消失。
在这里插入图片描述
如果想要终止进程,只能使用kill -9 27210 发送终止信号。

使用syslog 记录调试信息

syslog 是一个用于在 Unix-like 系统中记录日志的系统调用和库函数。它允许应用程序将日志消息写入系统日志文件,以便于跟踪、调试和审计。

openlog("my_program", LOG_CONS | LOG_PID | LOG_NDELAY, LOG_LOCAL1);

这里,“my_program” 是程序名称,用于标识日志消息的来源。LOG_PID 选项表示将进程ID附加到每个日志消息中,而 LOG_CONS 和 LOG_NDELAY 选项则用于控制日志的输出方式。
使用 syslog 函数来记录调试信息。它接受两个参数:第一个是日志的优先级掩码,用于指定哪些级别的消息应该被记录;第二个是要记录的消息。

syslog(LOG_INFO, "This is a kernel message from my daemon."); 

在终止程序之后,使用 **closelog()**关闭日志
使用命令查看日志信息

tail -f /var/log/syslog

上述代码执行效果:
一文讲透Linux应用编程—进程原理_第13张图片

syslog原理

操作系统中有一个守护进程syslogd(开机运行,关机才结束),这个守护进程syslogd负责进行日志文件的写入和维护。syslogd独立于任何一个进程而运行,我们当前进程可以通过调用openlog打开一个syslogd相连接的通道,我们通过syslog向syslogd发消息,然后由syslogd来将其写入到日志文件系统中。syslogd 相当于一个日志文件系统的服务器进程,提供日志服务,任何需要写日志的进程都可以通过openlog/syslog/closelog 三个函数来利用syslogd提供的服务。

禁止程序多次运行

当我们编写了守护进程之后,进程无法直接退出,当我们再次运行,后台就会又多一个守护进程,占用后台资源。一般来讲,守护进程只能运行一次。我们的守护进程应该有单例运行的功能。
实现方法:用一个文件的存在与否来做标志。具体做法是程序在执行之初去判断一个文件是否存在,若存在则表明进程已经在运行,若不存在则标明进程没有在运行。然后运行程序时去创建这个文件。当程序结束的时候的去删除这个文件即可。
详情示例,单例运行守护进程,进程创建之后,会创建一个临时文件,保证程序不会多次运行。第二次运行监测到文件存在会立即退出。如果提示权限被拒绝,请尝试使用chmod 777 之后继续运行

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#define MAX_BUFFER_SIZE 1024

#define FILE "/var/lock_file_single"

int main() {
    umask(0);
    int fd = -1;
    //运行之前,先去判断文件是否存在
    fd = open(FILE,O_RDWR | O_TRUNC | O_CREAT | O_EXCL,0664);
    if(fd < 0 ){
        perror("文件已经存在,请不要重复运行程序");
        exit(0);
    }
    pid_t pid = fork();

    if (pid < 0) {
        perror("Fork failed");
        exit(EXIT_FAILURE);
    }
    if (pid > 0) {
        // 父进程退出
        exit(EXIT_SUCCESS);
    }

    // 子进程继续执行
    int print_cnt =0;
    //  脱离终端  将当前进程设置为一个新的会话期
    setsid();

    // 更改工作目录
    chdir("/");

    // 关闭不必要的文件描述符
    close(STDIN_FILENO);
    close(STDOUT_FILENO);
    close(STDERR_FILENO);
    //将自身设置为最大的操作权限
    umask(0);
    // 核心执行后台功能
    openlog("MyDaemon", LOG_PID, LOG_DAEMON); // Open the syslog for the daemon
    while (print_cnt<30) {
        //打印内核信息
        print_cnt++;
        syslog(LOG_INFO, "This is a kernel message from my daemon.,%d",print_cnt); // Log a message
        sleep(1); 
    }

    //删除文件 保证程序退出之后,下次能继续运行
    if (remove(FILE) == 0) {
        printf("文件 %s 已被删除\n", FILE);
    } else {
        perror("删除文件失败");
    }
    syslog(LOG_INFO, "守护进程已经退出,%d",print_cnt); 
    closelog();
    close(fd);
    return 0;
}

实现效果:
一文讲透Linux应用编程—进程原理_第14张图片

你可能感兴趣的:(Linux,笔记,linux,运维,服务器)