一、进程
(一)概念
1. 进程
程序的一次执行过程就会产生一个进程。
进程是分配资源的最小单位(0-3G)。
进程就是一个正在执行的任务。
进程是一个动态的过程,它有生命周期:随着程序的运行开始,随着程序结束而消亡。
每个进程都有自己的独立的运行的空间,比如
每个进程都有自己的文件描述符,每个进程都拥有自己的缓冲区。
只要用户执行了一个程序,在内核空间就会创建一个task_struct结构体,这个结构体就代表当前的进程。
进程运行产生的所有的信息都被放到这个结构体中保存着。
2. 进程和程序的区别
程序:程序是经过编译器编译生成的二进制文件,程序在硬盘上存储。
程序是静态的,没有生命周期的概念,程序本身不会分配内存。
进程:程序的一次执行过程就会创建一个进程,进程是动态的,有生命周期。
进程运行的时候会分配0-3G的内存空间。进程在内存上存储。
3. 进程的组成
进程是由三个部分组成的:进程的PCB(Process Control Block)(即task_struct结构体),(程序段)文本段,数据段。
4. 进程的种类
(1)交互进程
这种进程维护一个终端,通过这个终端用户可以和进程进行交互。例如:文本编辑器
(2)批处理进程
这种进程优先级比较低,运行的时候会被放到一个运行的队列中。
随着队列的执行,而逐渐执行。
例如gcc编译程序的时候这个进程就是批处理进程。
(3)守护进程
守护进程就是后台运行的服务,随着系统的启动而启动,随着系统的终止而终止。
例如:windows上的各种服务
PID 就是操作系统给进程分配的编号,它是识别进程的唯一的标识。
在linux系统中PID是一个大于等于0的值。
在linux系统中创建的进程的个数是有限制的(通过如下命令查看)。
cat /proc/sys/kernel/pid_max
4194304
5. 特殊PID的进程
0:idle
:在linux系统启动的时候运行的第一个进程就是0号进程。
如果没有其他的进程执行就运行这个idle进程。
1:init
:1号进程是由0号进程调用内核kernel_thread函数产生的第一个进程,
它会初始化所有的系统的硬件。当初始化完之后会一直执行,比如会为孤儿进程回收资源。
2:kthreadd
:调度器进程,主要负责进程的调度工作。
(二)相关命令
1. ps 命令
(1) ps -ef 查看进程的pid和ppid
linux@ubuntu:~$ ps -ef #用来查看进程的pid和ppid
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 5月29 ? 00:00:18 /sbin/init auto noprompt
root 2 0 0 5月29 ? 00:00:00 [kthreadd]
root 3 2 0 5月29 ? 00:00:00 [rcu_gp]
- UID 所属用户的id
- PID 进程id
- PPID 父进程的PID
- C CPU占用率
- STIME 进程启动的时间
- TTY 是由否与之关联的终端 如果? 就表示没有
- TIME 进程占用CPU的时间
- CMD 运行进程的命令
(2)ps -ajx 查看进程状态
linux@ubuntu:~$ ps -ajx #用来查看进程状态
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
0 1 1 1 ? -1 Ss 0 0:18 /sbin/init auto nopr
0 2 0 0 ? -1 S 0 0:00 [kthreadd]
2 3 0 0 ? -1 I< 0 0:00 [rcu_gp]
- PGID 进程组id
- SID 会话id,
打开一个终端就会产生一个新的会话
一个会话中可以有多个进程组 其中 前台进程组只有一个 后台进程组可以有多个
每个进程组中又可以有多个进程 - TPGID 如果是-1 就表示是守护进程
- STAT 进程的状态
2. top和htop命令 动态查看系统中进程的情况
- 注:htop命令需要自己安装
sudo apt-get install htop
top和htop命令
//动态的查看系统中进程的情况
进程号 USER PR NI VIRT RES SHR %CPU %MEM TIME+ COMMAND
91769 linux 20 0 2364 576 512 R 99.7 0.0 8:21.70 a.out
1704 linux 20 0 4894516 895980 59164 S 6.0 22.6 29:18.19 gnome-shell
1539 linux 20 0 319112 50656 18724 S 3.3 1.3 10:35.31 Xorg
3. pidof命令
pidof a.out //查看系统中所有名为a.out的进程PID
4.kill命令–给进程发信号的
kill -l
//查看系统中的信号
kill -2 pid //给进程发一个终止信号 (ctrl+c) 发出的信号
kill -3 pid //终止前台所有进程(ctrl+\)
kill -9 pid //杀死一个进程
kill -18 pid //让停止的进程继续运行
kill -19 pid //停止(暂停)一个进程
kill -20 pid //暂停一个进程(ctrl+z)
killall a.out //杀死所有能操作的名为a.out的进程
- 注:信号9和19不能被屏蔽,不能被改变
二、进程状态
(一)进程状态
可通过 man ps
查看
R (runing or runnable) 运行态或者可被运行态(在运行队列中)
S (interruptible sleep) 可被中断的休眠态
D (uninterruptible sleep) 不可被中断的休眠态
T (stopped by job control signal) 停止态
Z (("zombie") process)僵尸态
X (dead ) 死亡态(永远不会被看到)
I (Idle kernel thread) 空闲内核线程
进程的附加状态:
< 高优先级的进程
N 低优先级的进程
s (session leader)会话组组长
l (multi-threaded)进程中包含多线程
+ 前台进程组的进程
L 有页在内存中被锁定
(二)进程状态切换实例
1. 实例1
#include <stdio.h>
int main(int argc, char const *argv[])
{
int i=0;
while(1){
printf("%d\n",i++);
sleep(1);
}
return 0;
}
① 该程序运行时,大部分时间在休眠,处于休眠态
./a.out
运行 ------ 前台运行
② 想让一个进程在后台运行
可以 在运行进程的命令后面加一个 &
如 ./a.out &
- 注:
- 前台运行时,终端会被该进程占用,无法继续接收命令
- 后台运行时,终端会被释放出来,可以接收命令
- 处于后台运行时,无法直接“CTRL+C”来结束进程,需要发送
kill -19 pid
③ 可以使用 kill -19 pid
给进程发19号信号让进程停止(T
停止态)
也可以使用 ctrl+z
来停止进程(相当于给进程发kill -20 pid
)
在被停止的进程的终端上使用 jobs -l
可以查看进程的作业号
- 注:此处必须在被停止的进程的终端上查看,其他终端无法查看
fg 作业号
让停止的进程继续在前台运行
bg 作业号
让停止的进程继续在后台运行
kill -18 pid
让停止的进程继续在后台运行
三、进程的创建
(一)原理
进程的创建是通过 完全 拷贝父进程来实现的,子进程所有资源都来自于父进程。
子进程创建成功后,父子进程的执行相互独立
(二)fork函数–创建进程
1. 定义
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
功能:拷贝父进程,产生子进程
参数:无
返回值:
成功 给父进程返回子进程的PID 给子进程返回 0
失败 -1 不会创建子进程 会重置错误码
2. 不关注返回值
在不考虑返回值时,调用n次fork函数,会产生2^n个进程(包含父进程)
#include <my_head.h>
int main(int argc, char const *argv[])
{
for(int i=0;i<2;i++){
fork();
printf("#");
}
return 0;
}
输出结果:
分析结果:
要注意此处printf中并没有\n,即打印的’#‘存放在输出缓冲区中,而在循环第二次执行时,fork函数通过拷贝父进程生成子进程,即将其缓冲区也进行了拷贝,因此打印出了8个’#’
#include <my_head.h>
int main(int argc, char const *argv[])
{
for(int i=0;i<2;i++){
fork();
printf("#\n");
}
return 0;
}
输出结果:
分析结果:
要注意此处printf中有\n,此时的’#‘直接打印到了终端上,而在循环第二次执行时,fork函数通过拷贝父进程生成子进程,其缓冲区内是空的,直接又各自执行了一次打印操作,因此打印出了6个’#’
3. 关注返回值
fork函数给父进程返回子进程的pid,给子进程返回0,因此通过pid进行条件判断。
pid>0,说明当前进程是父进程;pid==0,说明当前进程是子进程,由此来使其执行各自的程序。
#include <my_head.h>
int main(int argc, char const *argv[])
{
printf("-----start-----\n");
pid_t pid=fork();
if(-1 == pid){
ERR_LOG("fork error");
}else if(0 < pid){
printf("这是父进程\n");
}else if(0 == pid){
printf("这是子进程\n");
}
printf("-----end-----\n");
return 0;
}
(三) 父子进程的执行顺序
父子进程执行,没有先后顺序,也是时间片轮转,谁得到cpu谁执行
(四)父子进程内存空间
父进程在fork产生子进程时,用到了写时拷贝的原则
如果父子进程中如果都只对同一个变量有读操作,那么不会重新映射到不同的物理内存;
只有在父子进程的中的任意一方执行了写操作时,才会重新映射到不同的物理内存
(五)使用实例—多进程拷贝文件
功能需求:使用父子进程拷贝文件,提高拷贝效率
因为cpu在执行程序时是按照时间片轮转的方式,哪个进程得到cpu就在这个时间片执行程序,而使用父子进程同时拷贝,就相当于增加了时间片轮转轮到的概率,由此提高拷贝效率。
需求分析:
fork之前打开的文件,fork之后,父子访问文件时是共用光标的。
如果不想让光标共用,可以在fork之后,父子进程中分别使用open去打开文件。
将源文件和目标文件一分为二,父进程复制文件开头到中间的位置的内容,子进程复制文件中间位置到结尾的内容,两个进程分别打开文件,不共用文件标识符
- 补充:关于可否使用父子进程在打开的文件中共用光标来实现拷贝的可实现性:
不能保证在一个时间片段内可以完整的执行完一条机器指令;因此可能会出现以下情况,如父进程读取了一段内容后,准备执行光标后移,但是未完成光标后移时就发生了时间片轮转,接着子进程再次复制时,光标并未移动,而重复读取内容。
代码实现:
#include <my_head.h>
//初始化:保证有一个清空的目标文件,获取源文件的长度
int init_cp(const char *src_file, const char *dest_file){
//打开一个目标文件,不存在就创建,存在就清空
FILE *dest_fp=fopen(dest_file,"w");
if(NULL == dest_fp) ERR_LOG("open dest file error");
fclose(dest_fp);
//获取源文件长度
FILE *src_fp=fopen(src_file,"r");
if(NULL == src_fp) ERR_LOG("open src file error");
fseek(src_fp,0,SEEK_END);
int size = ftell(src_fp);
fclose(src_fp);
return size;
}
//单进程复制函数
int func_cp(const char *src_file, const char *dest_file,int offset,int len){
//打开文件
int src_fd = open(src_file,O_RDONLY);
if(-1 == src_fd) ERR_LOG("open src file error");
int dest_fd = open(dest_file,O_WRONLY);
if(-1 == dest_fd) ERR_LOG("open dest file error");
//将两个文件的指针移动到offset位置
lseek(src_fd,offset,SEEK_SET);
lseek(dest_fd,offset,SEEK_SET);
//复制函数
int w_byte=0;//记录写入的字节数
int r_byte=0;//记录本次读到的字节数
char buff[10];//缓冲区
while(0 < (r_byte=read(src_fd,buff,sizeof(buff)))){
w_byte+=r_byte;
if(w_byte>=len){
write(dest_fd,buff,len-(w_byte-r_byte));
break;
}
write(dest_fd,buff,r_byte);
}
return 0;
}
int main(int argc, char const *argv[])
{
if(3 != argc){
printf("Usage:%s src dest\n",argv[0]);
exit(-1);
}
//初始化
int size = init_cp(argv[1],argv[2]);
//创建子进程
pid_t pid=fork();
if(-1==pid){
ERR_LOG("fork error");
}else if(0 < pid){//父进程
func_cp(argv[1],argv[2],0,size/2);
}else if(0 == pid){//子进程
func_cp(argv[1],argv[2],size/2,size-size/2);
}
printf("pid:%d 复制完成\n",getpid());
return 0;
}
四、 getpid/getppid
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
功能:返回调用进程的pid
参数:无
返回值:总是会成功
pid_t getppid(void);
功能:返回调用进程的父进程pid
参数:无
返回值:总是会成功
eg:使用fork创建进程,A创建B,B创建C,在每个进程中打印pid和ppid。
#include <my_head.h>
int main(int argc, const char *argv[])
{
pid_t pid = fork();
if(-1 == pid){
ERR_LOG("fork error");
}else if(0 < pid){
sleep(3);
printf("我是A pid = [%d] ppid = [%d]\n", getpid(), getppid());
}else if(0 == pid){
if(-1 == (pid = fork())){
ERR_LOG("fork error");
}else if(0 < pid){
sleep(1);
printf("我是B pid = [%d] ppid = [%d]\n", getpid(), getppid());
}else if(0 == pid){
printf("我是C pid = [%d] ppid = [%d]\n", getpid(), getppid());
}
}
return 0;
}
五、孤儿进程
子进程还没有执行完,父进程就结束了,此时子进程就是孤儿进程
孤儿进程会被 init 进程收养,当孤儿进程结束时 init 给他回收资源
孤儿进程就是一个正常的进程,当只不过其父进程变成了init。
他并不是进程状态的一种。
六、僵尸进程
当子进程运行结束后,父进程没有为其回收资源,此时子进程就是僵尸进程
僵尸进程对系统有害,占着资源不释放
七、进程退出exit/_exit函数
(一)exit
- 注:
- return 本身不使用来结束进程的,而是用来结束函数调用的,
- 只有在main函数中执行到return 才会结束整个进程 而在子函数中实行return只会结束函数调用
#include <stdlib.h>
void exit(int status);
功能:他是一个库函数,用来结束进程,他会刷新缓冲区
参数:status:给父进程返回的进程退出状态值 (0-255)
#define EXIT_FAILURE 1
#define EXIT_SUCCESS 0
返回值:无
(二)_exit
#include <unistd.h>
void _exit(int status);
功能:他是一个系统调用,用来结束进程,他不会刷新缓冲区
参数:status:给父进程返回的进程退出状态值
返回值:无
八、进程回收资源
(一)wait
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *wstatus);
功能:
wait用于父进程中,用来阻塞等待任意一个子进程退出,给子进程回收资源
子进程exit退出时的状态值 会被wait接收到。
参数:
wstatus:用来保存子进程退出状态的缓冲区的首地址
如果不关心子进程退出的状态 可以传 NULL
如果使用了:
0-6:7个bit位中保存的是终止子进程的信号的编号
8-15: 8个bit位保存的是子进程退出的状态值
WIFEXITED(wstatus) 如果为真 说明子进程是正常结束的
WEXITSTATUS(wstatus) 如果子进程是正常结束的 可以用它获取子进程退出的状态值
WIFSIGNALED(wstatus) 如果为真 说明子进程是被信号中断的
WTERMSIG(wstatus) 如果子进程是被信号中断的 可以使用它来获取终止子进程的信号的编号
返回值:
成功 终止的子进程的pid
失败 -1 重置错误码
(二)waitpid
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *wstatus, int options);
功能:
等待指定pid的子进程退出,为其回收资源
参数:
pid:要回收资源的子进程的pid
>0 回收指定pid的子进程的资源
-1 回收任意一个子进程的资源
0 回收和父进程同组的任意一个子进程的资源
<-1 回收任意一个进程组id等于 pid 的绝对值的子进程的资源
wstatus:和wait用法一样
options:标志位 0 阻塞 WNOHANG 非阻塞
返回值:
成功 回收资源的子进程的pid
如果设置了 WNOHANG 且没有子进程退出时 返回 0
失败 -1 重置错误码
注:
有下面等价的用法:
wait(NULL) <==> waitpid(-1, NULL, 0);
wait(&wstatus) <==> waitpid(-1, &wstatus, 0);