什么是程序?
程序:可执行文件或者包含一堆可运行CPU指令的和数据
什么是进程?
进程 = 程序 + 执行
进程是执行中的程序,除了可执行代码外还包含进程的活动信息和数据,比如用来存放函数变量、局部变量、返回值的用户栈,存放进程相关数据的数据段,内核中进程间切换的内核栈,动态分配的堆。
进程是系统分配资源的基本单位(内存、CPU时间片)
进程是用来实现多进程并发执行的一个实体,实现对CPU的虚拟化,让每个进程感觉都拥有一个CPU,核心技术就是上下文切换和进程调度。
早期操作系统程序都是单个运行的,CPU利用率低下,为了提高CPU的利用率,加载多个程序到内存并发运行,在单核CPU中这种属于伪并发。
其实在同一时间只运行一个程序
进程控制块描述符
进程控制块描述符task_struct主要包含:
进程状态(state):表示进程当前的状态,比如运行、睡眠、停止等。
/* Used in tsk->__state: */
#define TASK_RUNNING 0x00000000
#define TASK_INTERRUPTIBLE 0x00000001
#define TASK_UNINTERRUPTIBLE 0x00000002
#define __TASK_STOPPED 0x00000004
#define __TASK_TRACED 0x00000008
/* Used in tsk->exit_state: */
#define EXIT_DEAD 0x00000010
#define EXIT_ZOMBIE 0x00000020
#define EXIT_TRACE (EXIT_ZOMBIE | EXIT_DEAD)
进程调度信息(sched_info):包括进程的调度策略、优先级等信息
int on_rq;
int prio;
int static_prio;
int normal_prio;
unsigned int rt_priority;
struct sched_entity se;
struct sched_rt_entity rt;
struct sched_dl_entity dl;
struct sched_dl_entity *dl_server;
const struct sched_class *sched_class;
on_rq:表示进程是否在就绪队列中,即是否正在等待被调度执行。
prio:表示进程的动态优先级。
static_prio:表示进程的静态优先级。
normal_prio:表示进程的普通优先级。
rt_priority:表示实时进程的优先级。
se:sched_entity 结构体,用于普通进程的调度实体。
rt:sched_rt_entity 结构体,用于实时进程的调度实体。
dl:sched_dl_entity 结构体,用于周期性实时进程的调度实体。
dl_server:指向调度该进程的周期性实时进程的指针。
sched_class:指向调度类(sched_class)的指针,用于确定进程的调度策略和行为。
进程标识符(pid):唯一标识一个进程的数字标识符,内核使用bitmap保证进程分配唯一的pid。
一个线程组中所有的线程使用和线程组组长相同的pid,它会被存进tgid中
使用getpid()系统调用获取pid
pid_t pid;
pid_t tgid;
进程堆栈(stack):用于保存进程的函数调用栈信息。
void *stack;
引用计数(usage):用于跟踪进程的引用计数,确保在不再需要时能够正确释放资源。
refcount_t usage;
进程标志位(flags):存储进程的各种标志位信息,比如是否在运行、是否被挂起等。
unsigned int flags;
多处理器支持字段(on_cpu、wake_entry):用于处理多处理器环境下的调度和唤醒操作。
#ifdef CONFIG_SMP
int on_cpu;
struct __call_single_node wake_entry;
unsigned int wakee_flips;
unsigned long wakee_flip_decay_ts;
struct task_struct *last_wakee;
/*
* recent_used_cpu is initially set as the last CPU used by a task
* that wakes affine another task. Waker/wakee relationships can
* push tasks around a CPU where each wakeup moves to the next one.
* Tracking a recently used CPU allows a quick search for a recently
* used CPU that may be idle.
*/
int recent_used_cpu;
int wake_cpu;
#endif
进程间家庭关系
/*
* Pointers to the (original) parent process, youngest child, younger sibling,
* older sibling, respectively. (p->father can be replaced with
* p->real_parent->pid)
*/
/* Real parent process: */
struct task_struct __rcu *real_parent;
/* Recipient of SIGCHLD, wait4() reports: */
struct task_struct __rcu *parent;
/*
* Children/sibling form the list of natural children:
*/
struct list_head children;
struct list_head sibling;
struct task_struct *group_leader;
进程的5种状态
创建态(New):当进程刚被创建时,处于创建态。在这个阶段,操作系统正在为进程分配资源和初始化进程控制块等信息。
就绪态(Ready):进程已经准备好运行,但由于还未获得处理器资源,暂时无法执行。进程在就绪队列中等待被调度执行。
运行态(Running):进程正在执行指令,占用 CPU 资源。在任意时刻,每个 CPU 上只能有一个进程处于运行态。
阻塞态(Blocked):进程由于等待某种事件(如 I/O 操作完成、信号量变为非零等)而暂时无法继续执行,进入阻塞态。在等待期间,进程不占用 CPU 资源。
终止态(Terminated):进程执行完毕或被操作系统终止后,进入终止态。在终止态下,进程的资源被释放,但仍保留进程控制块等信息,直到被操作系统回收。
查看进程状态
static void show_task(struct task_struct *volatile tsk)
{
unsigned int p_state = READ_ONCE(tsk->__state);
char state;
/*
* Cloned from kdb_task_state_char(), which is not entirely
* appropriate for calling from xmon. This could be moved
* to a common, generic, routine used by both.
*/
state = (p_state == TASK_RUNNING) ? 'R' :
(p_state & TASK_UNINTERRUPTIBLE) ? 'D' :
(p_state & TASK_STOPPED) ? 'T' :
(p_state & TASK_TRACED) ? 'C' :
(tsk->exit_state & EXIT_ZOMBIE) ? 'Z' :
(tsk->exit_state & EXIT_DEAD) ? 'E' :
(p_state & TASK_INTERRUPTIBLE) ? 'S' : '?';
printf("%16px %16lx %16px %6d %6d %c %2d %s\n", tsk,
tsk->thread.ksp, tsk->thread.regs,
tsk->pid, rcu_dereference(tsk->parent)->pid,
state, task_cpu(tsk),
tsk->comm);
}
init_task
初始化了 init_task 结构体的各个成员,包括线程信息、栈信息、调度优先级、CPU 信息、内存管理、信号处理、文件系统等等。
这些成员变量记录了进程的状态、资源分配、调度信息等重要内容。
struct task_struct init_task __aligned(L1_CACHE_BYTES) = {
.__state = 0,
.stack = init_stack,
.usage = REFCOUNT_INIT(2),
.flags = PF_KTHREAD,
.prio = MAX_PRIO - 20,
.static_prio = MAX_PRIO - 20,
.normal_prio = MAX_PRIO - 20,
.policy = SCHED_NORMAL,
.cpus_ptr = &init_task.cpus_mask,
.user_cpus_ptr = NULL,
.cpus_mask = CPU_MASK_ALL,
.nr_cpus_allowed= NR_CPUS,
.mm = NULL,
.active_mm = &init_mm,
.faults_disabled_mapping = NULL,
.restart_block = {
.fn = do_no_restart_syscall,
},
.se = {
.group_node = LIST_HEAD_INIT(init_task.se.group_node),
},
.rt = {
.run_list = LIST_HEAD_INIT(init_task.rt.run_list),
.time_slice = RR_TIMESLICE,
},
.tasks = LIST_HEAD_INIT(init_task.tasks),
.ptraced = LIST_HEAD_INIT(init_task.ptraced),
.ptrace_entry = LIST_HEAD_INIT(init_task.ptrace_entry),
.real_parent = &init_task,
.parent = &init_task,
.children = LIST_HEAD_INIT(init_task.children),
.sibling = LIST_HEAD_INIT(init_task.sibling),
.group_leader = &init_task,
RCU_POINTER_INITIALIZER(real_cred, &init_cred),
RCU_POINTER_INITIALIZER(cred, &init_cred),
.comm = INIT_TASK_COMM,
.thread = INIT_THREAD,
.fs = &init_fs,
.files = &init_files,
.signal = &init_signals,
.sighand = &init_sighand,
.nsproxy = &init_nsproxy,
.pending = {
.list = LIST_HEAD_INIT(init_task.pending.list),
.signal = {{0}}
},
.blocked = {{0}},
.alloc_lock = __SPIN_LOCK_UNLOCKED(init_task.alloc_lock),
.journal_info = NULL,
INIT_CPU_TIMERS(init_task)
.pi_lock = __RAW_SPIN_LOCK_UNLOCKED(init_task.pi_lock),
.timer_slack_ns = 50000, /* 50 usec default slack */
.thread_pid = &init_struct_pid,
.thread_node = LIST_HEAD_INIT(init_signals.thread_head),
INIT_PREV_CPUTIME(init_task)
};
EXPORT_SYMBOL(init_task);
进程创建函数
fork
fork函数用于创建一个新的进程,新进程是调用进程(父进程)的副本。
函数名称:fork
头文件:#include <unistd.h>
返回类型:pid_t(进程ID类型)
参数:无参数
返回值:在父进程中,fork返回新创建子进程的进程ID(PID),在子进程中,fork返回0。如果出现错误,fork返回-1。
通过调用fork函数,父进程会创建一个子进程,子进程会继承父进程的数据、堆栈、文件描述符等信息,但是它们各自有独立的内存空间。
vfork
vfork函数用于创建一个新的进程,但与fork不同的是,vfork保证子进程先运行,调用do_fork时比fork多了两个标志位,
CLONE_VFORK和CLONE_VM,分别代表父进程被挂起,直到子进程释放资源和父子进程运行在相同的内存空间。
函数名称:vfork
头文件:#include <unistd.h>
返回类型:pid_t(进程ID类型)
参数:无参数
返回值:在父进程中,vfork返回新创建子进程的进程ID(PID),在子进程中,vfork返回0。如果出现错误,vfork返回-1。
clone
clone函数用于创建一个新的线程或进程,可以指定不同的选项来控制创建的行为
int
clone(int (*fn)(void *), void *child_stack, int flags, void *arg, ... /* pid_t *ptid, unsigned long newtls, pid_t *ctid */ );
函数名称:clone
头文件:#include <sched.h>
返回类型:int
参数:
fn:指向新线程/进程入口点函数的指针
child_stack:子进程/线程的栈指针
flags:用于指定创建新线程/进程的选项
arg:传递给新线程/进程入口点函数的参数
...:可选参数,包括ptid、newtls和ctid
返回值:成功时返回新线程/进程的PID(对于线程,返回0表示成功),失败时返回-1。
kthread_create
kthread_create函数用于创建一个新的内核线程,该线程在内核空间中运行,可以执行内核级别的任务。
通过调用kthread_create函数,可以在Linux内核中创建一个新的内核线程,用于执行后台任务、定时任务等内核级别的工作。内核线程与用户空间的线程有所不同,它们在内核空间中运行,可以访问内核数据结构和执行特权操作。
struct task_struct *kthread_create(int (*threadfn)(void *data), void *data, const
char namefmt[], ...);
函数名称:kthread_create
返回类型:struct task_struct *(指向内核线程结构体的指针)
参数:threadfn:指向内核线程函数的指针,即内核线程的入口点函数
data:传递给内核线程函数的参数
namefmt:内核线程的名称格式字符串
...:可变参数,用于指定内核线程的调度优先级等其他选项
功能:
返回值:成功时返回指向新创建内核线程的task_struct结构体指针,失败时返回NULL。
do_fork
fork/vfork/clone/kthread_create底层都是通过调用do_fork创建进程
long do_fork(unsigned long clone_flags,unsigned long stack_start, unsigned long stack_size,int __user *parent_tidptr,int __user *child_tidptr)
{
return _do_fork(clone_flags, stack_start, stack_size,
parent_tidptr, child_tidptr, 0);
}
clone_flags:用于指定创建新进程/线程的选项,包括是否共享地址空间、文件描述符等。
stack_start:新进程/线程的栈起始地址。
stack_size:新进程/线程的栈大小。
parent_tidptr:指向父进程/线程的线程ID的指针。
child_tidptr:指向子进程/线程的线程ID的指针。
子进程不会继承的一些主要属性和资源:
进程ID(PID):子进程会有自己独立的进程ID,不会继承父进程的PID。
父进程ID(PPID):子进程的父进程ID会被设置为创建它的父进程的PID,而不是继承父进程的PPID。
信号处理器:子进程不会继承父进程设置的信号处理器,它们会有各自独立的信号处理器。
文件锁:子进程不会继承父进程设置的文件锁。
定时器:子进程不会继承父进程设置的定时器。
共享内存和信号量:子进程不会继承父进程的共享内存和信号量。
资源限制:子进程不会继承父进程设置的资源限制,如文件打开数限制等。
execve:
功能:execve系统调用用于加载并执行一个新的程序,替换当前进程的映像(代码和数据)为新程序的映像。
参数:execve接受三个参数,分别是要执行的程序路径、命令行参数数组和环境变量数组。
返回值:如果execve执行成功,则不会返回,因为当前进程的映像已被替换为新程序的映像;如果出现错误,则返回-1。
特点:execve会将当前进程的映像替换为新程序的映像,新程序开始执行时,会继承当前进程的PID等信息,但不会保留原有进程的任何状态。
写时复制技术
在传统的unix操作系统中,创建新进程时会复制父进程所拥有的资源,但是子进程不一定需要父进程的全部资源。
在现代的操作系统中采用了写时复制copy on write,COW技术,在创建子进程时只需要复制进程的地址空间页表,
只读共享进程地址空间,当父子进程其中一方需要修改页面数据时,触发缺页异常,此时才会从复制内容。
终止进程
1.程序主动主动调用exit退出
2.进程收到SIGKILL信号
kill -15 PID # 发送SIGTERM信号
kill -9 PID # 发送SIGKILL信号
3.触发内核异常
4.收到不能处理的信号
僵尸进程和孤儿进程
僵尸进程:
定义:当一个进程终止,但其父进程没有及时处理该进程的终止状态信息(称为SIGCHLD信号),导致该进程的进程描述符仍然存在,但进程已经终止,此时该进程就成为僵尸进程。
特点:僵尸进程不占用系统资源,但会占用进程表中的一个条目。
解决方法:父进程应该及时处理SIGCHLD信号,通过调用wait()或waitpid()等系统调用来回收子进程的资源,防止子进程变成僵尸进程。
孤儿进程:
定义:当一个进程的父进程提前终止,而该进程本身还在运行,此时该进程成为孤儿进程。
特点:孤儿进程会被init进程(PID为1)接管,init进程会成为孤儿进程的新父进程。
影响:孤儿进程的父进程终止后,孤儿进程会继续运行,直到自己终止或被init进程接管。
0号进程
**0、1号进程代码来源0.11版本内核**
0号进程通常指的是内核线程(kernel thread)或者是调度进程(scheduler process),其PID为0。这个进程在系统启动时就已经存在,并且在整个系统运行期间都存在。
0号进程通常被称为内核线程,因为它在内核空间运行,不属于用户空间的任何进程。它在系统中扮演着重要的角色,负责系统的调度、内存管理、I/O操作等核心功能。由于它是内核的一部分,因此没有对应的用户空间程序,也不会被用户直接创建或终止。
在Linux系统中,0号进程通常是系统中所有进程的祖先,即所有进程的父进程。当一个进程的父进程终止时,该进程会成为孤儿进程,并被0号进程(init进程,PID为1)接管。
sched_init(); // 初始化0进程
void sched_init(void)
{
int i;
struct desc_struct * p;
if (sizeof(struct sigaction) != 16)
panic("Struct sigaction MUST be 16 bytes");
set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss)); // 设置TSS
set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt)); // 设置LDT
p = gdt+2+FIRST_TSS_ENTRY; // 获取TSS
for(i=1;i<NR_TASKS;i++) { // 遍历2^6个任务,但要清空除了0之外的任务
task[i] = NULL;
p->a=p->b=0;
p++;
p->a=p->b=0;
p++;
}
/* Clear NT, so that we won't have troubles with that later on */
__asm__("pushfl ; andl $0xffffbfff,(%esp) ; popfl"); // 清空NT
ltr(0); // 挂载TSS到TR寄存器
lldt(0); // 挂载LDTR寄存器
// 设置定时器模式、以及设置高低位组成一个周期
outb_p(0x36,0x43); /* binary, mode 3, LSB/MSB, ch 0 */
outb_p(LATCH & 0xff , 0x40); /* LSB */
outb(LATCH >> 8 , 0x40); /* MSB */
set_intr_gate(0x20,&timer_interrupt); // 开启定时器中断
outb(inb_p(0x21)&~0x01,0x21); // 允许时钟中断
set_system_gate(0x80,&system_call); // 设置系统调用的入口
}
1号进程
在Linux系统中,1号进程通常指的是init进程,其PID为1。init进程是系统中所有进程的祖先进程,是系统启动时由内核创建的第一个用户级进程。init进程负责系统的初始化、进程的管理和系统的关机等任务。
void main(void) /* This really IS void, no error here. */
{ /* The startup routine assumes (well, ...) this */
mem_init(main_memory_start,memory_end); // 初始化内存映射
trap_init(); // 初始化中断捕获
blk_dev_init(); // 块设备初始化
chr_dev_init(); // 字符设备初始化
tty_init(); // 终端初始化
time_init(); // 时间初始化
sched_init(); // 初始化0进程
buffer_init(buffer_memory_end); // 缓冲区初始化
hd_init(); // 初始化硬盘
floppy_init(); // 初始化软盘
sti(); // 开启全局中断
move_to_user_mode(); // 将进程0特权调到3级
if (!fork()) { /* we count on this going ok */
init(); // 子进程进行初始化
}
void init(void)
{
int pid,i; // pid用于fork
setup((void *) &drive_info); // 配置系统,包括磁盘、文件系统
(void) open("/dev/tty0",O_RDWR,0); // 打开tty文件,此时是标准输入设备文件
(void) dup(0); // 从tty复制句柄,打开标准输出设备文件
(void) dup(0); // 继续复制句柄,打开标准错误输出设备
printf("%d buffers = %d bytes buffer space\n\r",NR_BUFFERS,
NR_BUFFERS*BLOCK_SIZE);
printf("Free mem: %d bytes\n\r",memory_end-main_memory_start);
if (!(pid=fork())) { // 到这里就要启动进程2了
// fs/open.c
close(0); // 进程2关闭输入
if (open("/etc/rc",O_RDONLY,0)) // 使用/etc/rc替换输入设备,加载一些开机需要执行的东西
_exit(1); // 替换失败就寄了
// do_execve(fs/exec.c)
execve("/bin/sh",argv_rc,envp_rc); // 执行shell,参数分别是shell执行参数(="NULL")其环境变量(="/")
// 由于输入已经改成了/etc/rc文件了,所以这里在运行/etc/rc的内容
_exit(2);
}
if (pid>0) // 进程1暂停工作,等待子进程工作完成(子进程只有进程2)
while (pid != wait(&i)) // 暂不深入wait -> 补充: 进程2退出了,回到了这里,pid = 2
/* nothing */;
while (1) {
if ((pid=fork())<0) { // 继续fork
printf("Fork failed in init\r\n");
continue;
}
if (!pid) { // 新的子进程
close(0);close(1);close(2); // 关闭了标准输入、标准输出、异常输出
setsid();
(void) open("/dev/tty0",O_RDWR,0); // 打开了tty0
(void) dup(0); // 标准输入
(void) dup(0); // 标准输出
_exit(execve("/bin/sh",argv,envp)); // 新进程打开sh,这里已经是用户可以控制的时候了
}
while (1)
if (pid == wait(&i)) // 进程1重新进入等待
break;
printf("\n\rchild %d died with code %04x\n\r",pid,i);
sync();
}
_exit(0); /* NOTE! _exit, not exit() */
}