【为项目做准备】Linux操作系统day2
这两天学校的事情总压着,day2拖了好几天..day2内容是进程数据结构
- 进程数据结构
- 信号处理
- 任务状态
- 进程调度
- 运行统计信息
- 进程亲缘关系
- 进程权限
- 用户和组标识符(IDs)
- linux capabilities
- 内存管理
- 文件与文件系统
- 用户态与内核态
- 用户态与内核态的转换
- 函数调用栈
- 内核栈和task_struct的关系
进程数据结构
进程or线程,在内核中,统一叫任务(Task),由一个统一的结构task_struct来管理。
怎么管理?
首先,需要一个链表,将所有任务串起来
struct list_head tasks;
接下来,具体看看任务包含哪些东西:
- 任务ID。
作为任务的唯一标识,但是task_struct里面涉及任务ID的,有好几个。
pid_t pid; // pid用于识别单个进程或者线程
pid_t tgid; // tgid用于表示线程组,所有属于同一线程组的线程共享相同的tgid
struct task_struct *group_leader;// 指向线程组的主线程,它的pid和tgid相同。用来代表整个线程组
为什么这么麻烦?因为进程和线程统一为任务了,会带来好几个麻烦。
第一个麻烦,是任务展示。
如果只有一个任务ID,那么用ps展示的时候,会展示所有线程,会非常长且复杂。但是用户可能只想看自己创建的线程
第二个麻烦,是下发指令。
加入用户希望进程结束运行,但是只有线程ID,那么就只能一个个线程去kill,多麻烦。所以他俩虽然都是任务,但是需要加以区分。
多线程的情况下:
- pid 是进程标识符(Process ID),唯一地标识一个进程。对于多线程程序,所有线程共享相同的 pid,即所有线程都属于同一个进程。
- tgid 是线程组标识符(Thread Group ID)。在 Linux 中,所有属于同一个线程组的线程共享相同的 tgid。这个值等于线程组的主线程(通常是创建线程组的那个线程)的 pid。所以,tgid 可以用来标识线程组的所有线程。
- group_leader 指向线程组的主线程(线程组领导者)。在一个多线程程序中,group_leader 的 pid 和 tgid 是相同的。
信号处理
task_struct里面关于信号处理的字段。
- signal 和 sighand:
这两个字段指向signal_struct和sighand_struct结构体,分别用于管理与信号相关的状态信息和信号处理函数。 - blocked,real_blocked,和 saved_sigmask:
这些集合表示当前阻塞的信号、实际阻塞的信号和保存的信号掩码,用于控制哪些信号可以传递给进程。 - pending:
表示当前等待处理的信号。 - sas_ss_sp、sas_ss_size 和 sas_ss_flags 用于管理在接收信号时可能使用的替代信号栈的信息。
任务状态
状态字段:task_struct 中的 state、exit_state 和 flags 字段描述任务的不同状态。
- state:
任务的当前状态,例如运行(TASK_RUNNING)、可中断睡眠(TASK_INTERRUPTIBLE)、不可中断睡眠(TASK_UNINTERRUPTIBLE)等。
- exit_state:
任务的退出状态,例如僵尸(EXIT_ZOMBIE)、死亡(EXIT_DEAD)等。
- flags:
标志位,表示任务的不同属性,例如 PF_EXITING(正在退出)、PF_VCPU(虚拟CPU上运行)等。
进程调度
- 调度相关字段:
包括是否在运行队列上(on_rq)
优先级(prio当前优先级、static_prio基本优先级、normal_prio正常优先级、rt_priority实时优先级)、调度器类(sched_class)等。
- 调度实体:
sched_entity调度实体、sched_rt_entity实时调度实体 和 sched_dl_entity时间调度实体。
policy:调度策略,如SCHED_FIFO、SCHED_RR等。
cpus_allowed 和 nr_cpus_allowed:表示进程可以运行的CPU。
示例代码:
这段代码首先阻塞SIGINT信号,检查该信号是否处于待处理状态,然后恢复原来的信号掩码。这样的操作对于理解如何在实际应用中管理信号非常有帮助。
#include <signal.h>
#include <stdio.h>
int main() {
sigset_t newmask, oldmask, pendmask;
// 设置新的信号掩码,阻塞SIGINT
sigemptyset(&newmask);
sigaddset(&newmask, SIGINT);
if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0) {
perror("Signal block error");
}
// 检查信号是否处于待处理状态
if (sigpending(&pendmask) < 0) {
perror("sigpending error");
}
if (sigismember(&pendmask, SIGINT)) {
printf("SIGINT pending\n");
}
// 恢复旧的信号掩码
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0) {
perror("Signal setmask error");
}
return 0;
}
运行统计信息
u64 utime;//用户态消耗的CPU时间
u64 stime;//内核态消耗的CPU时间
unsigned long nvcsw;//自愿(voluntary)上下文切换计数
unsigned long nivcsw;//非自愿(involuntary)上下文切换计数
u64 start_time;//进程启动时间,不包含睡眠时间
u64 real_start_time;//进程启动时间,包含睡眠时间
示例代码:
getrusage()函数,它是获取当前进程资源使用信息的标准方式。这些信息包括用户态和内核态消耗的CPU时间,以及自愿和非自愿的上下文切换次数。
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/resource.h>
int main() {
struct rusage usage;
// 获取当前进程的资源使用情况
getrusage(RUSAGE_SELF, &usage);
printf("User CPU time used: %ld.%06ld seconds\n", usage.ru_utime.tv_sec, usage.ru_utime.tv_usec);
printf("System CPU time used: %ld.%06ld seconds\n", usage.ru_stime.tv_sec, usage.ru_stime.tv_usec);
printf("Voluntary context switches: %ld\n", usage.ru_nvcsw);
printf("Involuntary context switches: %ld\n", usage.ru_nivcsw);
return 0;
}
进程亲缘关系
任何一个进程都有父进程。所以,整个进程其实就是一棵进程树。而拥有同一父进程的所有进程都具有兄弟关系。
struct task_struct __rcu *real_parent;
struct task_struct __rcu *parent;
struct list_head children;
struct list_head sibling;
- parent指向其父进程。当它终止时,必须向它的父进程发送信号
- children表示链表的头部。链表中的所有元素都是它的子进程。
- sibling用于把当前进程插入到兄弟链表中。
通常情况下,real_parent和parent是一样的,但是也会有另外的情况存在。例如,bash创建一个进程,那进程的parent和real_parent就都是bash。如果在bash上使用GDB来debug一个进程,这个时候GDB是parent,bash是这个进程的real_parent。
示例代码:
getppid()函数来获取并打印当前进程的父进程ID。这对于理解进程之间的关系以及系统如何管理这些关系非常有用。
#include <stdio.h>
#include <unistd.h>
int main() {
pid_t ppid = getppid(); // 获取父进程ID
printf("Parent process ID: %d\n", ppid);
return 0;
}
进程权限
用户和组标识符(IDs)
- 实际和有效用户/组ID(UID/GID)
- uid/gid:启动进程的用户或组的实际ID。
- euid/egid:决定进程权限的有效ID,用于大部分安全检查。
- suid/sgid:在执行setuid或类似操作时保存的用户或组ID。
- fsuid/fsgid:文件系统操作时使用的用户或组ID,主要用于特殊权限检查。
查看当前进程的uid和gid:
#include <unistd.h>
#include <stdio.h>
int main() {
printf("Current UID: %d\n", getuid());
printf("Current GID: %d\n", getgid());
return 0;
}
使用setuid或者setgid提升或降低权限:
setuid程序允许用户以文件所有者的权限运行程序,这在需要临时提升权限以执行特定任务时非常有用。
示例代码:创建一个setuid的可执行文件
# 编译程序
gcc program.c -o program
# 改变所有者到root
sudo chown root:root program
# 设置setuid位
sudo chmod u+s program
系统调用和权限检查:
示例代码:在服务中临时更改权限
#include <sys/types.h>
#include <unistd.h>
void temporary_drop_privileges(uid_t new_uid) {
uid_t original_uid = getuid();
// 临时降低权限
if (setuid(new_uid) != -1) {
// 执行低权限操作
}
// 恢复原始权限
setuid(original_uid);
}
使用setuid()系统调用:
#include <sys/types.h>// 包含用于定义数据类型的系统类型头文件,如uid_t。
#include <unistd.h>// 提供对POSIX操作系统API的访问,如setuid。
#include <stdio.h>// 标准输入输出头文件,提供了输入输出函数如printf和perror。
int main() {
//定义了一个uid_t类型的变量new_uid并初始化为1001。uid_t是用来表示用户ID的数据类型
uid_t new_uid = 1001;
//setuid函数是一个系统调用,用来设置进程的实际用户ID。如果调用成功,函数返回0;如果失败,返回-1
//这句代码是尝试将当前进程的用户ID设置为new_uid的值1001
if (setuid(new_uid) == -1) {
perror("Failed to change user ID");
return 1;
}
printf("User ID changed successfully\n");
return 0;
}
linux capabilities
细粒度权限控制:
Linux的能力(capabilities)将传统的root权限分解为更细粒度的权限集,允许对特定系统操作进行精细控制。
- 重要的能力标识(部分):
- CAP_CHOWN: 改变文件所有者。
- CAP_NET_BIND_SERVICE: 绑定到系统端口(1024以下)。
- CAP_SYS_BOOT: 允许重启系统。
- 能力集合:
- cap_effective: 实际有效的权限。
- cap_permitted: 允许的权限。
示例代码:
#include <sys/capability.h>//包含了Linux能力(capabilities)相关的API定义,允许程序查询或修改进程的能力。
void check_capability(cap_value_t cap) {
cap_t caps = cap_get_proc();//cap_get_proc()函数被调用以获取当前进程的能力对象。这个对象包含了进程的所有能力信息。
cap_flag_value_t cap_val;
cap_get_flag(caps, cap, CAP_EFFECTIVE, &cap_val);//cap_get_flag()函数用于检查指定能力(cap参数指定)在进程的有效能力集中是否被设置(CAP_EFFECTIVE表示正在查询有效能力集)。
//根据cap_val的值,输出该能力是否被设置。如果被设置,cap_val将等于CAP_SET。
if (cap_val == CAP_SET) {
printf("Capability is set\n");
} else {
printf("Capability is not set\n");
}
cap_free(caps);
}
int main() {
//调用了check_capability(),并检查当前进程是否有绑定低编号端口的能力(CAP_NET_BIND_SERVICE)。
//这是许多网络服务需要的权限,通常只有系统管理员(root)才具备。
check_capability(CAP_NET_BIND_SERVICE);
return 0;
}
内存管理
每个进程都有自己独立的虚拟内存空间,这需要有一个数据结构来表示,就是mm_struct。
struct mm_struct *mm;
struct mm_struct *active_mm;
文件与文件系统
/* Filesystem information: */
struct fs_struct *fs;
/* Open file information: */
struct files_struct *files;
用户态与内核态
用户态与内核态的转换
进程执行从用户态到内核态的转换通常发生在系统调用或中断发生时。这种状态转换涉及到以下两个关键成员变量:
- thread_info - 这是一个结构体,存在于每个进程的内核栈的底部,包含了指向task_struct的指针以及其他线程特定的信息。
- stack - 指向进程的内核栈,内核栈用于存储进入内核态时的临时数据,包括函数调用和中断处理的上下文。
函数调用栈
- 用户态函数栈:在用户空间中,函数的调用和返回是通过栈实现的。栈用于存储局部变量、返回地址和函数参数。
- 内核态函数栈:当进程进入内核态时,会使用独立的内核栈,这保证了内核操作的安全性和独立性。
内核栈和task_struct的关系
每个进程的内核栈顶部附近存储了一个thread_info结构体,该结构体中包含了一个指向对应task_struct的指针。
在某些架构中,可以直接通过内核栈地址找到task_struct,因为thread_info位于内核栈的底部。
示例代码1:
获取当前进程的task_struct
在Linux内核开发中,经常需要访问当前进程的task_struct。
#include <linux/sched.h> // 包含 task_struct 和 current 宏
#include <linux/module.h> // 包含内核模块的基本功能
//内核模块初始化函数
//它使用 current 宏获取当前进程的 task_struct,然后打印出该进程的名称(comm 字段)和进程ID(pid 字段)。这是诊断和监视当前运行环境非常有用的信息。
int init_module(void) {
struct task_struct *curr = current; // current 宏用于获取当前正在CPU上运行的进程的 task_struct 指针。
printk(KERN_INFO "Current process is \"%s\" (pid %i)\n", curr->comm, curr->pid);
return 0;
}
//当模块被卸载时执行的清理函数。此示例中,它仅打印一条消息表示模块正在被清理。
void cleanup_module(void) {
printk(KERN_INFO "Module cleanup\n");
}
//指定模块的许可证类型,这对于模块的分发和使用有法律上的意义。"GPL"表示该模块遵守GNU通用公共许可证。
MODULE_LICENSE("GPL");
这段代码定义了一个内核模块,当模块加载时,它会打印当前进程的名称和PID。
在用户程序中,通常不需要直接处理栈,但是理解栈的工作方式有助于调试和性能优化。例如,递归函数或深层嵌套的函数调用可能会耗尽栈空间,导致栈溢出。
示例代码2:
查看进程的父进程信息
#include <linux/sched.h>
#include <linux/module.h>
int init_module(void) {
struct task_struct *parent;
parent = current->real_parent; // 获取当前进程的父进程的 task_struct
printk(KERN_INFO "Parent process is \"%s\" (pid %i)\n", parent->comm, parent->pid);
return 0;
}
这段代码将打印当前进程的父进程的名称和PID,有助于理解进程树的结构。
示例代码3:
监视进程状态变化
#include <linux/sched.h>
#include <linux/module.h>
int init_module(void) {
printk(KERN_INFO "Current process state: %ld\n", current->state);
return 0;
}
这段代码查看并打印当前进程的状态(如运行、睡眠等)。state 字段是 task_struct 中用于追踪进程状态的关键成员。