1.进程创建:
1.1fork函数
在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
\#include <unistd.h>
pid_t fork(void);
返回值:子进程中返回0,父进程返回子进程id,出错返回-1
进程调用fork,当控制转移到内核中的fork代码后,内核做:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表当中
- fork返回,开始调度器调度
🔥当准备返回时,上面三个工作都有了,父进程继续执行开始 return
,子进程也可能执行 fork
的返回值,然后就会得到两次返回。
具体我们可以下面这段代码:
int main( void )
{
pid_t pid;
printf("Before: pid is %d\n", getpid());
if ( (pid=fork()) == -1 )perror("fork()"),exit(1);
printf("After:pid is %d, fork return %d\n", getpid(), pid);
sleep(1);
return 0;
}
运行结果如下:
这里看到了三行输出,一行before,两行after。进程15256先打印before消息,然后它有打印after。另一个after
消息有15257打印的。注意到进程15257没有打印before,为什么呢?如下图所示
所以,fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器
决定
fork之后,子进程的代码从fork开始往后执行,那OS怎么知道从哪里开始执行?
eip程序计数器会出手
创建子进程的内核数据结构:
(struct task_struct + struct mm_struct + 页表)+ 代码继承父进程,数据以写时拷贝的方式来进行共享或者独立。
fork
之后创建一批结构,代码以共享的方式,数据以写时拷贝的方式,两个进程必须保证 “独立性”,做到互不影响。在这种共享机制下子进程或父进程任何一方挂掉,不会影响另一个进程。
1.2写时拷贝:
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副
本。具体见下图:
❓为什么需要写时拷贝呢?
- 有浪费空间之嫌:父进程的数据,子进程不一定全用;即便使用,也不一定全部写入。
- 最理想的情况,只有会被父子修改的数据,进行分离拷贝。不需要修改的数据,共享即可。但是从技术角度实现复杂。
- 如果
fork
的时候,就无脑拷贝数据给子进程,会增加fork
的成本(内存和时间)
我们返回去看上图,修改内容前后,代码是共享同一块的,但是数据是发生写时拷贝的,当修改后代码段会指向不同的物理内存
最终采用写时拷贝:只会拷贝父子修改的、变相的,就是拷贝数据的最小成本。拷贝的成本依旧存在。
写时拷贝实际上以一种 延迟拷贝策略,延迟拷贝最大的价值:只有真正使用的时候才给你拷。
其最大的意义在于,你想要,但是不立马使用的空间,先不给你,那么也就意味着可以先给别人。
反正拷贝的成本总是要有,早给你晚给你都是一样。万一我现在给你你又不用,那其实不很浪费
所以我选择暂时先不给你,等你什么时候要用什么时候再给。这就变相的提高了内存的使用情况。
举个例子
❓我们在C语言的时候发现char*类型(字符串)不可被修改
💡原因在于在页表项的位置的时候设置只读属性
写时拷贝:只有当子进程要进行修改的时候才给子进程分配空间,本质是一个资源筛选或者叫做按需申请资源的策略
1.3fork函数返回值
- 子进程返回0,
- 父进程返回的是子进程的pid。
1.4fork常规用法
一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子
进程来处理请求。
一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
1.5fork调用失败的原因
fork函数创建子进程也可能会失败,有以下两种情况:
- 系统中有太多的进程,内存空间不足,子进程创建失败。
- 实际用户的进程数超过了限制,子进程创建失败。
2.进程终止
进程退出场景
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止
崩溃的本质:进程因为某些原因,导致进程收到了来自操作系统的信号
return 进程的退出码反映结果是否正确,可以供用户进行进程退出健康状态的判断
int main()
{
return 0;
}
返回值为 0
,表示进程代码跑完,结果是否正确,我们用 0
表示成功,非 0
表示失败。
2.1进程退出码
我们都知道main函数是代码的入口,但实际上main函数只是用户级别代码的入口,main函数也是被其他函数调用的,例如在VS2013当中main函数就是被一个名为__tmainCRTStartup的函数所调用,而__tmainCRTStartup函数又是通过加载器被操作系统所调用的,也就是说main函数是间接性被操作系统所调用的。
既然main函数是间接性被操作系统所调用的,那么当main函数调用结束后就应该给操作系统返回相应的退出信息,而这个所谓的退出信息就是以退出码的形式作为main函数的返回值返回,我们一般以0表示代码成功执行完毕,以非0表示代码执行过程中出现错误,这就是为什么我们都在main函数的最后返回0的原因。
🔥我们可以利用 echo $?
来查看最近的进程的退出码,$?只会保留最近一次执行的进程的退出码
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main( void )
{
return 1;
}
我们再次执行后发现退出码变成0了
.我们发现这些错误码都是用数字来表示,我们来查看各种退出码的含义
C 语言当中有个的 string.h``
中有一个 strerror
接口,是最经典的、将错误码表述打印出来的接口,
#define EPERM 1 /* Operation not permitted */
#define ENOENT 2 /* No such file or directory */
#define ESRCH 3 /* No such process */
#define EINTR 4 /* Interrupted system call */
#define EIO 5 /* I/O error */
#define ENXIO 6 /* No such device or address */
#define E2BIG 7 /* Argument list too long */
#define ENOEXEC 8 /* Exec format error */
#define EBADF 9 /* Bad file number */
#define ECHILD 10 /* No child processes */
#define EAGAIN 11 /* Try again */
#define ENOMEM 12 /* Out of memory */
#define EACCES 13 /* Permission denied */
#define EFAULT 14 /* Bad address */
#define ENOTBLK 15 /* Block device required */
#define EBUSY 16 /* Device or resource busy */
#define EEXIST 17 /* File exists */
#define EXDEV 18 /* Cross-device link */
#define ENODEV 19 /* No such device */
#define ENOTDIR 20 /* Not a directory */
#define EISDIR 21 /* Is a directory */
#define EINVAL 22 /* Invalid argument */
#define ENFILE 23 /* File table overflow */
#define EMFILE 24 /* Too many open files */
#define ENOTTY 25 /* Not a typewriter */
#define ETXTBSY 26 /* Text file busy */
#define EFBIG 27 /* File too large */
#define ENOSPC 28 /* No space left on device */
#define ESPIPE 29 /* Illegal seek */
#define EROFS 30 /* Read-only file system */
#define EMLINK 31 /* Too many links */
#define EPIPE 32 /* Broken pipe */
#define EDOM 33 /* Math argument out of domain of func */
#define ERANGE 34 /* Math result not representable */
#define EDEADLK 35 /* Resource deadlock would occur */
#define ENAMETOOLONG 36 /* File name too long */
#define ENOLCK 37 /* No record locks available */
#define ENOSYS 38 /* Function not implemented */
#define ENOTEMPTY 39 /* Directory not empty */
#define ELOOP 40 /* Too many symbolic links encountered */
#define EWOULDBLOCK EAGAIN /* Operation would block */
#define ENOMSG 42 /* No message of desired type */
#define EIDRM 43 /* Identifier removed */
#define ECHRNG 44 /* Channel number out of range */
#define EL2NSYNC 45 /* Level 2 not synchronized */
#define EL3HLT 46 /* Level 3 halted */
#define EL3RST 47 /* Level 3 reset */
#define ELNRNG 48 /* Link number out of range */
#define EUNATCH 49 /* Protocol driver not attached */
#define ENOCSI 50 /* No CSI structure available */
#define EL2HLT 51 /* Level 2 halted */
#define EBADE 52 /* Invalid exchange */
#define EBADR 53 /* Invalid request descriptor */
#define EXFULL 54 /* Exchange full */
#define ENOANO 55 /* No anode */
#define EBADRQC 56 /* Invalid request code */
#define EBADSLT 57 /* Invalid slot */
#define EDEADLOCK EDEADLK
#define EBFONT 59 /* Bad font file format */
#define ENOSTR 60 /* Device not a stream */
#define ENODATA 61 /* No data available */
#define ETIME 62 /* Timer expired */
#define ENOSR 63 /* Out of streams resources */
#define ENONET 64 /* Machine is not on the network */
#define ENOPKG 65 /* Package not installed */
#define EREMOTE 66 /* Object is remote */
#define ENOLINK 67 /* Link has been severed */
#define EADV 68 /* Advertise error */
#define ESRMNT 69 /* Srmount error */
#define ECOMM 70 /* Communication error on send */
#define EPROTO 71 /* Protocol error */
#define EMULTIHOP 72 /* Multihop attempted */
#define EDOTDOT 73 /* RFS specific error */
#define EBADMSG 74 /* Not a data message */
#define EOVERFLOW 75 /* Value too large for defined data type */
#define ENOTUNIQ 76 /* Name not unique on network */
#define EBADFD 77 /* File descriptor in bad state */
#define EREMCHG 78 /* Remote address changed */
#define ELIBACC 79 /* Can not access a needed shared library */
#define ELIBBAD 80 /* Accessing a corrupted shared library */
#define ELIBSCN 81 /* .lib section in a.out corrupted */
#define ELIBMAX 82 /* Attempting to link in too many shared libraries */
#define ELIBEXEC 83 /* Cannot exec a shared library directly */
#define EILSEQ 84 /* Illegal byte sequence */
#define ERESTART 85 /* Interrupted system call should be restarted */
#define ESTRPIPE 86 /* Streams pipe error */
#define EUSERS 87 /* Too many users */
#define ENOTSOCK 88 /* Socket operation on non-socket */
#define EDESTADDRREQ 89 /* Destination address required */
#define EMSGSIZE 90 /* Message too long */
#define EPROTOTYPE 91 /* Protocol wrong type for socket */
#define ENOPROTOOPT 92 /* Protocol not available */
#define EPROTONOSUPPORT 93 /* Protocol not supported */
#define ESOCKTNOSUPPORT 94 /* Socket type not supported */
#define EOPNOTSUPP 95 /* Operation not supported on transport endpoint */
#define EPFNOSUPPORT 96 /* Protocol family not supported */
#define EAFNOSUPPORT 97 /* Address family not supported by protocol */
#define EADDRINUSE 98 /* Address already in use */
#define EADDRNOTAVAIL 99 /* Cannot assign requested address */
#define ENETDOWN 100 /* Network is down */
#define ENETUNREACH 101 /* Network is unreachable */
#define ENETRESET 102 /* Network dropped connection because of reset */
#define ECONNABORTED 103 /* Software caused connection abort */
#define ECONNRESET 104 /* Connection reset by peer */
#define ENOBUFS 105 /* No buffer space available */
#define EISCONN 106 /* Transport endpoint is already connected */
#define ENOTCONN 107 /* Transport endpoint is not connected */
#define ESHUTDOWN 108 /* Cannot send after transport endpoint shutdown */
#define ETOOMANYREFS 109 /* Too many references: cannot splice */
#define ETIMEDOUT 110 /* Connection timed out */
#define ECONNREFUSED 111 /* Connection refused */
#define EHOSTDOWN 112 /* Host is down */
#define EHOSTUNREACH 113 /* No route to host */
#define EALREADY 114 /* Operation already in progress */
#define EINPROGRESS 115 /* Operation now in progress */
#define ESTALE 116 /* Stale NFS file handle */
#define EUCLEAN 117 /* Structure needs cleaning */
#define ENOTNAM 118 /* Not a XENIX named type file */
#define ENAVAIL 119 /* No XENIX semaphores available */
#define EISNAM 120 /* Is a named type file */
#define EREMOTEIO 121 /* Remote I/O error */
#define EDQUOT 122 /* Quota exceeded */
#define ENOMEDIUM 123 /* Nomedium found */
#define EMEDIUMTYEP 124 /*Wrongmedium found */
#define ECANCELED 125 /* Operation Canceled */
#define ENOKEY 126 /* Required key not available */
#define EKEYEXPIRED 127 /* Key has expired */
#define EKEYREVOKED 128 /* Key has been revoked */
#define EKEYREJECTED 129 /* Key was rejected by service */
#define EOWNERDEAD 130 /* Owner died */
#define ENOTRECOVERABLE 131 /* State not recoverable */
#define ERFKILL 132 /* Operation not possible due to RF-kill */
#define EHWPOISON 133 /* Memory page has hardware error */
其中,0
表示 success,1 表示权限不允许,2 找不到文件或目录
2.2进程终止的方式
exit是C库的接口,_exit是系统接口,
_exit函数
#include <unistd.h>
void _exit(int status);
参数:status 定义了进程的终止状态,父进程通过wait来获取该值
说明:虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现返回值
是255。
exit函数
#include <unistd.h>
void exit(int status);
exit最后也会调用_exit, 但在调用_exit之前,还做了其他工作:
- 执行用户通过 atexit或on_exit定义的清理函数。
- 关闭所有打开的流,所有的缓存数据均被写入
- 调用_exit
验证执行_exit之前会冲刷缓冲区
3.进程等待
3.1进程等待必要性
-
之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
-
另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法
杀死一个已经死去的进程。
-
最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,
或者是否正常退出。
-
父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
3.2进程等待的方法
如何获取status
wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
如果传递NULL,表示不关心子进程的退出状态信息。
否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特
位):
在status的低16比特位当中,高8位表示进程的退出状态,即退出码。进程若是被信号所杀,则低7位表示终止信号,而第8位比特位是core dump标志。
我们通过一系列位操作,就可以根据status得到进程的退出码和退出信号。
exitCode = (status >> 8) & 0xFF; //退出码
exitSignal = status & 0x7F; //退出信号
对于此,系统当中提供了两个宏来获取退出码和退出信号。
- WIFEXITED(status):用于查看进程是否是正常退出,本质是检查是否收到信号。
- WEXITSTATUS(status):用于获取进程的退出码。
wait方法
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
父进程等待子进程
这里子进程会跑5s,父进程一进来就是10s休息,也就是未来会有有5s子进程已经退了,父进程还在,这里子进程的状态应该是Z,然后5s后父进程开始回收,在进入5s,我们会看到第二个现象,子进程要消失,z状态要退出来了,父进程在跑,再过5s父进程退出来
waitpid方法
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
当正常返回的时候waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
pid:
Pid=-1,等待任一个子进程。与wait等效。
Pid>0.等待其进程ID与pid相等的子进程。
status:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
options:(阻塞,后面会讲)
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进
程的ID
如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退
出信息。
如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
如果不存在该子进程,则立即出错返回。
所以第二个参数要获取两个整数,不要当做一个完整的整数,而应该看做位图
我们可以通过kill -l
来观察所有的信号,我们发现,没有0号信号
我们来查看一下退出状态和退出信号
❓请问父进程是如何获取子进程的退出信息的呢?💡通过子进程的pcb信息
说明:父进程等待子进程,子进程也会执行自己的代码。当子进程执行了 return/exit 退出后,子进程会将自己的退出码信息写入自己的进程控制块 () 中。子进程退出了,代码可以释放,子进程退出后变成 Z 状态,其本质上就是将自己的 task_struct 维护起来(代码可以释放,但是 task_struct 必须维护)。所谓的 wait/waitpid 的退出信息,实际上就是从子进程的 task_struct 中拿出来的,即 从子进程的 task_struct 中拿出子进程退出的退出码。
所以,我们的父进程在等待子进程死亡,等子进程一死,就直接把子进程的退出码信息拷贝过去,通过 wait/waitpid 传进来的参数后,父进程就拿到了子进程的退出结果。即 子进程会将自己的退出信息写入 task_struct 。
4.进程阻塞
❓父进程在wait的时候,如果子进程没有退出,父进程在干嘛?
💡父进程只能一直在调用waitpid进行等待——阻塞等待
子进程的task_struct里面有一个parent指针,子进程一旦退出,通过这种指针逆向找回去将父进程的状态从S变R
4.1轮询检测
所谓的阻塞,其实就是挂起。在上层表现来看,就是进程卡住了(比如 scanf,cin 等)。
而非阻塞式等待是 “巧等”,会做些自己的事,而不是一屁股做那傻等!
多次调用非阻塞接口,这个过程我们称之为 轮询检测 (Polling)。
我们上一章中讲解 waitpid 时,举的例子都是 阻塞式 的等待。
如果我们想 非阻塞式 的等,我们可以设置 options 选项为 WNOHANG
。
这样一来,等待的子进程若是没有结束,那么waitpid函数将直接返回0,不予以等待。而等待的子进程若是正常结束,则返回该子进程的pid。
4.2基于非阻塞的轮询等待
例如,父进程可以隔一段时间调用一次waitpid函数,若是等待的子进程尚未退出,则父进程可以先去做一些其他事,过一段时间再调用waitpid函数读取子进程的退出信息。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if (id == 0){
//child
int count = 3;
while (count--){
printf("child do something...PID:%d, PPID:%d\n", getpid(), getppid());
sleep(3);
}
exit(0);
}
//father
while (1){
int status = 0;
pid_t ret = waitpid(id, &status, WNOHANG);
if (ret > 0){
printf("wait child success...\n");
printf("exit code:%d\n", WEXITSTATUS(status));
break;
}
else if (ret == 0){
printf("father do other things...\n");
sleep(1);
}
else{
printf("waitpid error...\n");
break;
}
}
return 0;
}
运行结果就是,父进程每隔一段时间就去查看子进程是否退出,若未退出,则父进程先去忙自己的事情,过一段时间再来查看,直到子进程退出后读取子进程的退出信息。
5.进程的程序替换
创建子进程的目的是什么?就是为了让子进程帮我执行待定的任务
- 让子进程执行父进程的一部分代码
- 如果子进程执行一个全新的程序代码?可以进行进程的程序替换
我们如何让子进程执行一个新的代码呢?
之前我们通过写时拷贝,让子进程和父进程在数据上互相解耦,保证独立性。如果想让子进程和父进程彻底分开,让子进程彻彻底底地执行一个全新的程序,我们就需要 进程的程序替换。
若想让子进程执行另一个程序,往往需要调用一种exec函数。
当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,并从新程序的启动例程开始执行。
5.1程序替换原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数
以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动
例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
- 将磁盘中的内存,加载入内存结构。
- 重新建立页表映射,设执行程序替换,就重新建立谁的映射(下图为子进程建立)。
- 效果:让父进程和子进程彻底分离,并让子进程执行一个全新的程序!
这个过程有没有创建新的进程呢?没有!根本就没有创建新的进程!
因为子进程的内核数据结构根本没变,只是重新建立了虚拟的物理地址之间的映射关系罢了。
内核数据结构没有发生任何变化! 包括子进程的 , 都不变,说明压根没有创建新进程。exec函数
5.2替换函数execl簇
其实有六种以exec开头的函数,统称exec函数:
#include <unistd.h>`
int execl(const char *path, const char *arg, …);
int execlp(const char *file, const char *arg, …);
int execle(const char *path, const char *arg, …,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[]);
见见猪跑execl函数:
int execl(const char* path, const char& arg, ...);
- 它的第一个参数是 path,属于路径。
- 参数 const char* arg, … 中的 … 表示可变参数,命令行怎么写(ls, -l, -a) 这个参数就怎么填。ls, -l, -a 最后必须以 NULL 结尾,表示 “如何执行程序的” 参数传递完毕
ls就是一个可执行程序,让一个进程去执行一个在磁盘的程序
站在程序的角度:
它就是被动的加载到内存,这个程序被加载了,那以后这些函数被称为加载器
既然我自己写的代码能加载新的程序,那么操作系统呢?
❓当创建进程的时候,先有进程的数据结构,还是先加载代码和数据?
💡先有数据结构,再把外部的代码弄进去,把这个行为叫做加载
❓为什么上述测试代码我们只看到begin没有看到end呢?
💡执行程序替换,新的代码和数据就被加载了,后续的代码属于老代码,直接被替换了,没有机会执行了
程序替换是整体替换,不能局部替换
程序替换只会影响调用进程,进程具有独立性!
今天子进程加载新程序的时候,是需要进行程序替换,发生写时拷贝(子进程执行的可是全新的程序,新的代码!写时拷贝在代码区也可以发生!)
❓execl是函数吗?函数调用可能会失败吗?
💡是函数,可能会失败,失败了就不会调用新程序运行就会执行后面的程序
execl:如果替换成功,不会有返回值,如果替换失败,一定有返回值——如果失败了,必定返回——只要有返回值,就失败了
因此不用对该函数进行返回值判断,只要继续向后运行一定是失败的
execv函数
int execv(const char *path, char *const argv[]);
这里的v代表的是vector,这里传参要传一个数组
execlp函数
int execlp(const char *file, const char *arg, ...);
带p的意义:当我们执行指定程序的时候,只需要指定程序名即可,系统会自动在环境变量path中尽显查找
❓这两个ls一样吗?💡不一样,第一个参数是 “供系统去找你是谁的”,后面的一坨代表的是 “你想怎么去执行它”
exevp函数
int execvp(const char *file, char *const argv[]);
这个其实就是前面两个的结合
execle函数
int execle(const char *path, const char *arg, ..., char * const envp[]);
envp是自定义的环境变量
execvpe函数
int execvpe(const char* file, char* const argv[], char* const envp[]);
事实上,只有execve才是真正的系统调用,其它五个函数最终都是调用的execve,所以execve在man手册的第2节,而其它五个函数在man手册的第3节,也就是说其他五个函数实际上是对系统调用execve进行了封装,以满足不同用户的不同调用场景的。
char *file, const char *arg, …);