十、进程
1.进程的基本概念
程序:磁盘上的可执行文件。
| 加载
v
进程:内存中的指令和数据。
执行 | ^
v | 访问
CPU---+
2.进程的分类
交互式进程:由Shell启动,借助标准I/O与用户交互。
批处理进程:在无需人工干预的条件下,自动运行一组批量任务。
守护(精灵)进程:后台服务,多数时候处于待命状态,一旦有需要可被激活完成特定的任务。
3.进程快照
ps - :显示当前用户拥有控制终端的进程信息
ps axuw - BSD风格选项
a: 所有用户
x: 既包括有控制终端也包括无控制终端的进程
u: 详细信息
w: 更大列宽
ps -efFl - SVR4风格选项
e: 所有用户的所有进程
f: 完整格式
F: 更完整格式
l: 长格式
ps aux 看到的每一列的意思:
USER/UID: 进程的实际用户ID
PID: 进程标识
%CPU/C: CPU使用率
%MEM: 内存使用率
VSZ: 占用虚拟内存大小(KB)
RSS: 占用半导体物理内存大小(KB)
TTY: 终端次设备号
ttyn - 物理终端(硬件设备)
pts/n - 虚拟终端(软件窗口)
? - 无控制终端,如后台进程
STAT/S: 进程状态
O - 就绪,等待被调度
R - 运行,Linux下没有O状态,就绪状态也用R表示
S - 可唤醒睡眠,系统中断,获得资源,收到信号,都可被唤醒,转入运行状态
D - 不可唤醒的睡眠,只能被wake_up系统调用唤醒
T - 暂停,收到SIGSTOP(19)信号转入暂停状态,收到SIGCONT(18)信号转入运行状态
W - 等待内存分页(2.6内核后被废弃)
X - 终止且被回收,不可见
Z - 僵尸,已退出但未被回收
< - 高优先级
N - 低优先级
L - 有被锁定在半导体内存中的分页
s - 会话首进程
l - 多线程化
+ - 在前台进程组中
START: 进程启动时间
TIME: 进程运行时间
COMMAND/CMD: 进程启动命令
F: 进程标志
1 - 通过fork产生的子进程,但是
并没有通过exec创建新进程
4 - 拥有超级用户(root)特权
PPID: 父进程的PID
NI: 进程nice值,-20~19,进程优先级浮动量
PRI: 进程优先级=80+nice,60~99,值越小优先级越高
I/O消耗型进程,奖励,提高优先级,降低nice值
处理机消耗型进程,惩罚,降低优先级,提高nice值
ADDR: 内核进程的内存地址,普通进程显示"-"
SZ: 占用虚拟内存页数
WCHAN: 进程正在等待的内核函数或事件
PSR: 进程当前正在被哪个处理器执行
4.父子孤尸
父进程创建子进程,子进程继承父进程。
一个父进程可以创建多个子进程,每个子进程有且仅有一个父进程,除非是根进程(PID=0,调度器实例)没有父进程。
进程树:
调度进程(PID=0)
init(PID=1)
xinetd
in.telnetd <- 远程登录
login <- 用户名和口令
bash <- Shell命令:ls
ls -> 显示目录条目清单
父进程在创建完子进程以后依然存在,甚至可以和子进程进行某种形式的交互,如:传参、回收、通信等。
旧进程在创建完新进程以后被其取代,新进程沿用旧进程的PID,继续独立地存在。
孤儿进程:
父进程创建子进程以后,子进程在操作系统的调度下与其父进程同时运行。如果父进程先于子进程的终止而终止,子进程即成为孤儿进程,同时被init进程收养,即成为init进程的子进程,因此init进程又被成为孤儿院进程。一个进程成为孤儿进程是正常的,系统中大多数守护进程都是孤儿进程。
僵尸进程:
如果子进程先于父进程的终止而终止,但父进程由于某种原因,没有回收子进程的尸体(终止状态),子进程即成为僵尸进程。僵尸进程虽然已经不再活动,但其终止状态和PID仍然被保留,也会占用系统资源,直到其被父进程或init进程回收为止。如果父进程直到其终止都没有回收其处于僵尸状态的子进程,init进程会立即回收这些僵尸。因此一个进程不可能同时既是僵尸进程又是孤儿进程。
5.进程的各种ID
系统内核会为每个进程维护一个进程表项,其中包括如下ID:
进程ID:系统为每个进程分配的唯一标识。内核在分配进程ID时,会持续增加,直到不在增加了,再从头寻找被释放的ID,即延迟重用。
父进程ID:父进程的PID,在创建子进程的过程中被初始化到子进程的进程表项中。
实际用户ID:启动该进程的用户ID。
实际组ID:启动该进程的用户组ID。
有效用户ID:通常情况下,取自进程的实际用户ID。如果该进程的可执行文件带有设置
用户ID位,那么该进程的有效用户ID就取自其可执行文件的拥有者用户ID。
有效组ID:通常情况下,取自进程的实际组ID。如果该进程的可执行文件带有设置组ID位,那么该进程的有效组ID就取自其可执行文件的拥有者组ID。
一个进程的能力和权限,由其有效用户ID和有效组ID决定。
#include <unistd.h>
pid_t getpid(void); // 返回调用进程的PID
pid_t getppid(void); // 返回调用进程的PPID, 即其父进程的PID
uid_t getuid(void); // 返回调用进程的实际用户ID
uid_t getgid(void); // 返回调用进程的实际组ID
uid_t geteuid(void); // 返回调用进程的有效用户ID
uid_t getegid(void); // 返回调用进程的有效组ID
代码:id.c
#include <stdio.h>
#include <unistd.h>
int main(void) {
printf(" 进程ID: %d\n", getpid());
printf(" 父进程ID: %d\n", getppid());
printf("实际用户ID: %d\n", getuid());
printf(" 实际组ID: %d\n", getgid());
printf("有效用户ID: %d\n", geteuid());
printf(" 有效组ID: %d\n", getegid());
return 0;
}
6.创建子进程
产生进程分支(fork)
pid_t fork(void);
成功分别父子进程中返回子进程的PID和0,失败返回-1。
调用一次返回两次:
在父进程中返回所创建子进程的PID,而在子进程中返回0
函数的调用者往往可以根据该函数返回值的不同,分别为父子进程编写不同的处理分支
pid_t pid = fork();
if (pid ==
-1) {
perror(“fork”);
exit(EXIT_FAILURE);
}
if (pid == 0) {
子进程的处理分支
exit(EXIT_SUCCESS);
}
父进程的处理分支
exit(EXIT_SUCCESS);
子进程是父进程不完全副本,子进程的数据区、BSS区、堆栈区(包括I/O缓冲区),甚至命令行参数和全局变量区都从父进程拷贝,唯有代码区与父进程共享。
fork函数成功返回以后,父子进程各自独立地运行,其被调度的先后顺序并不确定,某些实现可以保证子进程先被调度。
fork函数成功返回以后,系统内核为父进程维护的文件描述符表也被复制到子进程的进程表项中,文件表项并不复制。
父进程的进程表项
文件描述符表
…
返回失败的可能:
系统总线程数达到上限
cat (/proc/sys/kernel/threads-max)或用户总进程数达到上限(ulimit -u),fork函数将返回失败。
一个进程如果希望创建自己的副本并执行同一份代码,或希望与另一个进程并发地运行,都可以使用fork函数。
(父)进程
|
__^__fork
/ \
父进程 子进程
\_____/
|
代码共享
数据复制
(父)进程
|
__^__fork/vfork
/ \
| 子进程
父进程 | exec(另一个程序)
| 新进程
\_____/
|
数据和代码
都是独立的
代码:fork.c
#include <stdio.h>
#include <unistd.h>
int main(void) {
printf("%d进程:我要调用fork()了...\n",
getpid());
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return -1;
}
if (pid == 0) {
printf("%d进程:我是%d进程的子进程。\n",
getpid(), getppid());
return 0;
}
printf("%d进程:我是%d进程的父进程。\n",
getpid(), pid);
sleep(1);//等待子进程
return 0;
}
内存复制,子父进程的物理内存不一样
代码:mem.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int global = 100; // 数据区
int main(void) {
int local = 200; // 栈区
int* heap = malloc(sizeof(int));
*heap = 300; // 堆区
printf("父进程:%d %d %d\n",
global, local, *heap);
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return -1;
}
if (pid == 0) {
printf("子进程:%d %d %d\n",
++global, ++local, ++*heap);
free(heap);//复制的是父进程的malloc,如果只在子进程成malloc,父进程不用管
return 0;
}
sleep(1);
printf("父进程:%d %d %d\n",
global, local, *heap);
free(heap);
return 0;
}```
子进程会复制父进程的输出缓冲区
代码:os.c
```c
#include <stdio.h>
#include <unistd.h>
int main(void) {
printf("ABC");
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return -1;
}
if (pid == 0) {
printf("XYZ\n");//会复制父进程的缓冲区到子进程。
return 0;
}
sleep(1);
printf("\n");
return 0;
}
输入缓冲区也会被子进程复制
代码:is.c
#include <stdio.h>
#include <unistd.h>
int main(void) {
printf("父进程:");
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return -1;
}
if (pid == 0) {
scanf("%d%d%d", &a, &b, &c);
printf("子进程:%d %d %d\n", a, b, c);
return 0;
}
sleep(1);
printf