文章目录


1 基础

ps -u skx u 查看当前系统中属于skx用户(有效用户 id)的进程,最后那个 u 表示以用户格式显示。

$ ps -u skx u
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
skx 1698 0.0 0.1 77292 3584 ? Ss 9月19 0:12 /lib/systemd/systemd --user
skx 1699 0.0 0.0 261764 180 ? S 9月19 0:00 (sd-pam)
skx 1713 0.0 0.1 283304 2488 ? Sl 9月19 0:00 /usr/bin/gnome-keyring-daemon
skx 32611 0.2 0.2 24872 5536 pts/0 Ss 22:48 0:00 bash

各字段含义

名称

含义

USER

进程的属主

PID

进程的 id 号

%CPU

进程占用的 CPU 百分比

%MEM

占用的内存百分比

VSZ

进程虚拟大小

RSS

驻留页的数量

TTY

终端 id 号

STAT

进程状态(D、R、S、T、W、X、Z、<、N、L、s 等)

START

进程开始运行时间

TIME

累积使用的CPU时间

COMMAND

使用的命令

进程位于内存里,是有状态的,进程状态值的含义如下:

D 不可中断睡眠 R 运行或就绪态 S 休眠状态 T 停止或被追踪

W 进入内存交换(从内核2.6开始无效) X 死掉的进程 Z 僵尸进程

< 优先级高的进程 N 优先级较低的进程

L 有些页被锁进内存 s 进程的领导者(在它之下有子进程)

getpid 查看当前进程的 id, getppid 查看父进程 id。

// getmyid.c
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>

int main() {
pid_t pid = getpid();
pid_t ppid = getppid();
printf("Process ID : %d\n", pid);
printf("Parent process ID : %d\n", ppid);
sleep(3);
return 0;
}
$ gcc getmyid.c -o getmyid
$ ./getmyid
Process ID:32740
Parent process ID: 32611

进程 32611号是终端 pts/1 中的 bash 进程,而这个进程恰恰又是 getmyid 进程的父进程,这不是巧合,因为我刚刚在 pts/1 终端中键入了​​./getmyid​​​ ,这时 pts/1 中的 bash 进程生出了进程 32740号进程 ​​./getmyid​​ 。

2 fork函数

pid_t fork(void);

返回值是 pid_t 类型,是 typedef 定义的32位有符号整型数。

调用一次,返回两次,父进程返回子进程id(>0),子进程返回0,失败返回-1。

// myfork.c
#include<unistd.h>
#include<stdio.h>
#include<string.h>


int main(){
char buf[] = "Hello, I'm father\n";
write(STDOUT_FILENO, buf, strlen(buf));
printf("before fork\n");

pid_t pid = fork();

if(!pid){
printf("I'm child %d; my father is %d\n", getpid(), getppid());
sleep(2);
}
else if(pid > 0){
printf("I'm father %d; my child is %d\n", getpid(), pid);
sleep(2);
}
else{
perror("fork");
return 1;
}
return 0;
}
$ gcc myfork.c -o myfork
$ ./myfork
Hello, I'm father
.before fork
I'm father 32884; my child is 32885
I'd child 32885; my father is 32884

它把结果重定向到了 tmp 文件中,打开你的 tmp 文件看看吧!

$ ./myfork > tmp
$ cat tmp
Hello, I'm father
before fork
I'm father 33384; my child is 33385
before fork
I'm child 33385; my father is 33384

tmp 文件里的 before fork 出现两次

原因:

当 printf 接收到字符串后,把字符串复制到一个 char 数组(缓冲区)中,当数组遇到了特定的字符,比如 ‘\n’ 字符,回车或者装满等等,就会立即把字符写到屏幕终端上。而把 printf 重定向到文件时,如果 printf 函数遇到 ‘\n’ 字符,并不会立即把字符写到文件里,这是 printf 函数将字符定向到屏幕和文件的重要区别。所以当 ./myfork > tmp 这个进程执行到 fork 时,printf 里的缓冲区数据还没来得及被刷新到 tmp 文件里,就被 fork 函数复制了,同时,printf 的缓冲区也被复制了一模一样的一份出来。

3 进程空间

3.1 进程地址互不影响

实验​:有个经典的 C 语言的例子,就是进程 A 中往地址 0x50000000 写入一个 int 类型的整数 100,然后在进程 B 的地址0x50000000 写入另一个 int 类型的整数 1000,最后两个进程再各自通过 printf 函数打印这两个地址保存的值,发现进程 A 仍然可以正常打印 100,而进程 B 正常打印出 1000。

在windows 系统下运行(因为 linux 不直接提供在指定地址上分配内存的函数)

  • 进程 A 的代码
#include <windows.h>
#include<stdio.h>

int main()
{
int* buf = (int*)0x50000000;
LPVOID res = VirtualAlloc((LPVOID)buf, sizeof(int), MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
if (res != buf) {
printf("ERROR!\n");
return 1;
}

*buf = 100;

printf("A = %d\n", *buf);

Sleep(3000);
return 0;
}
  • 进程 B 的代码
#include <windows.h>
#include<stdio.h>

int main()
{
int* buf = (int*)0x50000000;
LPVOID res = VirtualAlloc((LPVOID)buf, sizeof(int), MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
if (res != buf) {
printf("ERROR!\n");
return 1;
}

*buf = 1000;

printf("B = %d\n", *buf);

Sleep(3000);
return 0;
}

Linux系统编程(一) ----进程基础_子进程

结论:进程空间就是进程虚拟地址空间。不同进程的地址空间是隔离的,在相同的地址写不同的值,互不影响。

Linux系统编程(一) ----进程基础_#include_02

两进程在相同位置写,互不影响

3.2 虚拟地址到物理地址的映射

对于进程来说,属于进程地址是虚拟的。这中间有一步到物理地址的映射过程。

Linux系统编程(一) ----进程基础_父进程_03

物理地址图两进程虚拟地址映射

每个进程认为自己有 4GB 的虚拟地址,如果物理空间不够,系统把一部分不经常用的数据放硬盘上。

虚拟地址空间与物理地址之间的映射关系保存在页表中。

3.3 fork函数原理

1、创建一个子进程

2、把父进程的数据复制到新创建的子进程中。

Linux系统编程(一) ----进程基础_子进程_04

fork两步操作,1、创建进程,2、复制数据

为什么子进程的 fork函数返回 0?

fork 就是将进程的地址空间复制一份,复制完后,操作系统直接把它赋值为 0。

3.4 写时复制技术

Linux系统编程(一) ----进程基础_子进程_05

fork执行时,数据没有复制,读时共享 ,写时复制

Linux系统编程(一) ----进程基础_父进程_06

需要写土豆数据,复制一份

4 fork函数与文件共享

实进程空间被分割为用户空间和内核空间。对于32 位 linux 来说,从 0-3GB 的空间是用户空间,从 3GB - 4GB 是内核空间。对于一个进程来说,是绝对无法读写内核空间的。进程的用户空间是隔离的,而内核空间是共享的。

Linux系统编程(一) ----进程基础_#include_07

进程的用户空间和内核空间

进程所有打开的描述符,都会有记录。记录保存该进程的 PCB 结构体中(PCB位于内核空间),该结构体的成员 ​​struct file *flip[NR_OPEN]​​ 保存了所有打开的文件。

Linux系统编程(一) ----进程基础_#include_08

进程打开的文件

struct file 结构体中的 f_inode 并不是真的直接指向磁盘文件,这中间有若干步骤。

进程 fork 后,变成下面这个样子。struct file 中的 f_count 都会自增1。父子进程共享 struct file 结构(该结构位于内核空间)。struct file 称为文件表。

Linux系统编程(一) ----进程基础_#include_09

fork 后父进程和子进程之间对打开文件的共享

实验:在 fork 之前以写的方式创建了一个文件 test.txt。然后 fork 出的子进程立即向文件中写入"world",然后睡眠5秒。而父进程在 fork 后睡眠3秒后向 test.txt 写入 “hello”,并关闭描述符。子进程恢复后,又向 test.txt 文件中写入 "lalala"后关闭描述符,结束。

// forkwrite.c
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>

int main() {
int fd = open("test.txt", O_WRONLY | O_CREAT, 0664);
if (fd == -1) {
perror("open");
return 1;
}
printf("I'm father\n");
printf("before fork\n");

pid_t pid = fork();
if (pid > 0) {
sleep(3);
printf("I'm father; I'm writing test.txt...\n");
write(fd, "hello", 5);
close(fd);
}
else if (pid == 0) {
printf("I'm child; I'm writing test.txt...\n");
write(fd, "world", 5);
sleep(5);
write(fd, "lalala", 6);
close(fd);
}
else {
perror("fork");
return 1;
}
return 0;
}
$ ./forkwrite 
I'm father
before fork
I'm child; I'm writing test.txt...
I'm father; I'm writing test.txt...
$ cat test.txt
worldhellolalala

可以看到文件是共享的。

Linux系统编程(一) ----进程基础_子进程_10

fork 后父进程和子进程之间对打开文件的共享

除打开的文件外,父进程的很多其他性质也会被子进程共享,比如各种 ID 号、当前工作目录、根目录、资源的限制、信号屏蔽字、进程环境、文件打开执行时关闭标志、共享存储段。

5 exec 系列函数与进程空间结构

5.1 exec系列函数

exec 系列函数的目的:把本进程空间的代码和数据全部替换成你指定的数据,然后从新程序的入口点开始执行。该进程执行的程序完全代替为新程序,新程序从其main函数开始执行,因为调用exec并不创建新进程,所以前后ID并不改变。exec只是用磁盘上的一个新程序代替了当前进程中的正文段,数据段,堆段和栈段。这里只介绍其中一个execvp。

Linux系统编程(一) ----进程基础_#include_11

区别1:带p的取文件名做参数(filename中有/,视为路径,否则用PATH环境变量寻找可执行文件),不带p取路径名,最后一个取文件描述符

区别2:参数传递,l表示列表,v表示矢量vector

区别3:像新程序传递环境变量,以e结尾的传递环境变量参数,其他使用当前环境。

实验​:使用 execvp 启动 ls 命令

#include <unistd.h>
#include <string.h>
#include <stdio.h>

int main() {
char* argv[] = { "ls", "-l", NULL }; // 构造 vector,注意argv[0] 是占用参数
if (execvp("ls", argv) == -1) { // 替换代码段和数据段,并重新从 ls 入口点执行
perror("exec");
return 1;
}

return 0;
}

运行结果和使用 ​​ls -l​​ 命令一模一样。

函数名中字母含义

p:以filename作为参数,并且用PATH环境变量寻找可执行文件

l:表示 list,表示函数参数是可变参数,与v互斥

v:代表 vector,表示参数是数组。

e:表示函数需要传递环境变量参数,不使用当前环境。

Linux系统编程(一) ----进程基础_linux_12

exec 系列函数,只有 execve 才是真正的系统调用接口,其它的 5 个函数都是标准 C 函数库中的函数,最终都调用了 execve 这个函数。

5.2 进程空间结构

Linux系统编程(一) ----进程基础_linux_13

进程空间结构

exec 系列函数的本质就是替换代码段和数据段,然后从新的入口点重新执行。对应到图中,就是替换整个粉红色的区域。使用 execvp 函数本质上就是进程执行到 execvp 的时候,要执行文件的二进制文件读取出来,替换掉代码段和数据段,同时 execvp 函数会把 BSS 段重新清 0。

5.3 exec + fork

父进程正常运行,子进程执行别的命令。

有了 fork 的出现,exec 才真正体现出它的强大。咱们完全可以把 exec 放到子进程中去。

实验​:

// forkandexec.c
#include <unistd.h>
#include <string.h>
#include <stdio.h>

int main() {
char* argv[] = { "ls", "-l", NULL };
pid_t pid = fork();

if (pid > 0) {
printf("I'm father\n");
}
else if (pid == 0) {
printf("I'm child\n");
if (execvp("ls", argv) == -1) {
perror("exec");
return 1;
}
}
else {
perror("fork");
return 0;
}
return 0;
}
$ ./forkandexec 
I'm father
I'm child
$ 总用量 16
-rwxr-xr-x 1 skx skx 8488 925 10:51 forkandexec
-rw-r--r-- 1 skx skx 342 925 10:51 forkandexec.c

vfork 采用了类似读时共享的机制,但是其不保证写时复制,它产生的子进程和父进程共享进程空间,所以,如果在 vfork 后没有使用 exec 或者 _exit 函数,其行为将是未定义的。

6 wait和waitpid

6.1 僵尸进程

子进程运行结束,父进程还没有,就会产生僵尸进程。父进程还没结束,子进程先死了(发送 SIGCHILD信号给父进程),父进程对它视而不见。只要子进程状态发生改变都会给父进程发信号(SIGCHLD)。

实验:造几个僵尸进程

// mywait.c
#include <unistd.h>
#include <stdio.h>
#include <string.h>

int main() {
printf("before fork\n");

pid_t pid, n = 5;

// 父进程生出 5 个子进程
while (n--) {
pid = fork();
if (pid == 0) break;
else if (pid < 0) {
perror("fork");
return 1;
}
}

// 子进程打印一句话就死了。
if (pid == 0) {
printf("hello, I'm child %d; my father is %d\n", getpid(), getppid());
return 0;
}

// 父进程永远在这打印。
while (1) {
sleep(3);
printf("hello, I'm father %d\n", getpid());
}
return 0;
}
$ gcc mywait.c -o mywait
$ ./mywait
before fork
hello, I'm child 34140; my father is 34139
hello, I'm child 34141; my father is 34139
hello, I'm child 34144; my father is 34139
hello, I'm child 34143; my father is 34139
hello, I'm child 34142; my father is 34139
hello, I'm father 34139
hello, I'm father 34139
...

查看僵尸进程

$ ps -ef | grep mywait
skx 34139 32611 0 20:48 pts/0 00:00:00 ./mywait
skx 34140 34139 0 20:48 pts/0 00:00:00 [mywait] <defunct>
skx 34141 34139 0 20:48 pts/0 00:00:00 [mywait] <defunct>
skx 34142 34139 0 20:48 pts/0 00:00:00 [mywait] <defunct>
skx 34143 34139 0 20:48 pts/0 00:00:00 [mywait] <defunct>
skx 34144 34139 0 20:48 pts/0 00:00:00 [mywait] <defunct>
skx 34146 34090 0 20:48 pts/1 00:00:00 grep --color=auto mywait

可以看到,这里有 5 个进程,名字被加了方括号,后面还跟着 <defunct> 字样。

实际上,子进程在死的时候,通知了它父亲(发送 SIGCHILD信号给父进程)父进程未理睬。除非子进程的父亲也死了,这时候会有 init 进程来替代原来的父亲替这些子进程收尸。

6.2 wait清理僵尸进程

僵尸进程越来越多,最后就会造成资源耗尽。

wait 函数原型如下:

pid_t wait(int *status);

参数保存子进程退出通知码,返回 -1 表示没有子进程或者错误。否则返回子进程的进程 id 号。

// wipeoutzombie.c
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <sys/types.h>

int main() {
printf("before fork\n");

pid_t pid, n = 5;
while (n--) {
pid = fork();
if (pid == 0) break;
else if (pid < 0) {
perror("fork");
return 1;
}
}

if (pid == 0) {
printf("hello, I'm child %d; my father is %d\n", getpid(), getppid());
return 0;
}


while (1) {
sleep(3);
pid = wait(NULL); // 忽略子进程通知码
if (pid == -1) {
perror("wait");
sleep(10);
printf("I'm father %d; I have wiped out all zombies\n", getpid());
return 1;
}
printf("Hello, I'm father %d; child %d exit\n", getpid(), pid);
}
return 0;
}
$ gcc wipeoutzombie.c -o wipeoutzombie
$ ./wipeoutzombie
before fork
hello, I'm child 34212; my father is 34211
hello, I'm child 34216; my father is 34211
hello, I'm child 34214; my father is 34211
hello, I'm child 34213; my father is 34211
hello, I'm child 34215; my father is 34211
Hello, I'm father 34211; child 34212 exit
Hello, I'm father 34211; child 34213 exit
Hello, I'm father 34211; child 34214 exit
Hello, I'm father 34211; child 34215 exit
Hello, I'm father 34211; child 34216 exit
wait: No child processes
I'm father 34211; I have wiped out all zombies

6.3 waitpid

作用:waitpid 函数和 wait 函数都是用来获取子进程的状态。

pid_t waitpid(pid_t pid, int *status, int options);

调用 ​​wait(&status)​​​相当于调用 ​​waitpid(-1, &status, 0)​​。

参数 pid

pid > 0,表示 waitpid 只等待子进程 pid。

pid = 0,表示 waitpid 等待和当前调用 waitpid 一个组的所有子进程。

pid = -1,表示等待所有子进程。

pid < -1,表示等组 id=|pid|(绝对值 pid)的所有子进程。

参数status

status 描述的是子进程的退出状态。通过宏来判断。status 分成下面四类:

  • 1 进程正常退出:WIFEXITED(status)
  • 2 进程被信号终止:WIFSIGNALED(status)
  • 3 进程被暂停执行:WIFSTOPPED(status)
  • 4 进程被恢复执行:WIFCONTINUED(status)

1、如果 WIFEXITED(status)返回true,进程正常退出。可以使用WEXITSTATUS(status)来获取进程的退出码(main 函数的 return 值或者 exit 函数的参数值)。

2、如果WIFSIGNALED(status) 返回 true,进程被信号终止。可以通过宏 WTERMSIG(status) 获取子进程被哪种信号终止。

3、如果 WIFSTOPPED(status) 返回 true,进程收到信号暂停执行。可以通过宏 WSTOPSIG(status)获取子进程被哪个信号暂停。这时可以通过宏 WCOREDUMP(status) 返回 true 还是 false 来判断是否生生了 core 文件。

4、如果 WIFCONTINUED(status) 返回 true,进程收到信号SIGCONT恢复执行。

参数 options

参数 options 是可个组合选项。可以任意使用位或 | 运算符自由组合他们。如果 options 置空意味着 waitpid 函数是阻塞的。三个可组合选项如下:

  • WNOHANG (设置非阻塞,即使子进程全部正常运行,waitpid 也会立即返回 0)
  • WUNTRACED (可获取子进程暂停状态,也就是可获取 stopped 状态)
  • WCONTINUED (可获取子进程恢复执行的状态,也就是可获取 continued 状态)

返回值

waitpid 的返回值会有 > 0,,= 0,= -1。

  • = −1意味着没有子进程或者其它错误。
  • = 0,只有在打开了 WNOHANG 的情况下才会可能出现。如果子进程都是正常运行没有发生状态改变,它就会返回 0。
  • > 0,只要有任意一个子进程状态发生改变,比如停止,终止,恢复执行,waitpid 返回该子进程的 pid。

实验​:

// wipeoutzombie2.c
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <sys/types.h>

int main() {
printf("before fork\n");

// 生成 5 个子进程
pid_t pid, n = 5;
while (n--) {
pid = fork();
if (pid == 0) break;
else if (pid < 0) {
perror("fork");
return 1;
}
}

// 每个子进程一生出来就睡眠 4*n + 1 秒,不要问为什么是 4*n + 1,这只是我随便写的,只想保证每个子进程睡眠时间不一样。
if (pid == 0) {
sleep(4 * n + 1);
printf("hello, I'm child [ %d ]; my father is [ %d ]\n\n", getpid(), getppid());

// 这一句,我故意让这个子进程 core 掉,主要用来看效果。
if (4 * n + 1 == 5) *((int*)0) = 0;

// 子进程再退出的时候,返回它的睡眠时间
return 4 * n + 1;
}


int status = 0;
while (1) {
// 这里 waitpid 接收所有子进程状态,但是没有打开 WNOHANG 开关,同学们可以自行加上看看效果
pid = waitpid(-1, &status, WUNTRACED | WCONTINUED);

// 出错情况,这种一般是子进程全部结束了。
if (pid == -1) {
perror("wait");
sleep(5);
printf("I'm father [ %d ]; I have wiped out all zombies\n\n", getpid());
return 1;
}
// 这种只会在 WNOHANG 开关打开的情况下出现
else if (pid == 0) {
printf("Hello, I'm father; I'm waiting child\n\n");
}
// 这种是只要有一个子进程状态发生改变就会进来。注意 4 类情况,比较多。
else {

if (WIFEXITED(status)) {
printf("child [ %d ] <exited> with code [ %d ]\n\n", pid, WEXITSTATUS(status));
}
else if (WIFSIGNALED(status)) {
printf("child [ %d ] <terminated> abnormally, signal [ %d ]\n\n", pid, WTERMSIG(status));
#ifdef WCOREDUMP
if (WCOREDUMP(status)) {
printf("<core file generated> in child [ %d ]\n\n", pid);
}
#endif
}
else if (WIFSTOPPED(status)) {
printf("child [ %d ] <stopped>, signal [ %d ]\n\n", pid, WSTOPSIG(status));
}
else if (WIFCONTINUED(status)) {
printf("child [ %d ] <continued>\n\n", pid);
}
}

}
return 0;
}
$ gcc wipeoutzombie2.c -o wipeoutzombie2
$ ./wipeoutzombie2
before fork
hello, I'm child [ 34327 ]; my father is [ 34322 ]

child [ 34327 ] <exited> with code [ 1 ]

hello, I'm child [ 34326 ]; my father is [ 34322 ]

child [ 34326 ] <terminated> abnormally, signal [ 11 ]

<core file generated> in child [ 34326 ]

hello, I'm child [ 34325 ]; my father is [ 34322 ]

child [ 34325 ] <exited> with code [ 9 ]

hello, I'm child [ 34324 ]; my father is [ 34322 ]

child [ 34324 ] <exited> with code [ 13 ]

hello, I'm child [ 34323 ]; my father is [ 34322 ]

child [ 34323 ] <exited> with code [ 17 ]

wait: No child processes
I'm father [ 34322 ]; I have wiped out all zombies

6.4 core 文件

由于各种异常或者bug导致程序运行过程中异常退出或者中止,在满足一定条件下会产生一个叫做core的文件。

7 kill命令

kill -9 2008,表示发送信号 9 给进程 2008。

那么当你执行 ​​./wipeoutzombie2​​ 的时候,你可以通过 kill 发送信号给子进程,看看屏幕打印的情况的变化。