创建进程

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
    pid_t pid; // 进程id类型
    // 创建一个新进程
    pid = fork();
    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, Parent PID: %d\n", pid, getpid());
    }
    return 0;
}

getpid()函数可以获取当前pid

exit函数

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
int main(void)
{
    printf("this process will exit");
    exit(0);
    printf("never be displayed");
}

wait函数回收进程

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
    pid_t pid;

    // 创建一个子进程
    pid = fork();

    if (pid < 0)
    {
        perror("fork");
        exit(-1);
    }
    else if (pid == 0)
    {
        printf("This is the child process,PID=%d\n", getpid());
        sleep(2);
        exit(12);
    }
    else
    {
        // 父进程代码
        printf("This is the parent process. Child PID: %d, Parent PID: %d\n", pid, getpid());
        // 使用wait等待子进程结束,获取退出状态码
        int status;
        wait(&status);
        // NOTE: WIFEXITED宏用于判断子进程是否正常退出,WEXITSTATUS宏用于获取子进程的退出状态值。
        /**
         * - `WEXITSTATUS`宏用于获取子进程的退出状态码。它接受`wait()`或`waitpid()`函数返回的状态码作为参数,并返回子进程的退出状态码。使用`WIFEXITED`宏之前,应该先使用`WIFEXITED`宏判断子进程是否正常退出。
            码作为参数,如果子进程正常退出,则返回一个非零值,否则返回0。
            通过判断子进程是否正常退出,可以决定是否使用`WEXITSTATUS`宏来获取子进程的退出状态码。
        */
        // NOTE: 注意:status是一个字节,也就是说,不能超过255,
        // 也可以把status>>8位获得
        // 如果程序的退出状态码超过255,它将被截断为一个字节。如果在`exit()`函数中传递了一个负数作为参数,它将被转换为一个正数。
        if (WIFEXITED(status))
        {
            printf("Child process exited with status %d\n", WEXITSTATUS(status));
        }
    }
    return 0;
}

信号结束获取返回值

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <signal.h>
int main()
{
    pid_t pid;
    pid = fork();
    if (pid < 0)
    {
        perror("fork");
        exit(-1);
    }
    else if (pid == 0)
    {
        printf("This is the child process,PID = %d", getpid());
        raise(SIGTERM);
        sleep(10);
        exit(1);
    }
    else
    {
        // 父进程代码
        printf("This is the parent process. Child PID: %d, Parent PID: %d\n", pid, getpid());
        int status;
        wait(&status);
        // NOTE: WIFEXITED宏用于判断子进程是否正常退出,WEXITSTATUS宏用于获取子进程的退出状态值。
        /**
         * - `WEXITSTATUS`宏用于获取子进程的退出状态码。它接受`wait()`或`waitpid()`函数返回的状态码作为参数,并返回子进程的退出状态
            码作为参数,如果子进程正常退出,则返回一个非零值,否则返回0。
            通过判断子进程是否正常退出,可以决定是否使用`WEXITSTATUS`宏来获取子进程的退出状态码。
        */
        // NOTE: 注意:status是一个字节,也就是说,不能超过255,
        // 也可以把status>>8位获得
        // 如果程序的退出状态码超过255,它将被截断为一个字节。如果在`exit()`函数中传递了一个负数作为参数,它将被转换为一个正数。
        if (WIFEXITED(status))
        {
            printf("Child process exited normally with status : %d\n", WEXITSTATUS(status));
        }

        // NOTE: WIFSIGNALED(status)判断子进程是否被信号结束
        // WTERMSIG(status) 获取结束子进程的信号类型
        else if (WIFSIGNALED(status))
        {
            printf("Child process terminated by signal: %d\n", WTERMSIG(status));
        }
    }
    return 0;
}

waitpid函数回收进程

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
    pid_t pid;
    pid = fork();
    if (pid < 0)
    {
        perror("fork");
        exit(-1);
    }
    else if (pid == 0)
    {
        printf("This is the child process, PID=%d\n", getpid());
        sleep(2);
        exit(1);
    }
    else
    {
        printf("This is the parent process. Child PID: %d, Parent PID: %d\n", pid, getpid());
        int status;
        //pid_t waitpid(pid_t pid, int *status, int option);
        //成功时返回回收的子进程的pid或0;失败时返回EOF 
        //pid可用于指定回收哪个子进程或任意子进程
        //status指定用于保存子进程返回值和结束方式的地址
        //option指定回收方式,0 或 WNOHANG
        //NOTE: 使用waitpid等待指定子进程结束,获取退出状态码
        pid_t result = waitpid(pid, &status, 0);
        if (result == -1)
        {
            perror("waitpid");
            exit(-1);
        }
        if (WIFEXITED(status))
        {
            printf("Child process exited normally with status: %d\n", WEXITSTATUS(status));
        }
    }
}

exec函数族

exec函数族包括execl、execv、execle、execlp、execvp、execve等变体,它们之间的主要区别在于如何传递参数和环境变量。

execl和execlp

下面是一个使用execl函数的基本例子,它在C语言中执行一个名为ls的程序

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

int main() {
    // 调用execl来执行ls程序
    // 参数列表:程序路径,程序名,"ls","-l",NULL(表示参数列表结束)
    if (execl("/bin/ls", "ls", "-l", (char *)NULL) == -1) {
        // 如果execl调用失败,会返回-1,并且当前进程不会被替换
        perror("execl failed");
        return 1;
    }

    // 如果execl成功,程序不会执行到这里,因为当前进程已经被替换了
    return 0;
}

下面是一个执行execlp的例子

#include <stdio.h>
#include <unistd.h>
int main()
{
    if (execlp("ls", "ls", "-l", (char *)NULL) == -1)
    {
        // 如果execlp调用失败,会返回-1,并且当前进程不会被替换
        perror("execl failed");
        return 1;
    }
    return 0;
}


execv 和 execvp

execv 和 execvp 函数都是用来在当前进程中执行一个新的程序。它们的主要区别在于 execvp 会根据环境变量 PATH 来查找可执行文件,而 execv 则需要你提供完整的文件路径。

execvexecvp 函数都是用来在当前进程中执行一个新的程序。它们的主要区别在于 execvp 会根据环境变量 PATH 来查找可执行文件,而 execv 则需要你提供完整的文件路径。

下面是一个使用 execv 的例子,它假设你有一个名为 myprogram 的可执行文件,并且这个文件位于当前目录或者在 PATH 环境变量指定的路径中:

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

int main() {
    // 定义一个指向参数的指针数组
    char *argv[] = {"myprogram", "arg1", "arg2", NULL};

    // 使用execv执行程序
    if (execv("myprogram", argv) == -1) {
        perror("execv failed");
        return 1;
    }

    // 如果execv成功,程序不会执行到这里
    return 0;
}

在这个例子中,argv 数组包含了要传递给 myprogram 的参数。argv[0] 是程序名,argv[1]argv[2] 是传递给程序的参数,最后一个 NULL 表示参数列表的结束。

下面是一个使用 execvp 的例子,它同样假设 myprogram 位于 PATH 环境变量指定的路径中:

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

int main() {
    // 定义一个指向参数的指针数组
    char *argv[] = {"myprogram", "arg1", "arg2", NULL};

    // 使用execvp执行程序
    if (execvp("myprogram", argv) == -1) {
        perror("execvp failed");
        return 1;
    }

    // 如果execvp成功,程序不会执行到这里
    return 0;
}

execvp 的例子中,你不需要指定文件的完整路径,因为 execvp 会根据 PATH 环境变量来查找 myprogram。如果 myprogram 不在 PATH 中,或者没有执行权限,execvp 会失败并返回错误。

system函数

#include <stdlib.h>
int main() {
    system("ls -l");
    return 0;
}

守护进程

守护进程(Daemon Process)是一种在后台运行的进程,它通常在系统启动时启动,并在系统运行期间持续运行,以提供系统服务或执行特定的任务。守护进程通常不与任何终端(终端会话)关联,它们独立于用户交互,可以在没有用户登录的情况下运行。

守护进程的特点包括:

  1. 后台运行:守护进程在后台执行,不占用终端,用户无法直接与之交互。

  2. 持续运行:守护进程通常在系统启动时启动,并持续运行,直到系统关闭或守护进程被显式停止。

  3. 服务提供者:守护进程提供各种系统服务,如网络服务(如HTTP服务器、FTP服务器)、系统监控、日志记录、定时任务执行等。

  4. 无用户交互:守护进程不需要用户输入,它们通过配置文件、系统调用或网络请求来接收指令。

  5. 进程管理:守护进程通常由系统管理员通过特定的命令行工具或脚本来管理,如启动、停止、重启和查看状态。

  6. 进程控制:守护进程可能需要特定的权限来执行某些操作,如访问系统资源或执行系统级任务。

在Unix和类Unix系统中,守护进程通常以 & 符号在后台启动,或者在系统启动脚本中配置为自动启动。例如,httpd(Apache HTTP服务器)和 sshd(SSH守护进程)都是常见的守护进程。

创建守护进程通常涉及以下步骤:

  • 分离与终端:调用 fork() 创建子进程,然后子进程调用 setsid() 来创建一个新的会话,这样即使父进程退出,子进程也能继续运行。

  • 改变工作目录:调用 chdir() 改变工作目录到根目录(/),以避免依赖于任何特定目录。

  • 关闭文件描述符:关闭所有打开的文件描述符,除了标准输入(stdin)、标准输出(stdout)和标准错误(stderr),这些通常重定向到 /dev/null

  • 处理信号:设置信号处理程序,以便正确处理如 SIGTERM(终止)和 SIGHUP(挂起)等信号。

  • 启动服务:开始执行守护进程的主要任务。

守护进程是操作系统中非常重要的组成部分,它们确保了系统的稳定运行和各种服务的可用性。


进程组(Process Group)、会话(Session)和控制终端(Controlling Terminal)是Unix和类Unix操作系统中用于管理进程的基本概念。下面是对这些概念的详细介绍:

  1. 进程组(Process Group)

    • 定义:进程组是一组进程的集合,这些进程共享某些属性,如作业控制信号。进程组的组长(Leader)是该组中第一个创建的进程,其进程ID(PID)也用作进程组ID(PGID)。
    • 创建:进程组可以通过fork()系统调用创建。当一个进程调用fork()创建子进程时,子进程会继承父进程的进程组。可以通过setpgid()系统调用来改变进程的进程组。
    • 用途:进程组允许对一组进程进行批量操作,例如,可以向整个进程组发送信号,或者改变整个进程组的工作目录。
  2. 会话(Session)

    • 定义:会话是进程组的集合,它提供了一个更高层次的组织结构。每个会话都有一个会话组长(Session Leader),通常是会话中的第一个进程。会话组长的进程ID(PID)就是会话ID(SID)。
    • 创建:会话可以通过setsid()系统调用创建。这个调用会创建一个新的会话和一个新的进程组,并且调用进程成为这两个新的组长。
    • 用途:会话允许进程组与控制终端分离,这对于创建守护进程特别有用,因为守护进程通常不希望与任何终端关联。
  3. 控制终端(Controlling Terminal)

    • 定义:控制终端是与进程组关联的终端设备。控制终端可以发送信号给与之关联的进程组,如挂起(SIGHUP)和中断(SIGINT)信号。控制进程(Controlling Process)是与控制终端关联的进程组的组长。
    • 用途:控制终端允许用户通过终端对进程组进行控制,例如,用户可以通过终端发送信号来控制进程组的行为。这对于交互式程序和脚本非常有用,因为它们通常需要与用户进行交互。

这些概念在Unix系统中用于实现作业控制,允许用户和系统管理员对进程进行更精细的管理。例如,用户可以通过终端控制与该终端关联的进程组,而守护进程则通过脱离控制终端来避免终端关闭时被意外终止。

umask 文件权限掩码

文件权限掩码(umask)是Linux和类Unix系统中用于控制新创建文件和目录默认权限的一种机制。它允许系统管理员和用户指定在创建新文件或目录时应该自动移除哪些权限。umask实际上是一个权限的“补码”,它定义了从默认权限中减去哪些权限,以生成实际的文件或目录权限。

在Linux中,文件和目录的权限通常由三个部分组成,分别对应文件所有者(owner)、所属组(group)和其他用户(others)。每个部分都有读(r)、写(w)和执行(x)三种权限,分别对应4、2和1的值。例如,一个文件的默认权限可能是666(-rw-rw-rw-),这意味着所有用户都有读写权限,但没有执行权限。

umask的值通常是一个八进制数,由三个数字组成,分别对应文件所有者、所属组和其他用户的权限。umask的每个数字都是从0到7的值,其中0表示不移除任何权限,7表示移除所有权限。umask的值是从默认权限中减去的,所以umask的值越大,新创建的文件和目录的默认权限就越小。

例如,如果umask的值是022(八进制),那么对于新创建的文件,其默认权限将是644(-rw-r–r–),对于新创建的目录,其默认权限将是755(drwxr-xr-x)。这里的计算方式是:

  • 文件默认权限:666(默认) - 022(umask) = 644(实际)
  • 目录默认权限:777(默认) - 022(umask) = 755(实际)

umask的值可以通过umask命令来查看和设置。在用户的shell配置文件(如.bashrc.bash_profile)中设置umask值可以使其在每次打开新的shell会话时自动生效。系统级别的umask值通常在/etc/profile/etc/bashrc中设置。

umask的设置对于系统安全非常重要,因为它可以防止新创建的文件和目录具有过于宽松的权限,从而减少安全风险。例如,通常不建议新创建的文件对所有用户都有写权限,因为这可能导致未授权的修改。通过合理设置umask,可以确保文件和目录的权限符合安全最佳实践。

dup函数

dup() 函数在Unix和类Unix系统中是一个用于复制文件描述符的系统调用。文件描述符是一个整数,它代表了打开的文件、管道、套接字等的引用。dup() 函数的作用是创建一个新的文件描述符,这个新的描述符与原始描述符指向同一个文件,并且共享相同的文件状态,包括文件偏移量、文件状态标志等。

函数原型:

#include <unistd.h>
int dup(int oldfd);

作用:

  • dup() 函数返回一个新的文件描述符,这个新的描述符与传入的 oldfd 参数指向同一个文件。
  • 新的描述符是当前系统中最小的未使用的文件描述符。
  • 如果 oldfd 是一个有效的文件描述符,那么新创建的描述符将具有与 oldfd 相同的权限和状态。
  • dup() 通常用于需要在同一个进程中对同一个文件进行多次操作的场景,例如,你可能需要在不同的文件描述符上执行不同的操作,但又不想关闭原始的文件描述符。

使用场景:

  1. 输入输出重定向:在shell脚本中,dup() 可以用来实现输入输出的重定向。例如,你可以将标准输出重定向到一个文件,同时保留标准输出用于其他目的。
  2. 进程间通信:在父子进程间,dup() 可以用来创建管道,允许父子进程通过同一个文件描述符进行通信。
  3. 文件操作:在需要对文件进行多次操作时,dup() 可以用来创建一个新的描述符,以便在不影响原始描述符的情况下进行操作。

示例:

以下是一个简单的C语言示例,展示了如何使用 dup() 函数:

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

int main() {
    int fd1, fd2;

    // 打开一个文件
    fd1 = open("example.txt", O_RDONLY);
    if (fd1 == -1) {
        perror("open");
        return 1;
    }

    // 使用dup()复制文件描述符
    fd2 = dup(fd1);
    if (fd2 == -1) {
        perror("dup");
        close(fd1);
        return 1;
    }

    // 使用fd2进行操作,例如读取文件内容
    // ...

    // 关闭文件描述符
    close(fd1);
    close(fd2);

    return 0;
}

在这个例子中,我们首先打开了一个名为 “example.txt” 的文件,然后使用 dup() 函数创建了一个新的文件描述符 fd2。这样,我们就可以在 fd2 上执行操作,而不会影响 fd1。最后,我们关闭了这两个文件描述符。

请注意,dup() 函数在不同的操作系统和环境中可能有不同的行为和限制。在实际应用中,你可能需要根据具体的需求和环境来调整代码。

创建守护进程代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <time.h>
#include <signal.h>
#include <stdbool.h>
// NOTE: 创建守护进程的函数
void create_daemon()
{
    // 创建子进程,父进程退出
    pid_t pid = fork();
    if (pid < 0)
    {
        perror("fork error");
        exit(EXIT_FAILURE);
    }
    else if (pid > 0)
    {
        exit(EXIT_SUCCESS);
    }

    // 创建新的会话,成为新的会话和进程组的组长
    if (setsid() < 0)
    {
        perror("setsid error");
        exit(EXIT_FAILURE);
    }

    // 再次创建子进程,确保守护进程不会被父进程的行为影响
    pid = fork();
    if (pid < 0)
    {
        perror("fork error");
        exit(EXIT_FAILURE);
    }

    // 更改当前工作目录为根目录
    if (chdir("/") < 0)
    {
        perror("chdir error");
        exit(EXIT_FAILURE);
    }
    // 设置文件权限掩码,以便守护进程创建的文件具有适当权限
    umask(0);

    // 关闭所有打开的文件描述符,避免资源泄露
    for (int i = 0; i < 3; i++)
    {
        close(i);
        open("/dev/null", O_RDWR); // 重定向标准输入、输出和错误到/dev/null
                                   /**
                                    *
                                    * O_RDWR 表示以可读写的方式打开文件。在打开文件时,你可以选择指定不同的标志位来定义文件的打开模式。以下是一些常见的文件访问标志:
                                       O_RDONLY:以只读方式打开文件。
                                       O_WRONLY:以只写方式打开文件。
                                       O_RDWR:以可读写方式打开文件。
                                   */
        dup(i);
        dup(i);
    }
    // 设置信号处理,忽略SIGCHLD信号,避免僵尸进程
    signal(SIGCHLD, SIG_IGN);

    // 主循环,守护进程的主要任务
    while (true)
    {
        // 这里可以添加守护进程的具体任务,例如写入日志文件
        // ...

        // 休眠一段时间
        sleep(60);
    }
}

int main()
{
    // 创建守护进程
    create_daemon();

    // 主进程不会执行到这里,因为create_daemon()函数中的while循环会一直运行
    return 0;
}

实际例子

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

// NOTE: 创建守护进程的函数
void create_daemon()
{
    // 创建子进程,父进程退出
    pid_t pid = fork();
    if (pid < 0)
    {
        perror("fork error");
        exit(EXIT_FAILURE);
    }
    else if (pid > 0)
    {
        exit(EXIT_SUCCESS);
    }

    // 创建新的会话,成为新的会话和进程组的组长
    if (setsid() < 0)
    {
        perror("setsid error");
        exit(EXIT_FAILURE);
    }

    // 再次创建子进程,确保守护进程不会被父进程的行为影响
    pid = fork();
    if (pid < 0)
    {
        perror("fork error");
        exit(EXIT_FAILURE);
    }

    // 更改当前工作目录为根目录
    if (chdir("/home/meowrain/learn_c") < 0)
    {
        perror("chdir error");
        exit(EXIT_FAILURE);
    }

    // 设置文件权限掩码,以便守护进程创建的文件具有适当权限
    umask(0);

    //关闭所有打开的文件描述符,避免资源泄露
    for (int i = 0; i < 3; i++)
    {
        close(i);
        open("/dev/null", O_RDWR); // 重定向标准输入、输出和错误到/dev/null
    }

    // 设置信号处理,忽略SIGCHLD信号,避免僵尸进程
    signal(SIGCHLD, SIG_IGN);

    // 主循环,守护进程的主要任务
    FILE *fp;
    time_t t;
    if ((fp = fopen("time.log", "a")) == NULL)
    {
        perror("fopen");
        exit(-1);
    }
    while (true)
    {
        time(&t);
        fprintf(fp, "%s", ctime(&t));
        fflush(fp);
        sleep(1);
    }
    fclose(fp); // Move this line outside the while loop
}

int main()
{
    // 创建守护进程
    create_daemon();

    // 主进程不会执行到这里,因为create_daemon()函数中的while循环会一直运行
    return 0;
}