OS - 进程管理
问题: 当执行程序时,其main函数是如何被调用的?
说明:C程序总是从main函数开始执行,main函数参数有两个参数
int argc 表示命令行参数的数目, argv 是指向参数的各个指针所构成的数组。
1. 当内核执行C程序时,在调用main前先调用一个启动例程,
2. 可执行程序文件将此启动例程指定为程序的起始地址。
3. 启动例程从内核取得命令行参数和环境变量值,最后调用main函数。
命令行参数是如何传递给程序的?
典型的存储器布局是什么样式?
如何分配另外的存储空间?
进程如何使用环境变量?
各种不同的进程终止方式有哪些?
第一种:从main函数返回
第二种:调用exit
第三种:调用_exit或_Exit
第四种:最后一个线程从其启动例程返回
第五种:最后一个线程调用pthread_exit
第六种:调用abort
第七种:接到一个信号并终结
第八种:最后一个线程对取消请求做出响应
一、进程表示及结构
进程是程序执行时的一个实例。
对Linux而言,线程是特殊的进程。
1. 进程结构
进程描述符及任务结构 include/linux/sched.h 头文件中 task_struct 结构体
* 状态信息 - 描述进程动态的变化 (volatile long state;)
* 链接信息 - 描述进程 父/子 关系
* 各种标识符 - 用简单数字对进程进行标识
* 进程间通信信息 - 描述多个进程在同一任务上协作工作
* 时间和定时器信息 - 描述进程在生存周期内使用CPU时间的统计,计费等信息
* 调度信息 - 描述进程优先级、调度策略等信息
* 文件系统信息 - 对进程使用文件情况进行记录
* 虚假内存信息 - 描述每个进程拥有的地址空间
* 处理器环境信息 - 描述进程的执行环境。
<linux/sched.h>
struct task_struct { struct thread_info thread_info;
volatile long state; // 进程状态, -1 unrunnable, 0 runnable, >0 stopped
....
pid_t pid; // 默认最大值为32768,可通过/proc/sys/kernel/pid_max来修改
pid_t tgid;
....
struct list_head task;
....
struct task_struct __rcu *real_parent;
struct task_struct __rcu *parent;
struct list_head children;
struct list_head sibling;
};
2. 进程状态(task_struct 域中的 state)
进程转换状态表

以下这张图摘录于 Linux 内核设计与实现进程状态一节

* TASK_RUNNING: 进程正在执行或即将执行。
* TASK_INTERRUPTIBLE: 进程被挂起,等待某些条件为真时执行。
* TASK_UNINTERRUPTIBLE(不可中断)
* __TASK_TRACED(被其他进程跟踪执行)
* __TASK_STOPPED(停止)
使用set_task_state(task, state)函数来设置进程的状态
3. 进程PID
是一个pid_t隐含类型,可以通过使用cat /proc/sys/kernel/pid_max 来查看或修改。
使用current_thread_info()->task 来查找当前进程信息。
#include <unistd.h> pid_t getpid(void) pid_t getppid(void) uid_t getuid(void); uid_t geteuid(void); gid_t getgid(void); gid_t getgid(void);
4. 进程父子关系
所有的进程都是PID为1的init的进程的后代,内核在系统启动的最后阶段启动init进程,该进程读取系统的初始化脚本initscript
并执行其他的相关程序,最终完成系统启动的整个过程。
系统中每个进程必有一个父进程,相应的,每个进程可以拥有零个或多个子进程。
查看进程资源信息 <resources.h>
[root@iZuf60r88kfl53l0xh9x04Z ~]# cat /proc/self/limits Limit Soft Limit Hard Limit Units Max cpu time unlimited unlimited seconds Max file size unlimited unlimited bytes Max data size unlimited unlimited bytes Max stack size 8388608 unlimited bytes Max core file size 0 unlimited bytes Max resident set unlimited unlimited bytes Max processes 7281 7281 processes Max open files 65535 65535 files Max locked memory 65536 65536 bytes Max address space unlimited unlimited bytes Max file locks unlimited unlimited locks Max pending signals 7281 7281 signals Max msgqueue size 819200 819200 bytes Max nice priority 0 0 Max realtime priority 0 0 Max realtime timeout unlimited unlimited us [root@iZuf60r88kfl53l0xh9x04Z ~]#
查看进程表
root@raspberrypi:/proc # ps -auxef UID PID PPID C STIME TTY TIME CMD root 1 0 0 1月13 ? 00:00:05 /sbin/init root 2 0 0 1月13 ? 00:00:00 [kthreadd] root 3 2 0 1月13 ? 00:00:00 [ksoftirqd/0] root 5 2 0 1月13 ? 00:00:00 [kworker/0:0H] root 7 2 0 1月13 ? 00:00:06 [rcu_sched] root 8 2 0 1月13 ? 00:00:00 [rcu_bh] root 9 2 0 1月13 ? 00:00:00 [migration/0] root 10 2 0 1月13 ? 00:00:00 [lru-add-drain] root 11 2 0 1月13 ? 00:00:00 [cpuhp/0] root 12 2 0 1月13 ? 00:00:00 [cpuhp/1] root 13 2 0 1月13 ? 00:00:00 [migration/1] root 14 2 0 1月13 ? 00:00:00 [ksoftirqd/1] root 16 2 0 1月13 ? 00:00:00 [kworker/1:0H] root 17 2 0 1月13 ? 00:00:00 [cpuhp/2]
* UID 表示启动进程的用户
* PID 表示当前进程的pid
* PPID 表示当前父进程ID
* C
* STIME
* TTY 表示由哪个终端启动的
* TIME 表示占用CPU时间
* CMD 表示启动命令
进程状态显示
| STAT代码 | 说明 |
| S |
睡眠。通常是在等待某个事件的发生 如一个信号或有输入可用。 |
| R | 运行。在运行队列中,处于正在执行或即将运行状态 |
| D | 不可中断的睡眠(等待)。 |
| T | 停止。通常是被Shell作业控制停止 |
| Z | 死进程或僵尸进程 |
| N | 低优先级任务 |
| W | 分页 |
| s | 进程是会话期首进程 |
| + | 进程属于前台进程组 |
| l | 进程是多线程的 |
| < | 高优先级任务 |
二、创建进程
第一步:调用fork()函数,拷贝当前进程创建子进程
第二步:调用exec()函数负责读取可执行文件开始执行。
#include <sys/types.h> #include <unistd.h> pid_t fork(void);
// 如果fork失败,返回-1
通常失败的两种原因
* 子进程数目超过了规定的限制(CHILD_MAX), errno设为EAGAIN
* 进程表里没有足够的空间或虚拟内存不足,errno设为ENOMEM
fork 流程如下

在父进程中的fork调用返回的是新的子进程的PID,新进程将继续执行
就像原进程一样,子进程中的fork调用返回0.
典型的fork代码如下
pid_t new_pid; new_pid = fork(); switch(new_pid) { case -1: /*Error*/ break; case 0: /*We are child*/ break; default : /*We are parent*/ break; }
一个例子如下
/*pro.c*/ #include <sys/types.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> int main() { pid_t pid; // 定义进程描述符 char *message; int n; printf("fork program starting\n"); pid = fork(); // fork 返回进程描述符,如果是0,为子进程,如果非0,为父进程,小于0为失败 switch(pid) { case -1: perror("fork failed"); exit(1); case 0: message = "This is the child"; n = 5; break; default: message = "This is the parent"; n = 3; break; } for(; n > 0; n--) { puts(message); sleep(1); } exit(0); } /* [root@localhost ~]# ./pro fork program starting This is the parent This is the child This is the parent This is the child This is the parent This is the child [root@localhost ~]# This is the child This is the child */
此程序以两个进程的形式在运行,子进程被创建并且输出消息5次,父进程输出消息3次
父进程在子进程打印完它的全部消息之前就结束,因此打印消息中有一个shell提示符。
Linux 通过clone() 系统调用实现fork()。 clone() 再去调用do_fork()。
do_fork定义在 kernel/fork.c 文件中, do_fork 调用 copy_process() 函数
wait函数: 暂停父进程直到它的子进程结束为止,返回子进程PID。
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *stat_loc);
状态信息允许父进程了解子进程的退出状态,即子进程的main函数返回的值或子进程中exit函数的退出码。
sys/wait.h 头文件中
WIFEXITED(stat_val) 如果子进程正常结束,它就取一上非零值
WEXITSTATUS(stat_val) 如果WIFEXITED非零,它返回子进程的退出码
WIFSIGNALED(stat_val) 如果子进程因为一个未捕获的信号而终止,它就取一个非零值
WTERMSIG(stat_val) 如果WIFSIGNALED非零,它返回一个信号代码
WIFSTOPPED(stat_val) 如果子进程意外终止,它就取一个非零值
WSTOPSIG(stat_val) 如果WIFSTOPPED非零,它返回一个信号代码
// wait.c #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> int main() { pid_t pid; char *message; int n; int exit_code; printf("fork program starting\n"); pid = fork(); switch(pid) { case -1: perror("fork failed"); exit(1); case 0: message = "This is the child"; n = 5; exit_code = 37; break; default: message = "This is the parent"; n = 3; exit_code = 0; break; } for(; n > 0; n--) { puts(message); sleep(1); }
// 父进程将会运行 if(pid != 0) { int stat_val; pid_t child_pid; child_pid = wait(&stat_val); printf("Child has finished: PID = %d\n", child_pid); if(WIFEXITED(stat_val)) printf("Child exited with code %d\n", WEXITSTATUS(stat_val)); else printf("Child terminated abnormally\n"); } exit(exit_code); } /* [root@localhost ~]# ./wait fork program starting This is the parent This is the child This is the parent This is the child This is the parent This is the child This is the child This is the child Child has finished: PID = 6229 Child exited with code 37 [root@localhost ~]# */
父进程用wait系统调用将自己的执行挂起,直到子进程的状态信息出现为止。
system函数启动新的进程: 运行以字符串参数形式传递给它的命令并等待该命令的完成。 需要调用shell,开销较大。
#include <stdlib.h> int system(const char *string);
// 如果无法启动shell来运行这个命令,返回错误代码127
// 如果是其他错误,返回-1
// system.c 使用system系统调用运行ps aux
#include <stdlib.h> #include <stdio.h> int main() { printf("Running ps with system\n"); system("ps aux &"); // 加& 与 不加的区别 printf("Done.\n"); exit(0); }
加& 表示在调用shell命令结束后,立刻返回。
exec 函数 可以把当前进程替换为一个新进程,新进程由path或file参数指定。
#include <unistd.h>
char **environ;
int execl(const char *path, const char *arg0, ..., (char*)0);
int execlp(const char *file, const char *arg0, ..., (char*)0);
int execle(const char *path, const char *arg0, ..., (char *)0, char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
#include <unistd.h> char *const ps_argv[] = {"ps", "ax", 0}; char *const ps_envp[] = {"PATH/bin:/usr/bin", "TERM=console", 0}; execl("/bin/ps", "ps", "ax", 0); execlp("ps", "ps", "ax", 0); execle("/bin/ps", "ps", "ax", 0, ps_envp); execv("/bin/ps", ps_argv); execvp("ps", ps_argv); execve("/bin/ps", ps_argv, ps_envp);
#include <unistd.h> #include <stdio.h> #include <stdlib.h> int main() { printf("Running ps with execlp\n"); execlp("ps", "ps", "ax", 0); printf("Done.\n"); exit(0); }
用fork来创建进程时,必须时刻考虑子进程的状态。
如果子进程率先结束,父进程还未结束,子进程将会变成僵尸进程,
因为子进程的表项不会立刻释放。
如果父进程异常终止,子进程将自动把PID为1的进程(init)作为自己的父进程。
僵尸进程将一直保留在进程表中,直到被Init进程发现并释放。
waitpid函数系统调用用来等待子进程的结束
#include <sys/types.h> #include <sys/wait.h> pid_t waitpid(pid_t pid, int *stat_loc, int options);
参数
* pid 表示需要等待的子进程PID,如果值为-1,waitpid将返回任一子进程的信息。
* stat_loc 如果不是空指针,将把状态信息写到它所指向的位置。
* options 用来改变waitpid的行为。 可能值如下
WNOHANG - 防止waitpid调用将调用者的执行挂起。
如果想周期性检查某个特定的子进程是否已终止
waitpid(child_pid, (int *) 0, WNOHANG);
* 如果子进程没有结束或意外终止,它就返回0,否则返回child_pid.
* 如果waitpid失败,它将返回-1并设置errno。
四、线程
对于Linux来说,线程只是一种进程间共享资源的手段。
五、进程终结(kernel/exit.c 中 do_exit() 函数)
六、进程调度程序
公平调度算法 在 kernel/sched_fair.c 文件中

浙公网安备 33010602011771号