进程
定义
进程是一段可执行代码,并放在内存中运行。
进程号
进程号是计算机识别进程的唯一标识。可以使用 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. 最后一个线程取消请求退出响应
- 在多线程程序中,当最后一个线程接收到取消请求(通常是
SIGTERM
或SIGINT
信号)时,整个进程会退出。 - 为了确保所有线程都能够正确地处理取消信号,以便进程可以干净地退出,通常需要实现信号处理函数来响应这些信号,并确保所有线程都能够接收到并响应这些信号。
总结:
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,并设置errno
为ECHILD
。
- 如果至少有一个子进程结束,
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,并设置errno
为ECHILD
。WUNTRACED
:如果调用waitpid
的进程暂停,等待它的子进程进入停止状态(如收到SIGSTOP信号),则返回。WCONTINUED
:如果调用waitpid
的进程暂停,等待它的子进程从停止状态恢复,则返回。
-
-
返回值:
- 如果至少有一个子进程结束,
waitpid
函数返回结束的子进程的PID。 - 如果没有任何子进程结束,
waitpid
函数返回-1,并设置errno
为ECHILD
。
- 如果至少有一个子进程结束,
僵尸进程
父进程创建子进程后,如果子进程运行结束而父进程没有回收子进程的资源,那么这个子进程就会变成僵尸进程。
特点
- 僵尸进程无法被杀死,因为它们已经完成了执行,仅等待父进程读取其退出状态。
- 僵尸进程会持续占用PCB(Process Control Block)资源。
危害
- 状态维护:僵尸进程的退出状态必须被维护,以便父进程了解任务执行结果。
- 资源占用:由于PCB需要占用内存,大量僵尸进程会导致内存资源的浪费。
- 内存泄漏:长时间不回收的僵尸进程可能导致内存泄漏。
孤儿进程
当父进程结束而其子进程仍在运行时,这些子进程就变成了孤儿进程。
特点
- 孤儿进程会被系统进程
init
(在Linux中是pid
为1的进程)领养。 init
进程会回收孤儿进程,释放它们占用的资源。
守护进程
守护进程是一种特殊的孤儿进程,通常在系统启动时启动,并在后台运行,执行系统任务。
守护进程通常用于执行系统级别的任务,如网络服务、日志记录和系统监控等。
创建守护进程的步骤
-
创建孤儿进程
- 子进程继承父进程的属性,但随后会脱离这些属性。
-
创建新的会话
-
原进程受原控制终端影响
- 原进程与控制终端关联,能够接收来自终端的输入和信号。
-
原进程受原进程组的影响
- 原进程属于一个进程组,与其他同组进程共享某些属性,如信号处理。
-
原进程受原会话的影响
- 原进程属于一个会话,会话中的进程可能共享控制终端,并可能互相影响。
- 使用
setsid()
创建新会话,使子进程成为新会话的首进程,并脱离原控制终端、原进程组和原会话。 setsid()
函数调用后,子进程:- 成为新会话的首进程。
- 成为新进程组的组长。
- 不再关联控制终端。
-
-
关闭文件描述符
- 关闭所有从父进程继承来的不再需要的文件描述符。
-
改变当前目录
- 将当前工作目录更改为根目录(
/
),防止占用可卸载的文件系统。
- 将当前工作目录更改为根目录(
-
重置文件权限掩码
- 设置文件权限掩码,确保守护进程创建文件时不会保留不必要的权限。
-
忽略信号
- 忽略或捕获信号,防止守护进程因接收到信号而终止。
daemon 函数
int daemon(int nochdir, int noclose);
描述
daemon
函数用于将当前进程转换为守护进程(daemon process)。守护进程是一种在后台运行的进程,通常没有控制终端,并且与登录会话无关。
参数
-
nochdir
:- 如果为 0,则
daemon
会将当前工作目录更改为根目录(/
)。 - 如果为非 0,则保持当前工作目录不变。
- 如果为 0,则
-
noclose
:- 如果为 0,则
daemon
会关闭所有打开的文件描述符。 - 如果为非 0,则保留所有文件描述符。
- 如果为 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
:进程堆栈的最大大小。 => 12MRLIMIT_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_setscheduler
和sched_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 系列的函数来替换当前进程映像
- 第一个参数:表示变量名
- 第二个参数:表示变量值
- 第三个参数:是否覆盖已存在的环境变量。如果
overwrite
为0
,则只有当环境变量不存在时才会设置;如果overwrite
为1
,则即使环境变量已存在也会覆盖其值。
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
: 命令行参数数组。