目录
1、进程的概念
2、进程与线程的区别、进程与程序的区别
2.1 进程与线程的区别
2.2 进程与程序的区别
3、进程相关 shell 命令
3.1 ps
3.3.1 参数说明
3.3.2 结果说明
3.2 pidof
3.3 pstree
3.4 top
3.5 kill
4、进程相关函数
4.1 fork
4.1.1 fork的函数原型
4.1.2 父子进程之间的继承
4.1.3 父子进程之间的区别
4.1.4 vfork
4.1.5 实例代码
4.2 getpid / getppid
4.3 exec函数族
4.3.1 什么是exec函数族
4.3.2 exec函数族函数原型
4.3.3 函数参数说明:
4.3.4 函数记忆方法
4.3.5 实例代码
4.4 _exit / exit
4.4.1 _exit:系统调用
4.4.2 exit():库函数
4.5 wait / waitpid
4.5.1 wait 函数原型
4.5.2 waitpid 函数原型
4.5.3 函数正常退出方式
4.5.4 函数异常退出方式
4.5.5 WEXITSTATUS(wstatus):提取进程退出传递的状态值
4.5.6 WIFEXITED(wstatus):判断子进程是否正常退出
4.5.7 关于参数status
4.5.8 实例代码
5、Linux中的特殊进程
5.1 孤儿进程(orphan)
5.1.1 孤儿进程是什么
5.1.2 实例代码
5.2 僵尸进程(zombie)
5.2.1 僵尸进程是什么
5.2.2 实例代码
5.2.3 如何回收僵尸进程
5.3 守护进程/幽灵进程(daemon)
5.3.1 什么是守护进程
5.3.2 查看守护进程 ps axj
5.3.3 创建守护进程
5.3.4 实例代码
1、进程的概念
1、进程是一个独立的可调度的任务。
2、进程是一个程序的一次执行的过程。
3、进程在被调度的时候,系统会自动分配和释放各种资源(时间片,cpu资源,进程调度块,内存资源等等)。
4、进程是一个抽象的概念。
2、进程与线程的区别、进程与程序的区别
2.1 进程与线程的区别
(1)一个线程从属于一个进程;一个进程可以包含多个线程。
(2)一个线程挂掉,对应的进程挂掉;一个进程挂掉,不会影响其他进程。
(3)进程是系统资源调度的最小单位;线程CPU调度的最小单位。
(4)进程系统开销显著大于线程开销;线程需要的系统资源更少。
(5)进程在执行时拥有独立的内存单元,多个线程共享进程的内存,如代码段、数据段、扩展段;但每个线程拥有自己的栈段和寄存器组。
(6)进程切换时需要刷新TLB并获取新的地址空间,然后切换硬件上下文和内核栈,线程切换时只需要切换硬件上下文和内核栈。
(7)通信方式不一样。
(8)进程适应于多核、多机分布;线程适用于多核
2.2 进程与程序的区别
程序是静态的,它是一些保存在磁盘上的指令的有序集合,没有任何执行的概念
进程是一个动态的概念,它是程序执行的过程,包括创建、调度和消亡
3、进程相关 shell 命令
3.1 ps
3.3.1 参数说明
-e 显示所有进程。
-f 全格式。
-h 不显示标题。
-l 长格式。
-w 宽输出。
-a 显示终端上的所有进程,包括其他用户的进程。
-r 只显示正在运行的进程。
-u 以用户为主的格式来显示程序状况。
-x 显示所有程序,不以终端机来区分。
3.3.2 结果说明
UID: 程序被该UID 所拥有,指的是用户 ID
PID: 就是这个程序的ID
PPID: PID的上级父进程的 ID
C: CPU使用的资源百分比
STIME: 系统启动时间
TTY: 登入者的终端机位置
TIME: 使用掉的CPU 时间
CMD: 所下达的指令为何STAT: 进程状态
D 不能被中断的阻塞态;
R 运行状态
S 休眠状态
T 挂起态
t 被追踪状态
X 死亡态,死亡是一瞬间的,不能被捕获到。
Z 僵尸状态,进程已经死亡,但是没有被回收。< 高优先级
N 低优先级
L 有些页被锁进内存。
s 会话组组长,有子进程的进程
l 多线程
+ 运行在前端的进程
3.2 pidof
根据程序名获取PID号
pidof a.out
3.3 pstree
显示进程关系树
3.4 top
实时显示进程的状态
top -d 秒数
top -p pid
3.5 kill
杀死进程
kill -9 pid 根据pid杀死进程
kill -9 进程名字 根据指定名字,杀死进程
4、进程相关函数
4.1 fork
4.1.1 fork的函数原型
pid_t fork(void);
/*
头文件:
#include <sys/types.h>
#include <unistd.h>
功能:创建一个进程
返回值: >0, 父进程中返回,创建出来的那个子进程的PID号
=0, 子进程中,返回0;
=-1, 父进程返回-1,同时更新错误码。此时没有成功创建出子进程。
*/
4.1.2 父子进程之间的继承
使用 fork 函数得到的子进程从父进程的继承了整个进程的地址空间,包括:
进程上下文、进程堆栈、内存信息、打开的文件描述符、信号控制设置、进程优先级、进程组号、当前工作目录、根目录、资源限制、控制终端等。
子进程会拷贝父进程的虚拟地址空间。子进程会拷贝父进程的数据段,正文段,栈。
4.1.3 父子进程之间的区别
1 、父进程设置的锁,子进程不继承
2 、各自的进程 ID 和父进程 ID 不同
3 、子进程的未决告警被清除
4 、子进程的未决信号集设置为空集5、父进程会拷贝一份资源给子进程,父子进程的资源是一致的,但是子进程不会运行创建它的那个fork函数以及fork以上的代码
6、fork后的父子进程哪个先运行不确定,只要看cpu调度机制以及时间片;
4.1.4 vfork
1、由于 fork 完整地拷贝了父进程的整个地址空间,因此执行速度是比较慢的。为了提高效率,Unix 系统设计者创建了 vfork。
2、vfork 也创建新进程,但不产生父进程的副本 。
3、它通过允许父子进程可访问相同物理内存从而伪装了对进程地址空间的真实拷贝,当子进程需要改变内存中数据时才拷贝父进程。
4、这 就是著名的 "写操作时拷贝"(copy on write) 技术
4.1.5 实例代码
int main()
{
pid_t pid;
if ((pid = fork()) == -1) {
perror("fork");
return -1;
} else if (pid == 0) {
/* this is child process */
printf("The return value is %d In child process!! My PID is %d, My PPID is %d\n", pid,getpid(), getppid());
} else {
/* this is parent */
printf("The return value is %d In parent process!! My PID is %d, My PPID is %d\n", pid,getpid(), getppid());
}
return 0;
}
4.2 getpid / getppid
pid_t getpid(void);
pid_t getppid(void);
/*
头文件:
#include <sys/types.h>
#include <unistd.h>
功能:获取当前进程/父进程的pid号
返回值:当前进程的pid / 父进程的pid
*/
4.3 exec函数族
4.3.1 什么是exec函数族
exec 函数族提供了一种在进程中启动另一个程序执行的方法。它可以根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段。
在执行完之后原调用进程的内容除了进程号外其他全部都被替换了。
可执行文件既可以是二进制文件也可以是任何 Linux 下可执行的脚本文件。
当进程认为自己不能再为系统和用户做出任何贡献了时就可以调用 exec 函数,让自己执行新的程序。
如果某个进程想同时执行另一个程序,它就可以调用fork 函数创建子进程,然后在子进程中调用任何一个exec 函数。这样看起来就好像通过执行应用程序而产生了一个新进程一样。
4.3.2 exec函数族函数原型
头文件 | #include <unistd.h> |
函数原型 | int execl(const char * path,const char * arg,…); |
int execle(const char * path,const char * arg,char * const envp[]); | |
int execlp(const char * file,const char * arg,…); | |
int execv(const char * path,char * const argv[]); | |
int execve(const char * path,char * const argv[],char * const envp[]); | |
int execvp(const char * file,char * const argv[]); | |
返回值 | -1代表调用exec失败,无返回代表调用成功 |
4.3.3 函数参数说明:
path :可执行文件的路径名字
arg:可执行程序所带的参数,第一个参数为可执行文件名字,没有带路径且arg必须以NULL结束。
file:如果参数file中包含/,则就将其视为路径名,否则就按PATH环境变量,在它所指定的各目录中搜寻可执行文件。
4.3.4 函数记忆方法
字符 | 说明 |
l | 使用参数列表 |
v | 应该先构造一个指向各参数的指针数组,然后将该数组的地址作为这些函数的参数。 |
p | 使用文件名,并从 PATH 环境寻找可执行文件 |
e | 多了 envp[] 数组,使用新的环境变量代替调用进程的环境变量 |
4.3.5 实例代码
int main(int argc,char **argv)
{
pid_t pid;
printf("PID = %d\n",getpid());
pid=fork();
if(pid==0)
{
execl("/bin/ls","ls","-l",NULL);
execl("/bin/ls",“ls”,NULL,NULL);
execlp("ls","ls","-al","/",NULL);
char *argv[] = {"ls","-l",NULL};
execv("/bin/ls",argv);
execvp("ls",argv);
sleep(10);
} else if(pid!=-1) {
//父进程
printf("\nParrent porcess,PID = %d\n",getpid());
} else {
printf("error fork() child proess!");
}
return 0 ;
}
4.4 _exit / exit
4.4.1 _exit:系统调用
void _exit(int status);
/*
头文件:
#include <unistd.h>
功能:终止进程,清除进程使用的内容空间;直接销毁缓冲区,不会刷新缓冲区;
参数:
@status:可以利用这个参数传递进程终止的状态值,可以输入任意整型;
这个参数会被wait/waitpid函数接收走;
*/
4.4.2 exit():库函数
void exit(int status);
/*
功能:终止进程,并刷新缓冲区;
头文件:
#include <stdlib.h>
参数:
@status:可以利用这个参数传递进程终止的状态值,可以输入任意整型;
这个参数会被wait/waitpid函数接收走;
*/
4.5 wait / waitpid
4.5.1 wait 函数原型
pid_t wait(int *wstatus);
/*
功能:阻塞函数,等待子进程退出,并回收子进程的资源;
头文件:
#include <sys/types.h>
#include <sys/wait.h>
参数:
@wstatus:获取子进程的退出状态:接收_exit/exit中的status;
如果不想接收,填NULL;
返回值:
成功,返回退出的子进程的pid;
失败,返回-1,更新errno;
如果进程没有创建子进程,调用wait函数不阻塞吗
1. 阻塞等待子进程退出,并回收子进程资源;
2. 如果没有子进程,父进程不等待,函数直接返回;
*/
4.5.2 waitpid 函数原型
pid_t waitpid(pid_t pid, int *wstatus, int options);
/*
功能:等待指定进程退出,并回收指定进程的资源。
注意:只能是父进程回收子进程,子进程不能回收父进程.
头文件:
#include <sys/types.h>
#include <sys/wait.h>
参数:
@pid:;
<-1 回收指定进程组下的任意一个子进程。指定的进程组pgid == pid的绝对值。
-1 等待当前进程下的任意一个子进程。
0 回收当前进程组下的任意一个子进程。
>0 回收指定子进程的资源,子进程的id == pid;
@wstatus:获取子进程的退出状态:接收_exit/exit中的status;
如果不想接收,填NULL;
@options:
0:阻塞方式回收,如果指定进程没有退出,当前函数阻塞等待;
WNOHANG:非阻塞方式回收,就算指定进程没有退出,当前函数也是不阻塞的,
立即返回,并执行后面代码;
返回值:
成功,阻塞方式回收:返回被回收的进程的pid;
非阻塞方式回收:-->如果指定进程退出,则返回被回收的进程的pid;
-->如果指定进程没有退出,但是函数运行成功,则返回0;
失败,返回-1,更新errno;
例子:
//阻塞方式回收,若指定进程没有退出,则当前函数阻塞
waitpid(pid, NULL, 0);
//非阻塞方式,就算指定进程没有退出,当前函数不阻塞;
waitpid(pid, NULL, WNOHANG);
*/
4.5.3 函数正常退出方式
1、Mian函数调用return
2、进程调用exit(),标准c库
3、进程调用_exit()或者——Exit(),属于系统调用
4、进程最后一个线程返回
5、最后一个线程调用pthread_exit
4.5.4 函数异常退出方式
1、调用abort
2、当进程收到某些信号时候,如ctrl+C
3、最后一个线程对取消(cancellation),请求作出响应
4.5.5 WEXITSTATUS(wstatus):提取进程退出传递的状态值
int status = WEXITSTATUS(wstatus);
4.5.6 WIFEXITED(wstatus):判断子进程是否正常退出
返回1,子进程正常退出。
返回0,子进程异常退出
4.5.7 关于参数status
此处为引用自以下这篇博客
进程(五)—— 进程退出、进程等待(waitpid函数)_程序退出函数_仲夏夜之梦~的博客-CSDN博客
status不能简单的当作整型来看,要从二进制的角度来看,32位下,整型转化为二进制有32个bit位但是我们仅关注低16位
(1) 正常退出时
进程正常退出时,子进程会返回退出码,即退出状态,8~15位记录着正常退出时的退出码,既然是正常退出就不会收到中止信号,所以0~7位都是 0
(2) 异常退出时
进程异常退出时,一般会收到一个中止进程的信号,而且不会执行到return 这句,所以自然就没有退出码,为了知道发生了何种异常,我们使用低 7 位,也就是 0~6 来记录 “中止信号”
注意:core dump是指在进程异常退出的时候,进程会把用户空间的内存数据保存到磁盘文件,文件名为core
4.5.8 实例代码
int main(int argc, const char *argv[])
{
pid_t pid = fork();
if(pid > 0)
{
//父进程运行
int wstatus;
//回收子进程pid参数可以填-1,也可以填子进程的id
//阻塞方式回收,若制定进程没有退出,则当前函数阻塞
pid_t w_pid = waitpid(pid, &wstatus, 0);
//非阻塞方式,就算制定进程没有退出,当前函数不阻塞;
pid_t w_pid = waitpid(pid, NULL, WNOHANG);
//获取子进程退出状态值
int status = WEXITSTATUS(wstatus);
printf("wstatus = %d %d\n", wstatus, status);
//判断子进程是否正常退出
int ret = WIFEXITED(wstatus);
if(ret)
printf("子进程正常退出\n");
else
printf("子进程异常退出\n");
while(1) {
printf("parent %d %d\n", getpid(), pid);
sleep(1);
}
} else if(0 == pid) {
//子进程运行
while(1) {
printf("child %d %d\n", getppid(), getpid());
sleep(1);
}
} else {
perror("fork");
return -1;
}
return 0;
}
5、Linux中的特殊进程
5.1 孤儿进程(orphan)
5.1.1 孤儿进程是什么
父进程如果不等待子进程退出,在子进程结束前就了结束了自己的“生命”,此时子进程就叫做孤儿进程。
Linux避免系统存在过多孤儿进程,init进程收留孤儿进程,变成孤儿进程的父进程[ init进程(pid=1)是系统初始化进程 ]。init 进程会自动清理所有它继承的僵尸进程。
孤儿进程不能用ctrl + c 退出,但是可以被kill -9杀死
5.1.2 实例代码
int main(int argc, const char *argv[])
{
//创建一个子进程,让父进程退出,子进程不退出
pid_t pid = fork();
if(pid > 0) {
//父进程运行
} else if(0 == pid) {
//子进程
while(1) {
printf("this is child %d %d\n", getppid(), getpid());
sleep(1);
}
} else {
perror("fork");
return -1;
}
return 0;
}
5.2 僵尸进程(zombie)
5.2.1 僵尸进程是什么
子进程退出,父进程没有退出,且子进程的资源没有被回收。这时候子进程就是僵尸进程。
5.2.2 实例代码
int main(int argc, const char *argv[])
{
//父进程没有退出,子进程退出,且父进程没有回收子进程的资源。
pid_t pid = fork();
if(pid > 0) {
//父进程运行
while(1) {
printf("this is parent\n");
sleep(1);
}
} else if(0 == pid) {
//子进程运行
} else {
perror("fork");
return -1;
}
return 0;
}
5.2.3 如何回收僵尸进程
第一种方式,使用waitpid函数让父进程停下来等待子进程退出,并回收子进程,此时就能解决僵尸进程。
第二种方式,子进程在退出的时候,会给父进程发送一个SIGCHLD信号,这个时候我们可以使用处理信号的知识来解决这个问题。
具体解决可以借鉴以下博客
解决僵尸进程的两种方式(重温waitpid函数、了解17号信号SIGCHLD)_僵尸进程怎么处理_仲夏夜之梦~的博客-CSDN博客
5.3 守护进程/幽灵进程(daemon)
5.3.1 什么是守护进程
1、守护进程脱离终端,并且在后台运行
2、守护进程在执行中不会将任何信息显示在终端上。并且不会被任何终端产生的终端信息打断
3、守护进程独立于控制终端,并且周期性执行某个任务或等待处理某些事情
4、大多数服务器都是由守护进程实现的,比如:像我们的tftp,samba,nfs等相关服务
5.3.2 查看守护进程 ps axj
所有的守护进程都有以下特点:
1、所有的守护进程都是以超级用户启动的(UID为0)
2、没有控制终端(TTY为?)
3、终端进程组ID为-1(TPGID表示终端进程组ID,该值表示与控制终端相关的前台进程组,如果未和任何终端相关,其值为-1
5.3.3 创建守护进程
(1)创建孤儿进程
由于守护进程是脱离控制终端的,因此,完成第一步后就会在shell终端里造成一程序已经运行完毕的假象。之后的所有后续工作都在子进程中完成,而用户在shell终端里则可以执行其他的命令,从而在形式上做到了与控制终端的脱离。
由于父进程已经先于子进程退出,会造成子进程没有父进程,从而变成一个孤儿进程。在Linux中,每当系统发现一个孤儿进程,就会自动由1号进程收养。原先的子进程就会变成init进程的子进程。
(2)创建新的会话
使子进程完全独立,子进程创建新的会话,不在依附于父进程的会话组,需要调用 setid() 函数
setid()函数主要有以下三个作用
让进程摆脱原会话的控制。
让进程摆脱原进程组的控制。
让进程摆脱原控制终端的控制。
由于在调用fork()函数时,子进程全盘复制了父进程的会话期、进程组和控制终端等,虽然父进程退出了,但原先的会话期、进程组和控制终端等并没有改变,因此,还不是真正意义上的独立。而setsid()函数能够使进程完全独立出来,从而脱离所有其他进程和终端的控制。
setid()函数原型
pid_t setsid(void);
/*
功能:创建会话组;
头文件:
#include <sys/types.h>
#include <unistd.h>
返回值:
成功,创建会话组的sid;
失败,返回(pid_t) -1,更新errno;
1.创建新的会话,成为会话组组长;
2.创建新的进程组,成为进程组组长;
*/
(3)修改当前孤儿进程的运行目录为不可卸载的文件系统
这一步也是必要的步骤。使用fork()创建的子进程继承了父进程的当前工作目录。
由于在进程运行过程中,当前目录所在的文件系统(如“/mnt/usb”等)是不能卸载的,这对以后的使用会造成诸多的麻烦(如系统由于某种原因要进入单用户模式)。
因此,通常的做法是让“/”作为守护进程的当前工作目录,这样就可以避免上述问题。当然,如有特殊需要,也可以把当前工作目录换成其他的路径,如/tmp。改变工作目录的常见函数是chdir()。
chdir()函数原型
int chdir(const char *path);
/*
头文件
#include <unistd.h>
功能:修改到指定目录下
参数:
@path:指定修改到哪个目录下
例如:
chdir("/");
chdir("/temp");
*/
(4)重设文件权限掩码:一般清0
文件权限掩码是指屏蔽掉文件权限中的对应位。
例如,有一个文件权限掩码是050,它就屏蔽了文件组拥有者的可读与可执行权限。由于使用fork()函数新建的子进程继承了父进程的文件权限掩码,这就给该子进程使用文件带来了诸多的麻烦。
因此,把文件权限掩码设置为0,可以大大增强该守护进程的灵活性。设置文件权限掩码的函数是umask()。在这里,通常的使用方法为umask(0)。即赋予最大的能力。
umask(0);
(5)关闭所有文件描述符
同文件权限掩码一样,用fork()函数新建的子进程会从父进程那里继承一些已经打开的文件。这些被打开的文件可能永远不会被守护进程读或写,但它们一样消耗系统资源,而且可能导致所在的文件系统无法被卸载。
在上面的第(2)步之后,守护进程已经与所属的控制终端失去了联系,因此,从终端输入的字符不可能达到守护进程,守护进程中用常规方法(如printf())输出的字符也不可能在终端上显示出来。
所以,文件描述符为0、1和2的3个文件(常说的输入、输出和报错这3个文件)已经失去了存在的价值,也应被关闭。
for(i=0; i<1024; i++)
close(i);
此处为什么是1024呢?
文件打开的文件数量是有限制的,我们通过 ulimit -a,即查看当前所有的资源限制。其中就包括了打开的最大文件数。
5.3.4 实例代码
int main(int argc, const char *argv[])
{
//创建孤儿进程
pid_t pid = fork();
if(0 == pid) {
//创建新的会话
pid_t sid = setsid();
//printf("sid = %d %d\n", sid, getpid());
//修改运行目录为不可卸载的文件系统
chdir("/");
//文件权限掩码
umask(0);
//关闭所有文件描述符
int i = 0;
for(i=0; i<1024; i++) {
close(i);
}
while(1) {
//周期性执行的功能代码
sleep(1);
}
} else if(pid > 0) {
//父进程
} else {
perror("fork");
return -1;
}
return 0;
}