目录
什么是进程
进程(程序)是存储在存储介质(硬盘)上的一段代码,它在运行时被加载到内存空间(RAM)中。CPU会为进程分配一块地址空间,这块地址空间是虚拟的,并且可能与其他进程共享(mmap)。操作系统会将文件的物理扇区映射到进程的虚拟地址空间,而内存管理单元(MMU)负责处理虚拟地址到物理地址的转换。进程通过操作系统提供的API(应用程序编程接口)来执行任务。进程的执行是通过CPU的寄存器来控制的。
时钟中断和上下文切换
CPU为了获得控制权会启动中断时钟,每隔一段时间产生中断,让控制权重新回到CPU执行中断程序
当进程开始执行时,操作系统会将进程的寄存器上下文(包括程序计数器、栈指针等)加载到CPU的寄存器中。进程通过这些寄存器来访问内存和执行指令。
当进程从用户模式切换到内核模式时(例如,由于系统调用、中断或异常),CPU会自动将一些关键的寄存器状态保存到当前进程的内核栈^[1]中(硬件自动保存)。
操作系统会更新内核寄存器到进程的进程结构的内存(PCB^[2])中(软件显式保存),样做的目的是,当进程再次被调度执行时,它可以恢复到被中断前的状态。同时,CPU的寄存器会被清空,以便为下一个进程的执行做准备。
总结一下就是;时钟中断:硬件自动保存用户模式下的寄存器状态到内核栈。操作系统决定切换:操作系统软件显式保存内核模式下的寄存器状态到进程的PCB或其他进程结构的内存中。
补充注释:
[1]内核栈:直接保存寄存器状态,是物理内存中的一个实际区域。进程都拥有自己的内核栈(Kernel Stack),这是进程在执行内核模式代码时使用的栈。内核栈与用户栈(User Stack)是分开的
[2]PCB:存储进程的更广泛的状态信息,是操作系统管理进程的关键数据结构,通常存储在系统的核心物理内存区域,这个区域是受操作系统保护的,普通用户进程无法直接访问。
程序入口
main 函数是程序执行的起始点。当程序启动时,操作系统会调用 main 函数开始执行程序。
进程的终止
正常终止:main函数返回,调用exit,调用_exit或_Exit,最后一个线程从其他期待例程返回,最后一个线程调用pthread_exit
异常终止:调用abort,接到一个信号并终止,最后一个线程对其取消请求做出相应
钩子函数: 在exit退出后,以注册的逆序方式运行(同defer的FILO)
int atexit(void (*function)(void));
命令行参数分析
1.getopt
int getopt(int argc, char * const argv[], const char *optstring);
接受命令行参数-后面的字母,optstring指定了程序可以接受的选项字符以及它们是否需要参数。选项字符前可以有一个冒号(:)来表示需要参数,参数用指针optarg调用,如果未提供getopt将返回一个错误。getopt返回下一个解析到的选项字符,如果所有选项都被解析完毕,它会返回 -1。
2.getopt_long
int getopt_long(int argc, char *const *argv, const char *options, const struct option *long_options, int *opt_index);
struct option {
const char *name; // 长选项的名称
int has_arg; // 选项是否有参数
int *flag; // 存储选项值的位置或为 NULL
int val; // 选项的值或标志
};
成功时,返回当前处理的选项的值(可以是字符或 val 字段的值)。当没有更多的选项时,返回 -1。当遇到非法选项时,返回 ':'。
参数说明
-argc: 命令行参数的个数。
-argv: 命令行参数的数组,第一个元素是程序名称。
-options: 一个字符串,描述了短选项的格式。每个字符代表一个短选项,后跟一个冒号表示该选项需要一个参数(例如 "a:b::" 中 a 需要一个参数,b 可以有一个参数,c 没有参数)。
-long_options: 一个结构体数组,描述了长选项。每个元素包含长选项的名称、是否有参数、值的存储位置和选项的值。
-opt_index: 指向整数的指针,返回当前处理的长选项在 long_options 数组中的索引。
环境变量和函数跳转
1.getenv
char *getenv(const char *name);
通过name获取环境变量
2.setenv
int setenv(const char *name, const char *value, int overwrite);
overwrite如果为非零值,则表示如果环境变量已经存在,其值将被新的值覆盖;如果为零,则表示如果环境变量已存在,则函数不会改变其值。
3.setjump
int setjmp(jmp_buf env);
首次调用 setjmp 时,它返回 0。当 longjmp 被调用并且跳转回 setjmp 时,setjmp 返回由 longjmp 传递的非零值。
4.longjump
void longjmp(jmp_buf env, int val);
传递给 setjmp 的值,当 longjmp 调用时。如果这个值为 0,setjmp 将返回 0;否则,它将返回这个非零值。
5.sigsetjmp
int sigsetjmp(sigjmp_buf env, int savesigs);
sigsetjmp 函数与 setjmp 类似,但它还允许你指定一个信号集,用于在恢复状态时忽略这些信号.savesigs 是一个布尔值,如果为非零值,则在恢复状态时会忽略指定的信号集
6.siglongjmp
void siglongjmp(sigjmp_buf env, int val);
同longjmp
进程相关函数
进程资源的获取和控制
常见的资源限制类型包括:
RLIMIT_CORE:核心文件的最大大小。
RLIMIT_CPU:CPU 时间限制。
RLIMIT_DATA:数据段的最大大小。
RLIMIT_FSIZE:文件的最大大小。
RLIMIT_NOFILE:进程可打开的文件描述符的最大数量。
RLIMIT_STACK:栈的最大大小
1.setrlimit
int setrlimit(int resource, const struct rlimit *rlim);
resource:指定要限制的资源类型,rlim:指向 rlimit 结构的指针,该结构包含资源限制的当前值和最大值。成功时返回 0。失败时返回 -1,并设置 errno 以指示错误类型。
2.getrlimit
int getrlimit(int resource, struct rlimit *rlim);
struct rlimit {
rlim_t rlim_cur; // 当前限制值
rlim_t rlim_max; // 最大限制值
};
查询的资源类型
进程标识符
进程标识符pid,类型是pid_t(一般情况下是16位无符号整形),pid进程号是循序向下使用,命令ps可以查看,init进程(pid = 1)是所有进程的祖先。fork的父子进程是写实拷贝技术,互不干涉。
1.getpid
pid_t getpid(void);
获取当前进程pid
2.getppid
pid_t getppid(void);
获取当前进程的父进程pid
3.fork
pid_t fork(void);
复制当前进程产生子进程,父进程返回子进程pid,子进程返回0,未决信号和文件锁不继承,资源利用量清0
进程的销亡和释放资源
1.wait
pid_t wait(int *wstatus);
堵塞等待,wstatus是一个整数指针,用于存储子进程退出状态的信息,可以给NULL。成功返回被等待的子进程的进程PID,失败返回 -1
2.waitpid
pid_t waitpid(pid_t pid, int *wstatus, int options);
指定要等待的子进程的PID,-1表示等待任何子进程。0表示等待与调用进程具有相同组 ID 的任何子进程。options是指定等待操作的行为,可以设置非堵塞。
进程会计
int acct(const char *filename);
struct acct {
char ac_flag; /* Accounting flags */
u_int16_t ac_uid; /* Accounting user ID */
u_int16_t ac_gid; /* Accounting group ID */
u_int16_t ac_tty; /* Controlling terminal */
u_int32_t ac_btime; /* Process creation time
(seconds since the Epoch) */
comp_t ac_utime; /* User CPU time */
comp_t ac_stime; /* System CPU time */
comp_t ac_etime; /* Elapsed time */
comp_t ac_mem; /* Average memory usage (kB) */
comp_t ac_io; /* Characters transferred (unused) */
comp_t ac_rw; /* Blocks read or written (unused) */
comp_t ac_minflt; /* Minor page faults */
comp_t ac_majflt; /* Major page faults */
comp_t ac_swaps; /* Number of swaps (unused) */
u_int32_t ac_exitcode; /* Process termination status
(see wait(2)) */
char ac_comm[ACCT_COMM+1];
/* Command name (basename of last
executed command; null-terminated) */
char ac_pad[X]; /* padding bytes */
};
enum { /* Bits that may be set in ac_flag field */
AFORK = 0x01, /* Has executed fork, but no exec */
ASU = 0x02, /* Used superuser privileges */
ACORE = 0x08, /* Dumped core */
AXSIG = 0x10 /* Killed by a signal */
};
跟踪系统活动和资源使用的系统调用,写入filename。它主要用于记录用户和系统进程的资源使用情况,如 CPU 时间、内存使用、I/O 操作等。acct 函数通常需要超级用户权限才能使用。账户记录文件通常由系统管理员管理,普通用户不应随意修改。
进程时间
clock_t times(struct tms *buf);
struct tms {
clock_t tms_utime; /* user time */
clock_t tms_stime; /* system time */
clock_t tms_cutime; /* user time of children */
clock_t tms_cstime; /* system time of children */
};
获取进程的时间相关信息
守护进程
守护进程(Daemon)是一种在后台运行的进程,通常用于提供系统服务,守护进程必须是后台会话(session)的领导者,拥有相同的pid,gid,sid,并且TTY(控制终端)为"?"。
独立唯一性:守护进程不依赖于任何终端,它们通常在系统启动时自动启动,或通过系统服务管理器(如 systemd 或 init)启动。守护进程唯一不可重复。
持续运行:守护进程通常持续运行,直到被显式地停止或系统关闭。
低优先级:守护进程通常被赋予较低的优先级,以确保它们不会占用过多的系统资源。
日志记录:守护进程将它们的状态和活动记录到日志文件中,而不是标准输出或标准错误。
1.setsid
pid_t setsid(void);
setsid 创建一个新的会话^[1],并使调用进程成为该会话的领导者。调用进程同时成为新的进程组的领导者,并成为没有控制终端的进程组的领导者。只有当进程不是进程组的领导者时,setsid 才能成功调用成功返回新的会话ID也就是调用进程的PID。失败返回-1。
2.setpgid
int setpgid(pid_t pid, pid_t pgid);
设置进程pid和gid
3.getpgid
pid_t getpgid(pid_t pid);
获取进程
补充注释:
[1]:在操作系统内核中,会话(Session)是一个进程集合,它通常由一个或多个进程组成,并且具有一些共同的属性和行为
exec函数族
exec函数族用于执行新程序的系统调用。这些函数允许一个正在运行的程序替换其执行的代码和数据,从而开始运行一个全新的程序。exec函数首先查找并加载,新的程序一旦新程序被加载,exec函数会替换当前进程的内存映像(pid不变),这意味着新程序的代码和数据会覆盖当前进程的内存空间,exec 函数会初始化进程的 CPU 寄存器,除了由 exec 函数显式保留的文件描述符外,当前进程的所有文件描述符会被关闭,最后操作系统会更新进程的状态,使其开始执行新程序的代码。函数不会返回,除非发生错误(-1)。注意fflush刷新缓冲区,避免重复输入输出。
1.execl
int execl(const char *pathname, const char *arg, ... /* (char *) NULL */);
只传递参数列表,不传递环境,以NULL空指针作为传递参数结尾。
2.execlp
int execlp(const char *file, const char *arg, ... /* (char *) NULL */);
传递参数列表,并且系统的使用PATH环境变量查找*file程序。
3.execle
int execle(const char *pathname, const char *arg, ... /*, (char *) NULL, char *const envp[] */);
传递参数列表和环境。
4.execv
int execv(const char *pathname, char *const argv[]);
传递参数数组。
5.execvp
int execvp(const char *file, char *const argv[]);
传递参数数组,使用 PATH 环境变量查找程序。
用户权限及权限组
在 Linux 系统中,每个进程都有几个与用户身份相关的 ID,这些 ID 决定了进程对系统资源的访问权限。以下是 UID 的三个主要类型:实际用户 ID(real UID,简称 rUID)、有效用户 ID(effective UID,简称 eUID)和保存的用户 ID(saved set-user-ID,简称 sUID)。
实际用户 ID(rUID)
定义:实际用户 ID 是用户登录时的 UID。它是用户身份的初始值,通常在用户登录时由系统设置。
用途:rUID 用于确定用户登录时的权限。当用户执行程序时,程序的 rUID 将被设置为用户的 rUID。
有效用户 ID(eUID)
定义:有效用户 ID 是进程在执行时用于检查文件访问权限的 UID。它决定了进程可以访问哪些文件和资源。
用途:eUID 通常用于限制进程的权限。通过将 eUID 设置为较低权限的用户,可以防止进程访问敏感资源。例如,Web 服务器进程通常会将 eUID 设置为非 root 用户,以减少被攻击的风险。
保存的用户 ID(sUID)
定义:保存的用户 ID 是在执行程序时由系统保存的 eUID 的副本。它在进程执行时被设置,并在进程执行结束后恢复。
用途:sUID 用于在程序执行期间暂时提升权限。当程序需要访问敏感资源时,可以暂时将 eUID 设置为 sUID,以便获得必要的权限。程序执行完成后,eUID 会恢复为 sUID 的值,从而降低权限。
1.getuid&getgid
uid_t getuid(void);
gid_t getgid(void);
获取调用进程的实际用户 ID 和组 ID。返回值:返回实际用户 ID 或组 ID。
2.geteuid&getegid
uid_t geteuid(void);
gid_t getegid(void);
获取调用进程的有效用户 ID 和组 ID。返回值:返回有效用户 ID 或组 ID。
3.setuid&setgid
int setuid(uid_t uid);
int setgid(gid_t gid);
设置调用进程的实际用户 ID 和组 ID。返回值:成功时返回 0,失败时返回 -1,并设置 errno。权限:通常只有超级用户(root)可以设置其他用户的 UID 或 GID。对于非特权进程,只能将 UID 或 GID 设置为实际用户 ID、有效用户 ID 或保存的设置用户 ID。
4.setreuid&setregid
int setreuid(uid_t ruid, uid_t euid);
int setregid(gid_t rgid, gid_t egid);
设置调用进程的实际用户 ID 和有效用户 ID,以及实际组 ID 和有效组 ID。返回值:成功时返回 0,失败时返回 -1,并设置 errno。特殊行为:如果传递 -1 作为参数,则相应的 ID 不会被更改。
5.seteuid&setegid
int seteuid(uid_t euid);
int setegid(gid_t egid);
仅设置调用进程的有效用户 ID 和组 ID。返回值:成功时返回 0,失败时返回 -1,并设置 errno。
6.setfsuid&setfsgid
int setfsuid(uid_t uid);
int setfsgid(gid_t gid);
设置文件系统用户 ID 和组 ID,这些 ID 用于文件系统相关操作。返回值:成功时返回 0,失败时返回 -1,并设置 errno。
系统日志(/var/log)
syslogd 是 Linux 和类 Unix 系统中用于日志记录的守护进程服务。它负责收集系统和应用程序的日志信息,并将这些信息记录到文件或发送到其他日志服务器。只有syslog才有权限些日志,其他要请求sysylogd服务来写日志。
1.openlog
void openlog(const char *ident, int option, int facility);
与syslogd进行关联,ident是人物,随便给个字段就行,option是行为,看man手册宏
2.syslog
void syslog(int priority, const char *format, ...);
向syslogd提交日志内容,priority是级别宏值,format是内容
3.closelog
void closelog(void);
关闭与syslogd关联
守护进程的代码实现
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#define FILENAME "~/桌面/linux sys/test.txt"
static FILE *fp;
static void protect_process(void) {
pid_t pid;
int fd;
pid = fork(); // 创建子进程
if (pid < 0) {
perror("fork()"); // 显示错误信息
exit(1); // 退出父进程
}
if(pid > 0) {
exit(0); // 父进程退出
}
if (pid == 0) {
// 子进程继续执行
fd = open("/dev/null", O_RDWR); // 打开 /dev/null
if (fd < 0) {
perror("open()"); // 显示错误信息
exit(1); // 退出子进程
}
}
// 将标准输入、标准输出和标准错误重定向到 /dev/null
dup2(fd, 0); // 重定向标准输入
dup2(fd, 1); // 重定向标准输出
dup2(fd, 2); // 重定向标准错误
if (fd > 2) // 如果文件描述符 fd 大于 2,则关闭它
close(fd);
// 创建新的会话,并使当前进程成为会话领导者、进程组领导者和没有控制终端的进程组的领导者
setsid();
// 更改工作目录为根目录
chdir("/");
// 设置文件权限掩码为 0,即不屏蔽任何权限
umask(0);
}
static void process_exit(int s) {
closelog();//关闭日志
fclose(fp); // 关闭文件
}
// 守护进程向 test.txt 文件写入数字
int main() {
openlog("p_p", LOG_PID, LOG_DAEMON);//打开日志
struct sigaction sa;
//退出清理
sa.sa_handler = process_exit;
sigemptyset(&sa.sa_mask);
sigaddset(&sa.sa_mask, SIGINT);
sigaddset(&sa.sa_mask, SIGQUIT);
sigaddset(&sa.sa_mask, SIGTERM);
sa.sa_flags = 0;
sigaction(SIGINT, &sa, NULL);
sigaction(SIGQUIT, &sa, NULL);
sigaction(SIGTERM, &sa, NULL);
//signal的方法同一handler可能导致重复调用handler产生问题,用sigaction屏蔽其他信号避免了这种问题
// signal(SIGQUIT, process_exit);
// signal(SIGQUIT, process_exit);
// signal(SIGIERM, process_exit);
protect_process(); // 保护进程
syslog(LOG_INFO, "protect_process success");
// 使用 fopen 打开文件,注意这里应该使用文件名的字符串形式
fp = fopen(FILENAME, "w");
if (fp == NULL) {
perror("fopen()"); // 显示错误信息
}
for(int i = 0; ; i++) {
fprintf(fp, "%d\n", i); // 写入数字和换行符
fflush(fp); // 刷新缓冲区,确保数据写入文件
sleep(1); // 休眠 1 秒
}
exit(0); // 正常退出
}