进程相关概念
进程是执行一个程序所分配的资源的总称,是动态的。
进程和线程的区别如图所示:
BSS段:存放程序中未初始化的全局变量
数据段:已初始化的全局变量
代码段:程序执行代码 //机器语言
堆(heap):malloc等函数分配内存
栈(stack):局部变量,函数参数,函数的返回值
进程控制块(pcb):进程用户,PID, 进程状态优先级,文件描述符表
- 进程类型:交互进程(在shell下启动eg:./a.out)、批处理进程(和终端无关,顺序执行);守护进程(和终端无关。后台运行,在shell上无法控制,是分离的)。
- 进程状态:运行态、等待态、停止态、死亡态(僵尸态):
常用查看系统进程的命令
- ps 查看系统进程快照
- top 查看进程动态信息
- /proc 查看进程详细信息
应用举例:
/* 在终端下输入
参数:
-e:显示所有进程
-l:长格式显示更加详细的信息
-f 全部列出,通常和其他选项联用
*/
ps elf|grep filename
//查看某个进程
top -p PID
- jobs 查看后台进程
- bg 将挂起的进程在后台运行
- fg 把后台运行的进程放到前台运行
- ctrl+z 把运行的前台进程转为后台并停止
- ./test & 把test程序后台运行
- nice [-n NI值] 按用户指定的优先级运行进程,NI数值越大优先级越低;
(普通用户NI范围[0-19],root用户范围[-20~19];普通用户只能升高优先级不能降低。) - renice 改变正在运行进程的优先级,eg:renice [优先级] PID。
创建子进程
#include <unistd.h>
pid_t fork(void);
fork函数应用举例:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
pid_t fpid;//fpid表示fork函数返回的值
int count=0;
fpid=fork();
if(fpid<0)
printf("error in fork!");
else if(fpid==0) //进程创建失败
{
printf("我是子进程,id:%d\n",getpid());
count++;
}
else
{
printf("我是父进程,id:%d\n",getpid());
count++;
}
printf("统计结果是:%d\n",count);
exit(0);
}
执行结果:(注意:父子进程 执行顺序是不确定的,也有可能先打印子进程)
我是父进程,id:[父进程的PID]
我是子进程,id:[子进程的PID]
统计结果是:1
统计结果是:1
上述结果表明:父子进程拥有独立的地址空间,互不影响,每个进程都只增加了自己内存空间中的count一次。
进程退出
#include <stdlib.h>
void exit(int status);
exit函数会结束当前进程,关闭所有标准 I/O 流,释放动态分配的内存等。刷新(流)缓冲区,并将status返回(状态码0表示正常退出,非0值则表示出现了某种错误或异常情况)。
#include <unistd.h>
void _exit(int status);
_exit函数它用于立即终止进程,不执行清理操作。
return 和exit的区别:普通函数return是返回上一级,而exit是直接退出程序。
(main函数结束时会隐式地调用exit函数)
进程回收
#include <unistd.h>
pid_t wait(int *status); // wait()函数返回子进程的PID,如果调用失败,则返回-1;
/* wait()
1、用于父进程等待其子进程的状态变化,若子进程还没有终止,则父进程会被阻塞。
2、当父进程调用wait()检测到子进程终止时,释放子进程所占用的资源,包括内存、文件描述符等。
3、status用于存储子进程的终止状态,设置为NULL表示直接释放子进程PCB,不接收返回值
*/
#include <unistd.h>
pid_t waitpid(pid_t pid, int *status, int option);
// waitpid() 函数是一个用于等待特定子进程状态改变的系统调用。它比 wait() 函数更加灵活,可以指定要等待的子进程的PID,以及一些附加选项。
exec 函数族
进程调用exec函数族执行某个程序,当前内容被指定的程序替换。
当有父子进程时,实现让父子进程执行不同的程序。
这位前辈讲的exec 函数族非常详细,我这里就不赘述了。
给出两个简单的代码例子供大家理解,看注释:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
printf("Executing ls command using execvp()\n");
char *args[] = {"ls", "-l", NULL};
// 执行 ls 命令
execvp("ls", args);
// execvp() 函数只在出错时返回,因此如果执行到这里,说明调用失败
perror("execvp");
exit(EXIT_FAILURE);
// 这里的代码不会被执行
printf("This line will not be executed.\n");
return 0;
}
这是个使用fork()函数创建父子进程,调用execl()的例子。
#include <stdio.h>
#include <unistd.h>
int main(){
pid_t pid;
printf("before exec\n");
pid = fork();
if(pid==0){
if(execl("/bin/ls","-a","-l","./",NULL)<0){
perror("execl");
}
}
printf("after execl\n");
}
执行结果为:
before exec
after execl
进程信息......
进程信息......
守护进程
守护进程(Daemon Process)是Linux三种进程类型之一,是 Linux 中的后台服务进程,独立于控制终端,是一个孤儿进程。
目的:之所以脱离于终端是为了避免进程被任何终端所产生的信息所打断,其在执行过程中的信息也不在任何终端上显示。
更简便地创建守护进程:一般不推荐这样创建守护进程
// nohup 命令
nohup command [arguments] & //& 符号表示将命令放入后台运行。
相关概念
守护进程创建过程:
- 创建子进程,父进程退出
- 子进程创建新会话
- 更改当前工作目录
- 重设文件权限掩码
- 关闭打开的文件描述符
实现代码展示如下:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/stat.h>
int main(){
pid_t pid;
pid = fork();
if(pid<0){
perror("fork");
return 0;
}else if(pid>0){ // 1、父进程退出 子进程变成孤儿进程,被init进程收养 子进程在后台运行
exit(0);
// sleep(100);
}
printf("I am a deamon\n");
printf("sid=%d,pid=%d,pgid=%d\n",getsid(getpid()),getpid(),getpgid(getpid()));
if(setsid()<0){ // 2、子进程创建新会话 子进程成为新的会话组长 脱离原先的终端
perror("setsid");
exit(0);
}
printf("after sid=%d,pid=%d,pgid=%d\n",getsid(getpid()),getpid(),getpgid(getpid()));
chdir("/"); // 3、更改当前工作目录
if(umask(0)<0){ // 4、重设文件权限掩码
perror("unmask");
exit(0);
}
close(0); // 5、关闭打开的文件描述符
close(1);
close(2);
printf("after close \n");
sleep(100);
}
执行结果:
GDB调试多进程程序
- set follow-fork-mode child 设置GDB调试子进程
- set follow-fork-mode parent 设置GDB调试父进程
- set detach-on-fork on/off 设置GDB跟踪调试单个进程或多个
on: 只调试父进程或子进程的其中一个,(根据follow-fork-mode来决定),这是默认的模式
off:父子进程都在gdb的控制之下,其中一个进程正常调试(根据follow-fork-mode来决定),另一个进程会被设置为暂停状态。 - info inferiors 显示GDB调试的进程
- inferiors 进程序号(1,2,3…) 切换GDB调试的进程
这位前辈对于GDB调试部分写的非常详细。
以上是个人的一些学习总结和心得体会。参考了GPT及一些网络资源,后面还有学习心得会继续补充。新手,如有建议或写的不对的地方,欢迎讨论一下哦。欢迎交流,共同进步!