进程的基本概念

进程

定义

进程是一段可执行代码,并放在内存中运行。

进程号

进程号是计算机识别进程的唯一标识。可以使用 pid_t getpid(void); 函数获取进程号。每个进程的进程号是由系统随机分配的。

父进程号

每个进程都有一个父进程号,表示创建该进程的父进程。可以使用 pid_t getppid(void); 函数获取父进程号。

进程

进程属于计算机资源管理的最小单位。一个程序至少包括一个进程。

进程组(Process Group)

  • 定义:进程组是一组相关进程的集合,它们通常是由同一作业启动的,或者有某种逻辑上的关联。
  • 特性:
    • 每个进程组有一个唯一的进程组ID(PGID)。
    • 进程组内的所有进程共享某些属性,如信号处理。
    • 进程组通常由一个前台进程组和一个或多个后台进程组组成。
    • 一个进程可以成为进程组的组长(Leader),此时它的进程ID(PID)就是该进程组的PGID。

会话(Session)

  • 定义:会话是一个或多个进程组的集合,它们通常与一个控制终端(Controlling Terminal)相关联。
  • 特性:
    • 每个会话有一个唯一的会话ID(SID)。
    • 会话由会话首进程(Session Leader)创建,会话首进程的PID同时也是会话的SID。
    • 会话可以有一个控制终端,这个终端可以与前台进程组交互。
    • 当会话首进程终止时,会话可能不会结束,除非它是最后一个进程组的最后一个进程。

进程组与会话的关系

  • 创建:当会话首进程创建时,它会创建一个初始进程组,该进程组成为会话的前台进程组。
  • 包含:一个会话可以包含多个进程组,但每个进程组只能属于一个会话。
  • 控制:会话首进程通常控制整个会话的行为,例如,它可以通过发送信号来影响会话中的所有进程。
  • 终端交互:会话中的前台进程组可以与控制终端交互,而后台进程组通常不能直接与终端交互。

属性

  • 动态性:进程可以动态产生,也可以动态消亡。
  • 独享性:每个进程都有自己独立的内存空间,不会与其他进程共享。
  • 并发行:多个进程可以并发执行,共同竞争CPU资源。
  • 异步性:在某一时间点,只能有一个进程的代码在执行。

五态模型

  • 新建态:进程刚被创建,但尚未开始执行。
  • 就绪态:进程已准备好执行,等待CPU时间片。
  • 运行态:进程正在执行。
  • 挂起态:进程因为某些原因(如等待I/O操作完成)而被暂停执行。
  • 终止态:进程已执行完毕或被系统终止。

进程间通讯

  • 信号:用于进程间同步和控制。
  • 管道:用于单向数据传输。
  • 消息队列:用于进程间传递消息。
  • 共享内存:用于进程间共享内存区域。
  • 信号量:用于进程间同步。
  • 套接字:用于网络中的进程间通讯。

创建子进程

使用 pid_t fork(void); 函数创建一个新的子进程。

  • 返回值
    • 0:表示当前进程是子进程。
    • child_pid:表示父进程的进程号。
    • -1:表示创建子进程失败。

使用pid_t vfork(void);函数创建一个新的子进程。

fork()vfork()区别:

fork()创建的子进程与父进程的内存是独享的

vfork()创建的子进程与父进程的内存是共享的,子进程不能用return退出,因为return是堆栈的释放,会导致段错误

vfork()创建的子进程确保先运行


进程退出

进程退出有8种方式,分为正常退出和异常退出。

正常退出

  • main 函数调用 return 的退出。
  • 调用 exit 函数。
  • 调用 _exit 或者 _Exit 函数。
  • 最后一个线程在运行实例中退出。
  • 最后一个线程调用pthread_exit()退出。

异常退出

1.调用 abort() 函数

  • 当你调用 abort() 函数时,它会引发 SIGABRT 信号,并立即终止进程。
  • 信号 SIGABRT 通常与一个硬件中断指令(如 int $0x6)关联,该指令会生成一个异常,导致操作系统发送 SIGABRT 信号给当前进程。
  • 这等同于使用 kill() 函数向当前进程发送 SIGABRT 信号:
kill(getpid(), SIGABRT);

2. 进程接收到信号而退出

  • 当进程接收到一个信号时,它可以通过调用 kill() 函数来终止进程。
  • kill() 函数的签名如下:
int kill(pid_t pid, int sig);
  • pid 参数表示要发送信号的进程号。如果 pid 为负数,信号会被发送给进程组中的所有进程。
  • sig 参数表示要发送的信号的编号。
    • SIGHUP (1):当终端会话结束时发送,通常用于通知守护进程。
    • SIGINT (2):当用户按下 Ctrl+C 时发送,用于中断进程。
    • SIGQUIT (3):当用户按下 Ctrl+\ 时发送,与 SIGINT 类似,但还会将进程的堆栈内容打印到标准错误输出。
    • SIGILL (4):当进程尝试执行非法指令时发送。
    • SIGTRAP (5):当进程执行了一个导致陷阱的指令时发送。
    • SIGABRT (6):当进程调用 abort() 函数时发送。
    • SIGIOT (6):与 SIGABRT 相同,是早期 Unix 系统中的信号名。
    • SIGBUS (7):当进程尝试访问非法内存地址时发送。
    • SIGFPE (8):当进程发生浮点异常时发送。
    • SIGKILL (9):无法被捕获或忽略的信号,用于强制终止进程。
    • SIGUSR1 (10):用户定义的信号,通常用于进程间通信。
    • SIGSEGV (11):当进程尝试访问无效的内存区域时发送。
    • SIGUSR2 (12):另一个用户定义的信号,通常用于进程间通信。
    • SIGPIPE (13):当进程试图写入一个已经关闭的管道时发送。
    • SIGALRM (14):当定时器超时时发送。
    • SIGTERM (15):用于优雅地终止进程。
    • SIGSTKFLT (16):当栈溢出时发送。
    • SIGCHLD (17):当子进程终止或停止时发送。
    • SIGCONT (18):用于继续一个暂停的进程。
    • SIGSTOP (19):用于暂停一个进程。
    • SIGTSTP (20):当用户按下 Ctrl+Z 时发送,用于暂停进程。
    • SIGTTIN (21):当后台进程尝试从其控制终端读取时发送。
    • SIGTTOU (22):当后台进程尝试向其控制终端写入时发送。
    • SIGURG (23):当套接字上有紧急数据时发送。
    • SIGXCPU (24):当进程使用了超过其允许的 CPU 时间时发送。
    • SIGXFSZ (25):当进程试图创建超过其文件大小限制的文件时发送。
    • SIGVTALRM (26):当虚拟定时器超时时发送。
    • SIGPROF (27):当进程使用了超过其允许的 CPU 时间时发送,与 SIGXCPU 类似,但更严格。
    • SIGWINCH (28):当终端窗口大小改变时发送。
    • SIGIO (29):当文件描述符上有 I/O 操作可以执行时发送。
    • SIGPOLL (29):与 SIGIO 相同,是早期 Unix 系统中的信号名。
    • SIGLOST (30):当文件锁丢失时发送。
    • SIGPWR (31):当电源状态改变时发送。

3. 最后一个线程取消请求退出响应

  • 在多线程程序中,当最后一个线程接收到取消请求(通常是 SIGTERMSIGINT 信号)时,整个进程会退出。
  • 为了确保所有线程都能够正确地处理取消信号,以便进程可以干净地退出,通常需要实现信号处理函数来响应这些信号,并确保所有线程都能够接收到并响应这些信号。

总结:

  • abort() 函数会引发 SIGABRT 信号,并立即终止进程。
  • kill() 函数用于向指定进程发送信号,以终止进程。
  • 在多线程程序中,当最后一个线程接收到取消请求信号时,整个进程会退出。

return 和 exit 区别

  • return 是关键字,exit是函数
  • return是语言级别的,exit 是系统级别的
  • return 返回表示堆栈的释放,exit返回表示一个进程的结束
  • return 返回是把控制权交给调用函数,exit返回是把控制权交给系统

_exit 和 exit 区别

  • _exit 退出时不会进行缓冲区刷新、关闭文件描述符或释放内存空间等清理工作。
  • exit 会执行清理工作。

进程清理函数

使用 int atexit(void (*function)(void)); 函数注册进程退出时要调用的函数。

该函数将在进程终止时(例如,当进程正常退出或被信号终止时)被调用。这个函数可以用来执行一些清理工作,比如关闭文件描述符、释放内存、关闭网络连接等。

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

void cleanup() 
{
    printf("Cleaning up resources...\n");
    // 在这里执行清理工作,比如关闭文件描述符、释放内存等
}

int main() 
{
    // 注册cleanup函数,以便在进程结束时执行
    atexit(cleanup);

    printf("Starting the program...\n");
    // 程序的主要逻辑

    printf("Program finished successfully.\n");
    return 0;
}

进程等待

wait函数

  • 原型

    pid_t wait(int *status);
    
  • 功能wait函数会暂停调用它的进程,直到它的一个子进程退出。调用进程会阻塞,直到至少有一个子进程结束。

  • 参数

    • status:一个指针,指向一个整数,用于接收子进程退出时的状态信息。
      • WIFEXITED(status):如果子进程正常退出,则返回非零值。
      • WEXITSTATUS(status):如果WIFEXITED(status)返回非零,则返回子进程的退出状态码。
      • WIFSIGNALED(status):如果子进程被信号终止,则返回非零值。
      • WTERMSIG(status):如果WIFSIGNALED(status)返回非零,则返回导致子进程退出的信号编号。
  • 返回值

    • 如果至少有一个子进程结束,wait函数返回结束的子进程的进程ID(PID)。
    • 如果没有任何子进程结束,wait函数会返回-1,并设置errnoECHILD

waitpid函数

  • 原型

    pid_t waitpid(pid_t pid, int *status, int options);
    
  • 功能waitpid函数与wait函数类似,但它允许指定等待哪个子进程。waitpid可以等待指定的子进程,也可以等待任意的子进程。

  • 参数

    • pid:要等待的子进程的PID。

      • 如果pid小于1,等待指定绝对值是同一组的任意子进程;
      • 如果pid为-1,则等待任意子进程;(与wait()等价)
      • 如果pid为0,则等待调用fork时产生的所有子进程;
      • 如果pid大于0,则只等待指定的子进程;
    • status:一个指针,指向一个整数,用于接收子进程退出时的状态信息。

      • WIFEXITED(status):如果子进程正常退出,则返回非零值。
      • WEXITSTATUS(status):如果WIFEXITED(status)返回非零,则返回子进程的退出状态码。
      • WIFSIGNALED(status):如果子进程被信号终止,则返回非零值。
      • WTERMSIG(status):如果WIFSIGNALED(status)返回非零,则返回导致子进程退出的信号编号。
    • options:一个整数,包含了等待子进程时的选项。

      ​ 常见的选项包括:通常为0 .

      • WNOHANG:如果没有任何子进程结束,waitpid函数不会阻塞,而是立即返回-1,并设置errnoECHILD
      • WUNTRACED:如果调用waitpid的进程暂停,等待它的子进程进入停止状态(如收到SIGSTOP信号),则返回。
      • WCONTINUED:如果调用waitpid的进程暂停,等待它的子进程从停止状态恢复,则返回。
  • 返回值

    • 如果至少有一个子进程结束,waitpid函数返回结束的子进程的PID。
    • 如果没有任何子进程结束,waitpid函数返回-1,并设置errnoECHILD

僵尸进程

父进程创建子进程后,如果子进程运行结束而父进程没有回收子进程的资源,那么这个子进程就会变成僵尸进程。

特点

  • 僵尸进程无法被杀死,因为它们已经完成了执行,仅等待父进程读取其退出状态。
  • 僵尸进程会持续占用PCB(Process Control Block)资源。

危害

  • 状态维护:僵尸进程的退出状态必须被维护,以便父进程了解任务执行结果。
  • 资源占用:由于PCB需要占用内存,大量僵尸进程会导致内存资源的浪费。
  • 内存泄漏:长时间不回收的僵尸进程可能导致内存泄漏。

孤儿进程

当父进程结束而其子进程仍在运行时,这些子进程就变成了孤儿进程。

特点

  • 孤儿进程会被系统进程init(在Linux中是pid为1的进程)领养。
  • init进程会回收孤儿进程,释放它们占用的资源。

守护进程

守护进程是一种特殊的孤儿进程,通常在系统启动时启动,并在后台运行,执行系统任务。

守护进程通常用于执行系统级别的任务,如网络服务、日志记录和系统监控等。

创建守护进程的步骤

  1. 创建孤儿进程

    • 子进程继承父进程的属性,但随后会脱离这些属性。
  2. 创建新的会话

    1. 原进程受原控制终端影响

      • 原进程与控制终端关联,能够接收来自终端的输入和信号。
    2. 原进程受原进程组的影响

      • 原进程属于一个进程组,与其他同组进程共享某些属性,如信号处理。
    3. 原进程受原会话的影响

      • 原进程属于一个会话,会话中的进程可能共享控制终端,并可能互相影响。
    • 使用setsid()创建新会话,使子进程成为新会话的首进程,并脱离原控制终端、原进程组和原会话。
    • setsid()函数调用后,子进程:
      1. 成为新会话的首进程。
      2. 成为新进程组的组长。
      3. 不再关联控制终端。
  3. 关闭文件描述符

    • 关闭所有从父进程继承来的不再需要的文件描述符。
  4. 改变当前目录

    • 将当前工作目录更改为根目录(/),防止占用可卸载的文件系统。
  5. 重置文件权限掩码

    • 设置文件权限掩码,确保守护进程创建文件时不会保留不必要的权限。
  6. 忽略信号

    • 忽略或捕获信号,防止守护进程因接收到信号而终止。

daemon 函数

int daemon(int nochdir, int noclose);

描述

daemon 函数用于将当前进程转换为守护进程(daemon process)。守护进程是一种在后台运行的进程,通常没有控制终端,并且与登录会话无关。

参数

  • nochdir:

    • 如果为 0,则 daemon 会将当前工作目录更改为根目录(/)。
    • 如果为非 0,则保持当前工作目录不变。
  • noclose:

    • 如果为 0,则 daemon 会关闭所有打开的文件描述符。
    • 如果为非 0,则保留所有文件描述符。

返回值

  • 成功时返回 0。
  • 失败时返回 -1,并设置 errno 来指示错误。

注意事项

  • 调用 daemon 时,进程可能已经是守护进程,或者具有守护进程的属性。这种情况下,daemon 函数通常不会有任何效果。
  • 如果 nochdir 为 0,守护进程将失去对当前工作目录的引用,通常会切换到根目录。
  • 如果 noclose 为 0,守护进程将关闭标准输入(stdin)、标准输出(stdout)和标准错误(stderr)的文件描述符,从而避免守护进程与终端设备相关联。
  • 在调用 daemon 之前,应确保所有必要的文件描述符都已正确打开,并且守护进程不会依赖于这些文件描述符。
  • 守护进程通常由 init 进程(进程号为 1)或另一个守护进程作为父进程。

相关系统调用

  • pid_t getpgid(pid_t pid);:获取指定进程的进程组ID。
  • pid_t getpgrp(void);:获取当前进程的进程组ID。
  • int setpgid(pid_t pid, pid_t pgid);:设置指定进程的进程组ID。
  • int setpgrp(void);:等价于setpgid(0, 0);,设置当前进程为进程组的组长。
  • pid_t getsid(pid_t pid);:获取指定进程的会话ID。
  • pid_t setsid(void);:创建一个新会话,并将当前进程设为新会话的首进程。

获取系统资源

int getrlimit(int resource, struct rlimit *rlim);
int setrlimit(int resource, const struct rlimit *rlim);

int main() {
    struct rlimit rlim;
    int result;

    // 获取当前进程的打开文件描述符的最大数量限制
    result = getrlimit(RLIMIT_NOFILE, &rlim);
    if (result == -1) {
        perror("getrlimit failed");
        return 1;
    }

    printf("Current limits:\n");
    printf("Soft limit = %llu\n", (unsigned long long)rlim.rlim_cur);
    printf("Hard limit = %llu\n", (unsigned long long)rlim.rlim_max);

    // 尝试将软限制设置为硬限制
    rlim.rlim_cur = rlim.rlim_max;

    // 设置新的资源限制
    result = setrlimit(RLIMIT_NOFILE, &rlim);
    if (result == -1) {
        perror("setrlimit failed");
        return 1;
    }

    // 再次获取资源限制以确认更改
    result = getrlimit(RLIMIT_NOFILE, &rlim);
    if (result == -1) {
        perror("getrlimit failed");
        return 1;
    }

    printf("New limits:\n");
    printf("Soft limit = %llu\n", (unsigned long long)rlim.rlim_cur);
    printf("Hard limit = %llu\n", (unsigned long long)rlim.rlim_max);

    return 0;
}

第一个参数:表示获取资源

  • RLIMIT_NOFILE:进程可以打开的最大文件描述符数量。
  • RLIMIT_STACK:进程堆栈的最大大小。 => 12M
  • RLIMIT_AS: 进程的虚拟内存大小限制(地址空间限制)。
  • RLIMIT_CORE: 核心文件的最大大小,如果设置为0则禁止创建核心文件。
  • RLIMIT_CPU: 进程可以使用的CPU时间总量,以秒为单位。
  • RLIMIT_DATA: 进程数据段的最大大小(包括初始化和未初始化的数据)。
  • RLIMIT_FSIZE: 进程可以创建的文件的最大大小。
  • RLIMIT_LOCKS: 进程可以设置的文件锁数量。
  • RLIMIT_MEMLOCK: 进程可以锁定到内存中的最大字节数。
  • RLIMIT_MSGQUEUE: 进程可以使用的POSIX消息队列的最大字节数。
  • RLIMIT_NICE: 进程可以通过 setpriority 提升到最高优先级的限制(仅限Linux)。
  • RLIMIT_NPROC: 进程可以拥有的最大子进程数量(仅限Linux)。
  • RLIMIT_RSS: 进程的常驻集合大小(物理内存使用量)的最大限制。
  • RLIMIT_RTPRIO: 进程可以通过 sched_setschedulersched_setparam 设置实时优先级的最高值(仅限Linux)。
  • RLIMIT_SIGPENDING: 进程可以挂起的信号的最大数量。

第二个参数:表示资源结构体

struct rlimit {
    rlim_t rlim_cur;  /* Soft limit */
    rlim_t rlim_max;  /* Hard limit (ceiling for rlim_cur) */
};
  • rlim_cur: 软限制,是内核强制执行的当前限制值。进程可以将其软限制增加到硬限制的值。
  • rlim_max: 硬限制,是软限制可以设置的最大值。非特权进程不能增加硬限制。

注意:

  • 软资源限额不能大于硬资源限额

返回值:

  • 成功:0
  • 失败:-1

进程环境

基本概念

  • 环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数
  • 如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
  • 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性
extern char **environ;

获取环境变量

char *getenv(const char *name);

char *hostname = getenv("HOSTNAME");

例如,获取 HOSTNAME 环境变量。

修改环境变量 或者 添加环境变量

int setenv(const char *name, const char *value, int overwrite);

	// 设置一个新的环境变量
    setenv("MY_NEW_ENV", "Hello, World!", 1);

    // 或者更新一个已存在的环境变量
    setenv("HOSTNAME", "my-new-host", 1);

    // 注意:setenv 函数可能不会立即生效,它可能会在进程下一次调用 getenv 时才更新环境变量
    // 如果你想立即更新环境变量,可以调用 exec 系列的函数来替换当前进程映像
  • 第一个参数:表示变量名
  • 第二个参数:表示变量值
  • 第三个参数:是否覆盖已存在的环境变量。如果 overwrite0,则只有当环境变量不存在时才会设置;如果 overwrite1,则即使环境变量已存在也会覆盖其值。
int putenv(char *string);

	char *env_string = "MY_NEW_ENV=Hello, World!";
    putenv(env_string);
  • string format : name=value

删除环境变量

int unsetenv(const char *name);

	// 从环境表中删除一个环境变量
    unsetenv("MY_ENV");

删除所有的环境变量

int clearenv(void);

	// 清除环境表中的所有环境变量
    clearenv();

exec函数家族

创建新的进程替换当前的进程,而唯一保留下来的就是当前进程的进程号

execl

int execl(const char *path, const char *arg0, ... /*, (char *)0 */);

execl("/bin/ls", "ls", "-l", (char *)0);
  • path: 要执行的程序的完整路径。
  • arg0: 程序的命令名,通常与 path 中的文件名相同。
  • ...: 可变数量的参数,每个参数都是命令行参数的一部分,以字符串形式传递。
  • (char *)0: 一个空指针,用来标记参数列表的结束。

execv

int execv(const char *path, char *const argv[]);

char *const argv[] = {
        "ls",      // 命令名
        "-l",      // 选项
        NULL       // 参数列表结束标志
    };

execv("/bin/ls", argv);
  • path: 要执行的程序的完整路径。
  • argv: 一个字符串数组,其中包含命令名和参数,数组的最后一个元素必须是 NULL

execle

int execle(const char *path, const char *arg0, ... , (char *)0, char *const envp[]);

char *const envp[] = {
        "MY_CUSTOM_ENV=HelloWorld", // 自定义环境变量
        NULL                         // 环境变量列表结束标志
    };

execle("/bin/ls", "ls", "-l", (char *)0, envp);
  • path: 要执行的程序的完整路径。
  • arg0: 程序的命令名。
  • ...: 命令行参数列表,以空指针结束。
  • envp: 一个字符串数组,其中包含环境变量字符串,数组的最后一个元素必须是 NULL

execve

int execve(const char *path, char *const argv[], char *const envp[]);

char *const argv[] = {
        "ls",      // 命令名
        "-l",      // 选项
        NULL       // 参数列表结束标志
    };

    // 定义环境变量数组
    char *const envp[] = {
        "HOME=/home/user",  // 环境变量
        "PATH=/bin:/usr/bin", // 环境变量
        NULL                // 环境变量列表结束标志
    };

execve("/bin/ls", argv, envp);
  • path: 要执行的程序的完整路径。
  • argv: 命令行参数数组。
  • envp: 环境变量数组。

execlp

int execlp(const char *file, const char *arg0, ... /*, (char *)0 */);

execlp("ls", "ls", "-l", (char *)0);
  • file: 要执行的程序的文件名,不需要完整路径,系统会从 PATH 环境变量指定的目录中搜索这个文件。
  • arg0: 程序的命令名。
  • ...: 命令行参数列表,以空指针结束。

execvp

int execvp(const char *file, char *const argv[]);

char *const argv[] = {
        "ls",      // 命令名
        "-l",      // 选项
        NULL       // 参数列表结束标志
    };

    // 使用 execvp 执行 ls -l 命令
    // 注意:execvp 会替换当前进程的映像,所以下面的 printf 不会执行
execvp("ls", argv);
  • file: 要执行的程序的文件名,不需要完整路径。
  • argv: 命令行参数数组。
  • 26
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值