4、linux c 进程

【三】进程

1. 进程与程序的区别

  • 程序:存放在磁盘上的指令和数据的有序集合(文件),是静态的。

  • 进程:执行一个程序所分配的资源的总称,是动态的。

2. 进程的组成部分

  • BSS段(bss):存放程序中未初始化的全局变量。

  • 数据段(data):存放已初始化的全局变量。

  • 代码段(text):存放程序执行代码。

  • 堆(heap) :通过malloc等函数分配内存,动态扩张或缩减。

  • 栈(stack):存放局部变量、函数参数和函数返回值,先进后出。

  • 进程控制块(PCB):包含进程ID(PID)、进程优先级、文件描述符表等。

3. 进程类型

  • 交互进程:用户通过终端与系统交互的进程。

  • 批处理进程:在后台运行,不需要用户交互。

  • 守护进程:在后台运行的特殊进程,通常用于系统服务。

4. 进程状态

  • 运行态:进程正在运行。

  • 等待态:进程等待某些资源(如I/O)。

  • 停止态:进程被暂停。

  • 死亡态:进程已结束,但父进程尚未读取其状态信息。

5. 堆与栈

  • 堆(heap):用于存放进程运行中被动态分配的内存段,当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)

  • 栈(stack):又称堆栈,是用户存放程序临时创建的局部变量,(但不包括static声明的变量,static意味着在数据段中存放变量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进后出特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。


一、查看进程信息

1. ps命令

用于查看系统进程的快照。

1.1 常用参数

  • -e:显示所有进程。

  • -l:长格式显示更详细信息。

  • -f:全格式显示,通常与其他选项联用。

1.2 输出字段说明

字段 含义
F 进程标志,说明进程的权限。常见标志:1(不可执行)、4(超级用户权限)等。
S 进程状态。常见状态:-D(不可唤醒的睡眠状态)、-R(运行)、-S(睡眠)、-T(停止)、-Z(僵尸进程)等。
UID 运行此进程的用户ID。
PID 进程的ID。
PPID 父进程的ID。
C 进程的CPU使用率(百分比)。
PRI 进程的优先级,数值越小优先级越高。
NI 进程的nice值,数值越小优先级越高。
ADDR 进程在内存中的位置。
SZ 进程占用的内存大小。
WCHAN 进程是否运行,"-"表示正在运行。
TTY 进程由哪个终端产生。
TIME 进程占用CPU的时间。
CMD 产生此进程的命令名。

1.3 示例

复制代码
ps -elf

2. top命令

用于查看进程的动态信息。

2.1 常用操作

  • shift + >:后翻页。

  • shift + <:前翻页。

  • top -p PID:查看指定进程。

3. /proc目录

用于查看进程的详细信息。

每个进程在/proc目录下都有一个以PID命名的子目录,包含进程的各种信息文件,如statusmaps等。


二、改变进程优先级

1. nice命令

按用户指定的优先级运行进程。

1.1 命令格式

复制代码
nice [-n NI值] 命令
  • NI值范围:-20~19。

    • 数值越大,优先级越低。

    • 普通用户可调整范围为0~19,只能调高优先级。

    • 只有root用户可以设置负值。

2. renice命令

改变正在运行进程的优先级。

2.1 命令格式

复制代码
renice [优先级] PID

三、进程管理

1. 查看后台进程

1.1 命令

复制代码
jobs

2. 将挂起的进程在后台运行

2.1 命令

复制代码
bg

3. 将后台进程放到前台运行

3.1 命令

复制代码
fg

4. 将前台进程转为后台并停止

4.1 操作

复制代码
Ctrl + Z

5. 后台运行程序

5.1 示例

复制代码
./test &

四、创建子进程

1. fork()函数

创建新的进程。

1.1 函数原型

复制代码
#include <unistd.h>
pid_t fork(void);

1.2 返回值

  • 成功

    • 父进程返回子进程的PID。

    • 子进程返回0。

  • 失败:返回-1。

1.3 特点

  • 子进程只执行fork()之后的代码。

  • 父子进程的执行顺序由操作系统决定。

  • 子进程继承父进程的内容,但父子进程有独立的地址空间。

1.4 注意事项

  • 若父进程先结束:

    • 子进程成为孤儿进程,被init进程收养。

    • 子进程可能成为后台进程。

  • 若子进程先结束:

    • 父进程如果没有及时回收,子进程变成僵尸进程。

1.5 示例

复制代码
#include <stdio.h>
#include <unistd.h>
​
int main() {
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork failed");
        return 1;
    } else if (pid == 0) {
        printf("I am the child process, PID: %d\n", getpid());
    } else {
        printf("I am the parent process, PID: %d, Child PID: %d\n", getpid(), pid);
    }
    return 0;
}

五、进程的退出

1. exit()函数

结束当前进程。

1.1 函数原型

复制代码
#include <stdlib.h>
void exit(int status);

1.2 特点

  • 会刷新流缓冲区。

  • 通常用于正常退出。

2. _exit()函数

结束当前进程。

2.1 函数原型

复制代码
#include <unistd.h>
void _exit(int status);

2.2 特点

  • 不刷新流缓冲区。

  • 通常用于异常退出。

3. returnexit的区别

  • main函数结束时会隐式调用exit

  • 普通函数的return是返回上一级。


六、进程的回收

1. wait() 函数

回收子进程。

1.1 函数原型

复制代码
#include <unistd.h>
pid_t wait(int *status);

1.2 返回值

  • 成功时返回回收的子进程的 PID。

  • 失败时返回 -1。

1.3 特点

  • 若子进程未结束,父进程会阻塞。

  • status 用于保存子进程的返回值和结束方式。

  • statusNULL,表示直接释放子进程的 PCB。

2. waitpid() 函数

更灵活的子进程回收。

2.1 函数原型

复制代码
#include <unistd.h>
pid_t waitpid(pid_t pid, int *status, int options);

2.2 参数说明

  • pid

    • >0:只等待指定 PID 的子进程。

    • = -1:等待任意子进程。

    • = 0:等待同一进程组中的子进程。

    • < -1:等待指定进程组中的子进程。

  • options

    • WNOHANG:非阻塞,若子进程未结束,立即返回 0。

    • WUNTRACED:返回终止子进程信息和因信号停止的子进程信息。

2.3 示例代码

示例 1:wait() 示例
复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
​
int main() {
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork failed");
        return 1;
    }
​
    if (pid == 0) {
        // 子进程
        printf("Child process, PID: %d\n", getpid());
        sleep(2); // 模拟子进程运行
        printf("Child process finished\n");
        return 0; // 子进程退出
    } else {
        // 父进程
        printf("Parent process, PID: %d, waiting for child...\n", getpid());
        int status;
        wait(&status); // 等待子进程结束
        printf("Child process exited with status: %d\n", status);
        return 0;
    }
}
示例 2:waitpid() 示例
复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
​
int main() {
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork failed");
        return 1;
    }
​
    if (pid == 0) {
        // 子进程
        printf("Child process, PID: %d\n", getpid());
        sleep(3); // 模拟子进程运行
        printf("Child process finished\n");
        return 0; // 子进程退出
    } else {
        // 父进程
        printf("Parent process, PID: %d, waiting for child...\n", getpid());
        int status;
        pid_t ret = waitpid(pid, &status, WNOHANG); // 非阻塞等待子进程
        if (ret == 0) {
            printf("Child process is still running\n");
        } else if (ret == -1) {
            perror("waitpid failed");
        } else {
            printf("Child process exited with status: %d\n", status);
        }
        return 0;
    }
}

七、exec 函数族

1. 背景

fork 创建进程后,子进程会继承父进程的代码和数据,父子进程执行相同的代码。然而,在实际开发中,我们通常希望子进程执行与父进程不同的程序。exec 函数族的作用就是用来替换当前进程的代码和数据,从而执行指定的程序。

2. 作用

exec 函数族用于替换当前进程的代码和数据,执行指定的程序。这些函数不会创建新的进程,而是直接替换当前进程的内容。

3. 函数原型

复制代码
#include <unistd.h>
​
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int system(const char *command);

4. 参数说明

4.1 execl

复制代码
int execl(const char *path, const char *arg, ...);
  • path:指定要执行的程序的路径。

  • arg :传递给程序的参数列表,必须以 NULL 结尾。

  • 特点:参数列表是可变参数,每个参数都是字符串。

4.2 execlp

复制代码
int execlp(const char *file, const char *arg, ...);
  • file :指定要执行的程序的文件名。execlp 会在环境变量 PATH 中查找该文件。

  • arg :传递给程序的参数列表,必须以 NULL 结尾。

  • 特点 :不需要指定文件的全路径,execlp 会自动在 PATH 中查找。

4.3 execv

复制代码
int execv(const char *path, char *const argv[]);
  • path:指定要执行的程序的路径。

  • argv :传递给程序的参数数组,必须以 NULL 结尾。

  • 特点:参数以数组形式传递,适合参数较多的情况。

4.4 execvp

复制代码
int execvp(const char *file, char *const argv[]);
  • file :指定要执行的程序的文件名。execvp 会在环境变量 PATH 中查找该文件。

  • argv :传递给程序的参数数组,必须以 NULL 结尾。

  • 特点 :不需要指定文件的全路径,execvp 会自动在 PATH 中查找。

4.5 system

复制代码
int system(const char *command);
  • command:要执行的命令字符串。

  • 特点system 是一个高级函数,会调用 /bin/sh 来执行命令。它会创建一个子进程来运行命令,并等待命令执行完成。

5. 返回值

  • 成功 :执行指定的程序,不会返回到调用 exec 的代码。

  • 失败 :返回 -1,并设置 errno

6. 特点

  1. 进程替换exec 函数会替换当前进程的代码和数据,但进程号(PID)保持不变。

  2. 参数传递

    • 第一个参数(如 argargv[0])通常是程序的名称,虽然它在程序中没有实际用途。

    • 参数列表或数组必须以 NULL 结尾。

  3. 路径查找

    • execlexecv 需要指定程序的完整路径。

    • execlpexecvp 会在环境变量 PATH 中查找程序。

  4. 环境变量exec 函数不会改变当前进程的环境变量。

7. 示例代码

示例 1:execl 示例

复制代码
#include <stdio.h>
#include <unistd.h>
​
int main() {
    printf("Before exec\n");
    execl("/bin/ls", "ls", "-l", NULL); // 替换当前进程为 ls 程序
    printf("After exec\n"); // 不会执行到这里
    return 0;
}

示例 2:execlp 示例

复制代码
#include <stdio.h>
#include <unistd.h>
​
int main() {
    printf("Before exec\n");
    execlp("ls", "ls", "-l", NULL); // 替换当前进程为 ls 程序
    printf("After exec\n"); // 不会执行到这里
    return 0;
}

示例 3:execv 示例

复制代码
#include <stdio.h>
#include <unistd.h>
​
int main() {
    printf("Before exec\n");
    char *args[] = {"/bin/ls", "-l", NULL};
    execv("/bin/ls", args); // 替换当前进程为 ls 程序
    printf("After exec\n"); // 不会执行到这里
    return 0;
}

示例 4:execvp 示例

复制代码
#include <stdio.h>
#include <unistd.h>
​
int main() {
    printf("Before exec\n");
    char *args[] = {"ls", "-l", NULL};
    execvp("ls", args); // 替换当前进程为 ls 程序
    printf("After exec\n"); // 不会执行到这里
    return 0;
}

示例 5:system 示例

复制代码
#include <stdio.h>
#include <stdlib.h>
​
int main() {
    printf("Before system\n");
    system("ls -l"); // 使用 shell 执行命令
    printf("After system\n"); // 会执行到这里
    return 0;
}

exec 函数族的使用注意事项

1. 参数传递

  • 参数列表或数组必须以 NULL 结尾,否则会导致未定义行为。

  • 第一个参数(如 argargv[0])通常是程序的名称,虽然它在程序中没有实际用途,但建议正确填写。

2. 环境变量

  • exec 函数不会改变当前进程的环境变量。如果需要修改环境变量,可以使用 putenvsetenv 函数。

3. 文件描述符

  • 在调用 exec 函数之前,应确保关闭不需要的文件描述符,以避免资源泄漏。

  • 如果需要保留某些文件描述符(如日志文件),应确保它们在调用 exec 之前已正确打开。

4. 错误处理

  • 如果 exec 函数失败,会返回 -1,并设置 errno。可以通过 perrorstrerror 获取错误信息。

  • 常见错误包括:

    • 文件路径错误或文件不存在。

    • 文件没有执行权限。

    • 环境变量 PATH 中未找到程序。

  1. fork 结合使用
  • exec 函数通常与 fork 结合使用,创建子进程并执行不同的程序。

  • 示例代码:

复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
​
int main() {
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork failed");
        return 1;
    }
​
    if (pid == 0) {
        // 子进程
        printf("Child process, PID: %d\n", getpid());
        execlp("ls", "ls", "-l", NULL); // 子进程执行 ls 程序
        perror("execlp failed"); // 如果 execlp 失败,会执行到这里
        return 1;
    } else {
        // 父进程
        printf("Parent process, PID: %d\n", getpid());
        wait(NULL); // 等待子进程结束
        printf("Child process finished\n");
    }
    return 0;
}

6. 使用 system 的注意事项

  • system 函数会调用 /bin/sh 来执行命令,因此可能会引入安全风险(如注入攻击)。

  • 如果需要执行简单的命令,可以使用 system,但建议优先使用 exec 函数族。

  • 示例:避免注入攻击

复制代码
#include <stdio.h>
#include <stdlib.h>
​
int main() {
    char *filename = "test.txt";
    char command[256];
    snprintf(command, sizeof(command), "ls -l %s", filename); // 安全地构造命令
    system(command);
    return 0;
}

八、守护进程(Daemon Process)

1. 概念

守护进程(Daemon Process)是一种生存期较长的进程,通常独立于控制终端,并周期性地执行某种任务或等待处理某些事件。

2. 特点

  • 始终在后台运行,独立于任何终端。

  • 周期性地执行任务或等待处理特定事件。

  • 是一种特殊的孤儿进程,脱离终端,避免被终端信息打断。

3. 创建守护进程

3.1 简便方法

使用 nohup 命令:

复制代码
nohup xxxx &

3.2 使用 setsid 函数

复制代码
#include <unistd.h>
​
pid_t setsid(void);
  • 成功时返回调用进程的会话 ID。

  • 失败时返回 -1,并设置 errno

  • 调用 setsid 的进程成为新的会话组长和组长进程。

3.3 使用 getsid 函数

复制代码
#include <unistd.h>
​
pid_t getsid(pid_t pid);
  • 成功时返回调用进程的会话 ID。

  • 失败时返回 -1,并设置 errno

  • 参数 pid 为 0 时,查看当前进程的会话 ID。

3.4 其他辅助函数

  • getpid:获取进程 ID。

    复制代码
    pid_t getpid(void);
  • getpgid:获取进程组 ID。

    复制代码
    pid_t getpgid(pid_t pid);

4. 创建守护进程的步骤

  1. 创建子进程,父进程退出

    复制代码
    if (fork() > 0) {
        exit(0); // 父进程退出
    }
  2. 子进程变成孤儿进程,被 init 进程收养

    • 子进程在后台运行。
  3. 子进程创建新会话

    复制代码
    if (setsid() < 0) {
        exit(-1); // 创建会话失败
    }
    • 子进程成为新的会话组长。

    • 子进程脱离原先的终端。

  4. 更改当前工作目录

    复制代码
    chdir("/"); // 或 chdir("/tmp");
    • 防止守护进程的工作目录被卸载。
  5. 重设文件权限掩码

    复制代码
    umask(0); // 文件权限掩码设置为 0
  6. 关闭打开的文件描述符

    复制代码
    for (int i = 0; i < 3; i++) {
        close(i); // 关闭标准输入、输出和错误
    }

4.1 示例代码

复制代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
​
int main() {
    // 创建子进程,父进程退出
    if (fork() > 0) {
        exit(0); // 父进程退出
    }
​
    // 创建新会话
    if (setsid() < 0) {
        perror("setsid failed");
        exit(-1);
    }
    
// 更改当前工作目录
if (chdir("/") < 0) {
    perror("chdir failed");
    exit(-1);
}
​
// 重设文件权限掩码
umask(0);
​
// 关闭打开的文件描述符
for (int i = 0; i < 3; i++) {
    close(i); // 关闭标准输入、输出和错误
}
​
// 执行守护进程的任务
while (1) {
    printf("Daemon process running...\n");
    sleep(10); // 模拟周期性任务
}
​
return 0;
}

九、守护进程的注意事项

1. 避免守护进程占用终端

守护进程不应与终端关联,否则可能会因终端关闭而退出。使用 setsid() 可以确保进程脱离终端。

2. 防止工作目录被卸载

守护进程的工作目录应设置为根目录(/)或 /tmp,避免因工作目录被卸载而导致守护进程无法运行。

3. 关闭不必要的文件描述符

守护进程不应继承父进程的文件描述符。关闭标准输入、输出和错误(stdinstdoutstderr),并根据需要重新打开日志文件。

4. 日志记录

守护进程通常需要记录日志以便调试和监控。可以将日志写入 /var/log 或其他日志文件中,而不是直接输出到终端。

5. 示例:守护进程与日志记录

示例代码

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
​
int main() {
    // 创建子进程,父进程退出
    if (fork() > 0) {
        exit(0); // 父进程退出
    }
​
    // 创建新会话
    if (setsid() < 0) {
        perror("setsid failed");
        exit(-1);
    }
​
    // 更改当前工作目录
    if (chdir("/") < 0) {
        perror("chdir failed");
        exit(-1);
    }
​
    // 重设文件权限掩码
    umask(0);
​
    // 关闭标准输入、输出和错误
    for (int i = 0; i < 3; i++) {
        close(i);
    }
​
    // 打开日志文件
    int log_fd = open("/var/log/mydaemon.log", O_WRONLY | O_CREAT | O_APPEND, 0644);
    if (log_fd < 0) {
        perror("open log file failed");
        exit(-1);
    }
​
    // 重定向标准输出和错误到日志文件
    dup2(log_fd, STDOUT_FILENO);
    dup2(log_fd, STDERR_FILENO);
​
    // 执行守护进程的任务
    while (1) {
        printf("Daemon process running...\n");
        sleep(10); // 模拟周期性任务
    }
​
    return 0;
}
相关推荐
小辉同志6 分钟前
C语言之链表
c语言·开发语言·链表
可问 可问春风2 小时前
Linux 找回 Root 密码(多发行版本)
linux·运维·chrome
拾忆,想起2 小时前
Nacos命名空间Namespace:微服务多环境管理的“秘密武器”如何用?
java·运维·spring boot·spring cloud·微服务·架构
双叶8364 小时前
(C语言)写一个递归函数DigitSum(n),输入一个非负整数,返回组成它的数字之和(递归函数)
c语言·开发语言·数据结构·算法·游戏
木子欢儿4 小时前
Debian系统清理垃圾
linux·运维·服务器·debian
翻滚吧键盘4 小时前
查看debian的版本信息
运维·chrome·debian
Blockchina4 小时前
Python自动化脚本:2分钟快速搭建MTProto代理服务(支持多端口负载均衡)
运维·自动化·代理模式
字节源流4 小时前
【SpringMVC】常用注解:@SessionAttributes
java·服务器·前端
谷晓光4 小时前
python中print函数的flush如何使用
linux·服务器·数据库
Hacker_Albert4 小时前
Linux 内核模块签名
linux