深入Linux系统编程:缓冲区、进程管理(fork()写时拷贝、exec())、系统调用和库函数与信号处理全指南
printf()
输出缓冲区
-
缓冲区满
-
强制刷新缓冲区 “\n",fflush()
-
程序结束
#include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<string.h> int main() { printf("hello,world!!!\n"); //fflush(stdout); sleep(3); exit(0);//退出大小当前进程 }
库函数
-
声明:stdio.h /usr/include
-
实现:libc.a libc.so
静态库,共享库
/lib ,usr/lib
win:静态库.lib,动态链接库 .dll (Dynamic Link Library)
Linux:静态库.a(Archive 档案文件),共享库.so(shared object)
库:预先编译好的方法的集合 add.c----->add.o/add.obj
静态库
archiver(归档库,存储器) c(create创建) r(replace 替换) v(Verbose)详细模式
共享库

sudo su--->切换管理员
mv libfoo.so /lib
gcc -o main main.c -l foo(-L---->已经默认是在 标准位置/lib)
ldd main---->main可执行程序中所用的共享库 (List Dymanic Dependencies)共享库依赖分析工具
静态库和共享库的区别:
gcc -o main main.c -L. -lfoo 静态库
大 包含(拷贝)了用到的foo库中的方法 ---->删除库文件,main可以正常运行
gcc -o main main.c -L. -lfoo 共享库(优先)-->在程序运行时动态链接
小 没有包含(拷贝)了用到的foo库中的方法,只做标记 删除库文件,main无法执行
ldd查看可执行文件用到哪些共享库
LD_LIBRARY_PATH环境变量,可以指定可执行程序链接库文件的位置
eg:LD_LIBRARY_PATH=. ---> "."代表当前位置
export LD_LIBRARY_PATH
环境变量:
操作系统中用于存储系统运行状态、程序配置信息的动态参数------>本质是“键值对”(PATH=/usr/bin),供系统和应用程序读取
共享配置信息
将某个文件夹的路径添加到全局都可以访问的状态:用户环境变量、系统环境变量
-
用户变量:当前电脑登录的用户全局都可以访问
-
系统环境变量:一台电脑的多个用户全局都可以访问
进程fork()复制进程
《操作系统》:管理计算机软硬件资源,为用户提供一个交互的接口
进程、内存、文件管理(文件系统) 中断
进程:一个正在运行的程序
PCB:进程控制块 Linux进程描述符
fork()复制内存,以页为单位复制,写时拷贝(延时/免除拷贝)---->修改时才复制,否则共享
内核为了提高fork()复制效率---->写时拷贝

父进程的fork()返回值----->子进程pid
子进程fork()返回值---->0
引用头文件#include<unistd.h>
pid_t pid=fork()
getpid()--->当前进程的pid
getppid()---->父进程pid
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
int main(int argc, char* argv[],char* envp[])
{
char * s = NULL;
int n = 0;
pid_t pid = fork();
assert( pid != -1 );
if ( pid == 0 )
{ s = "child";
n = 3;
}
else
{
s = "parent";
n = 7;
}
int i = 0;
for(; i < n; i++ )
{
printf("cur_pid=%d,p_pid=%d,s=%s\n",getpid(),getppid(),s);
sleep(1);
}
exit(0);
}
//n的逻辑地址相同,物理地址不同(页面 2^32/2^12=2^20---->4K(2^12))
nm +程序名---->查看逻辑地址
逻辑地址、物理地址(一般不可以直接指定)
printf
面试题:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
int main(int argc, char* argv[],char* envp[])
{
fork()||fork();// fork()&&fork();
printf("A\n");//3个A
exit(0);
}
注意:子进程进行 是从 父进程执行fork() 之后 开始执行
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
for(int i=0;i<2;i++)
{
fork();
printf("A\n"); //6个A A (A A A A)
}
return 0;
}
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
for(int i=0;i<2;i++)
{
fork();
printf("A"); //大于6个 8个--->缓冲区没有刷新 fork()会复制父进程的A
}
return 0;
} A AAA
进程状态:
-
运行
-
就绪
-
阻塞
-
僵死进程(defunc)(ps) exit(0)\return 0; exit_code=0--->会一直保存 父进程会可以查看子进程退出状态
-
wait(int *)--->查看后才释放 exit_code -128~127(8位)
-
wait() sys/wait.h 父进程调用wait()解决僵死进程
-
子进程先于父进程结束,父进程没有获取子进程的退出码
-
父进程先结束,子进程--->孤儿进程,系统给子进程分配父进程(早期----->pid=1 init)
-
./fork &(后台运行) ---->ps
-
当子进程 先于 父进程 结束时:
-
内核会释放 子进程的大部分资源(内存、文件描述符等)。
-
但会保留子进程的进程控制块(PCB),里面包含了它的退出状态码。
-
内核会向父进程发送一个
SIGCHLD信号,通知它 “你的子进程结束了,快来收尸”。 -
如果父进程不调用
wait()或waitpid()来回收这个退出状态,子进程的 PCB 就会一直占用系统资源,这个进程就变成了僵尸进程。
写时拷贝:提高fork()效率
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include<wait.h>
int main(int argc, char* argv[],char* envp[])
{
char * s = NULL;
int n = 0;
pid_t pid = fork();
assert( pid != -1 );
if ( pid == 0 )
{ s = "child";
n = 3;
}
else
{
s = "parent";
n = 7;
int val=0;
wait(&val); //阻塞(子进程没有结束) wait(int *)
if(WIFEXITED(val))//判断程序是否正常结束
{
printf("val=%d",WEXITSTATUS(val));//获取退出码的值
}
//printf("val=%d",val>>8);
}
int i = 0;
for(; i < n; i++ )
{
printf("cur_pid=%d,p_pid=%d,s=%s\n",getpid(),getppid(),s);
sleep(1);
}
exit(0);
}
库函数|系统调用
Linux操作文件的系统调用----->#include<fcntl.h>------>操作文件,需要用到(File Control Header)
fopen()库函数 调用open()
open/close read/write man 2(系统调用) write
Linux--->一切皆文件
-
O_RDONLY 只读打开
-
O_WRONLY 只写打开
-
O_RDWR 读写方式打开
-
O_CREAT 文件不存在则创建
-
O_APPEND 文件末尾追加
-
O_TRUNC 清空文件,重新写入
#include<stdio.h>
#include<fcntl.h>//操作文件,需要用到
int main()
{
int fd=open("file.txt",O_CREAT|O_WRONLY,0600);// read:4 write:2 无:1
if(fd==-1)
{
printf("open err\n");
exit(1);
}
write(fd,"hello",5);
close(fd);
exit(0);
}
#include<stdio.h>
#include<fcntl.h>
int main()
{
int fd=open("file.txt",O_RDONLY,0600);// read:4 write:2 无:1
if(fd==-1)
{
printf("open err\n");
exit(1);
}
char buff[128]={0};
int n=read(fd,buff,127);
printf("n=%d buff=%s\n",n,buff);
close(fd);
exit(0);
}
#include<stdio.h>
#include<fcntl.h>
int main()
{
int fd=open("file.txt",O_RDONLY,0600);// read:4 write:2 无:1
int fd1=open("file1.txt",O_CREAT|O_WRONLY,0600);
if(fd==-1||fd1==-1)
{
printf("open err\n");
exit(1);
}
int n;
char buff[128]={0};
while((n=read(fd,buff,128)>0)
{
write(fd1,buff,n);
}
//printf("n=%d buff=%s\n",n,buff);
close(fd);
exit(0);
}
复制图片
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
int main(int argc,char*argv[])
{
if(argc!=3)
{
printf("arg err eg:./mycp a.png b.png ");
exit(1);
}
char* s=argv[1];
char* t=argv[2];
int fd1=open(s,O_RDONLY);
if(fd1==-1)
{
printf("filename:%s open err\n",s);
exit(1);
}
int fd2=open(t,O_WRONLY|O_CREAT,0600);
if(fd2==-1)
{
printf("Create %s err\n",t);
exit(1);
}
int num;
char* wr[512];
while((num=read(fd1,wr,512))>0)
{
write(fd2,wr,num);
}
exit(0);
}
父进程先打开的文件,fork后 子进程也可以访问 ,并且父子进程共享文件偏移量
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
int main()
{
int fd = open("file.txt",O_RDONLY);
if( fd == -1 )
{
exit(1);
}
pid_t pid = fork();
if( pid == -1 )
{
exit(1);
}
printf("fd=%d",fd);// 3
if( pid == 0 )
{
char buff[32] = {0};
read(fd,buff,1);
printf("child read:%s\n",buff);
sleep(1);
read(fd,buff,1);
printf("child read:%s\n",buff);
}
else
{
char buff[32] = {0};
read(fd,buff,1);
printf("parent read:%s\n",buff);
sleep(1);
read(fd,buff,1);
printf("parent read:%s\n",buff);
}
close(fd);
exit(0);
}
文件表(已打开的文件)
0 标准输入设备:FILE* stdin 1 (屏幕)标准输出设备: FILE* stdout
2 标准错误输出:FILE* stderr
(0,,1,2······)---->文字描述符
关闭一个文件后,再重新打开一个文件,会选择最小的序号
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
int main()
{
printf("A");//--->缓冲区 printf--->库函数
write(1,"B",1);--->系统调用
fork();
exit(0);
}
//BAA
printf--->用户空间------->库函数
write()---->内核------>系统调用
系统调用和库函数的区别?
-
系统调用:
-
操作系统内核提供的、用户程序请求内核服务的接口(内核态的入口),用户态与内核态交互------>唯一合法途径
-
需要从用户态--->内核态切换过程有性能消耗
-
-
库函数:
-
编程语言/库函数提供的封装函数,构建在系统调用或其他库函数之上,是面向开发者的高级接口
-
是对系统调用的“封装/增强”(printf--->库函数,底层调用write,但增加了“格式化输出+缓冲区”)
-
纯用户态(strlen/strcpy)、系统调用(可以通过优化减少切换次数,如printf的缓冲区)
-
系统调用的过程: 用户态→内核态的切换 + 内核服务执行 + 内核态→用户态的切回
-
用户态程序调用系统调用接口(如
sys_read); -
触发软中断(int 0x80 或 syscall 指令),CPU 切换到内核态;
-
内核保存用户态上下文(寄存器、栈);
-
内核根据系统调用号执行对应内核函数;
-
内核完成操作后,恢复用户态上下文,返回结果;
-
CPU 切回用户态,程序继续执行。

信号、替换exec()系列
#define SIG
-
发出信号 kill(pid_t pid,int sig);
-
进程
-
响应
-
默认 0 SIG_DFL
-
忽略 1 SIG_IGN (ignore)
-
自定义 fun signal(); SIGCHLD----->17
-
函数指针====>sighandler_t
ctr+ \ ----->信号3 ctr+c --->信号2
void (*signal(int sig,void(*func)(int)))(int);
//分解
typedef void(*P)(int);//类型重命名 函数指针
P signal(int sign,P func);//信号
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
void sig_fun(int sig)//sig信号的代号,内核传参
{
printf("sig=%d\n",sig);
signal(sig,SIG_DFL);//将SIGINT信号和对应方式调整为默认 第二次调用恢复默认
}
void main()
{
signal(SIGINT,sig_fun);
//signal(SIGINT,SIG_IGN);//忽略
while(1)
{
printf("hello pid=%d\n",getpid());
sleep(1);
}
}
发送信号
kill+pid (15)默认终止 mykill+pid sig 9-->强制杀死 2-->ctrl+c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
// ./mykill 3345 2
int main(int argc,char* argv[])
{
if(argc!=3)
{
printf("argc err,eg:mykill 3345 2\n");
exit(1);
}
int pid=atoi(argv[1]);
int sig=atoi(argv[2]);
if(kill(pid,sig)==-1)//发送信号
{
printf("kill err\n");
}
exit(0);
}
处理僵死进程
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<stdlib.h>
#include<sys/wait.h>
void sig_fun(int sig)
{
printf("sig=%d",sig);
int val=0;
wait(&val);//不会阻塞,准确收到了子进程结束的信号
//原来在父进程运行wait();-->会阻塞,要等子进程结束收到其退出码才继续执行
printf("val=%d\n",val);
}
int main()
{
int n=0;
signal(SIGCHLD,sig_fun);//子进程结束——————>信号17
//signal(SIGCHLD,SIGING);//直接忽略-->内核(仅在Linux)-->默认不需要子进程的退出码
char* s=NULL;
pid_t pid=fork();
if(pid==-1)
{
exit(1);
}
if(pid==0)
{
n=3;
s="child";
}
if(pid>0)
{
n=7;
s="parent";
}
for(int i=0;i<n;i++)
{
printf("%s\n",s);
sleep(1);
}
exit(0);
}
exec()进程替换
产生全新的进程:fork()+exec()

-
execl()
-
execlp()
-
execle()
-
execv
-
execvp
-
execvpe()---->系统调用 底层内核调用相同,只是函数封装,参数传递不同
which ps---->/usr/bin/ps
-
which:用于查找系统命令对应的可执行文件路径,它会在$PATH环境变量指定的目录中搜索目标命令的位置
-
pwd(print working directory):打印当前工作目录的绝对路径
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main(int argc,char* argv[],char* envp[])
{
printf("main pid=%d\n",getpid());
execl("/usr/bin/ps","ps","-f",(char*) 0);//替换,用ps替换当前程序
execlp("ps","ps","-f",(char*)0);//一旦含有p 均会在$PATH环境变量中查找 echo $PATH echo-->打印
execle("/usr/bin/ps","ps","-f",(char*) 0",envp);//可以修改环境变量
char* myargv[]={"ps","-f",0}
execv("usr/bin/ps",myargv);
execvp("ps",myargv);
execvpe("/usr/bin/ps",myargv,envp);//系统调用
//exec()无返回值,若替换成功,直接执行ps(已替换的进程),main程序将会消失
printf("exec err\n");
exit(0);
}
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main(int argc,char* argv[],char* envp[])
{
// 打印当前进程的PID,方便观察:替换后的ps进程和原main进程是同一个PID(进程替换不创建新进程)
printf("main pid=%d\n",getpid());
// 1. execl:l表示参数列表(list),需指定程序绝对路径,参数逐个传入,最后以(char*)0结尾
execl("/usr/bin/ps","ps","-f",(char*) 0);
// 说明:一旦这行执行成功,后续所有代码都不会运行(进程被ps替换);只有失败才会执行下一行
// 2. execlp:p表示搜索PATH环境变量(path),无需写绝对路径,参数列表形式
execlp("ps","ps","-f",(char*)0);
// 对比execl:不用写/usr/bin/ps,系统会在$PATH里找ps命令的位置
// 3. execle:e表示自定义环境变量(environment),参数列表+环境变量数组,原代码此处有语法错误(多了双引号)
execle("/usr/bin/ps","ps","-f",(char*) 0, envp); // 修正:去掉(char*)0后的多余双引号
// 4. execv:v表示参数数组(vector),需指定绝对路径,参数放在字符串数组中,数组以0结尾;原代码有2处错误:少分号、路径少/
char* myargv[]={"ps","-f",0}; // 修正:补充分号
execv("/usr/bin/ps",myargv); // 修正:路径从usr/bin/ps改为/usr/bin/ps(少了根目录/)
// 5. execvp:v(参数数组)+ p(搜索PATH),无需绝对路径,用参数数组传参
execvp("ps",myargv);
// 6. execvpe:v(参数数组)+ p(搜索PATH)+ e(自定义环境变量),系统调用级别的进程替换
execvpe("/usr/bin/ps",myargv,envp);
// 只有所有exec函数都替换失败时,才会执行到这里(打印错误提示)
printf("exec err\n");
exit(0);
}
exec 函数命名规则(帮你记忆):
| 后缀 | 含义 |
|---|---|
| l | list:参数以逐个列举的形式传入(如"ps","-f",(char*)0) |
| v | vector:参数放在字符串数组中传入(如myargv) |
| p | path:自动搜索 $PATH 环境变量,无需写程序绝对路径 |
| e | environment:自定义环境变量,需传入环境变量数组(如envp) |
参数要求:
-
无论
l还是v,参数的第一个元素必须是程序名(如"ps"),后续是命令参数(如"-f"),最后必须以0或(char*)0结尾(表示参数结束); -
envp是 main 函数的第三个参数,保存了当前进程的环境变量(如 PATH、HOME 等),execle/execvpe可以通过它传递自定义环境变量。 -

bash--->命令解释器
mybash.c
while(1)
-
键盘输入命令和参数 ls -l,cp a.c b.c
-
fork()
-
exec(ls)
#include<stdio.h>
#include<unistd.h>
#include<wait.h>
#include<string.h>
#include<stdlib.h>
#include<pwd.h>
//cp a.c b.c---->cp\0a.c\0b.c
char* get_cmd(char* buff,char** myargv)//char* myargv[]
{
if(buff==NULL||myargv==NULL)
{
return NULL;
}
int i=0;
char* s=strtok(buff," ");
while(s!=NULL)
{
myargv[i++]=s;
s=strtok(NULL," ");//strtok会用 偷偷记录的位置(分隔符后的第一个字符) 作为起点
}
return myargv[0];
}
void print_info()
{
char* user_str="$";
int uid=getuid();//获取当前用户的id号
if(uid==0)
{
user_str="#";
}
struct passwd* p=getppwuid(uid);
if(p==NULL)
{
printf("mybush $");
fflush(stdout);
return;
}
//p->pw_name
char host[128]={0};
gethostname(host,128);//获取主机名
char cur_dir[128]={0};//当前路径
getcwd(cur_dir,128);
printf("\033[1;32m%s@%s\033[0m:\033[1;34%s\033[0m%s",p->pw_name(),host,cur_dir,usr_str);
fflush(stdout);
}
int main()
{
while(1)
{
char buff[128]={0};
/*printf("stu@localhost ~$");
fflush(stdout);//强制刷新缓冲区
*/
print_info();
fgets(buff,128,stdin);//从键盘中读取数据 回车可以读到 ls--->"ls\n"
//scanf();输入回车---->阻塞 无有效字符
buff[strlen(buff)-1]='\0';// 0 --->"ls"
char* myargv[10]={0};//主函数传参 内容
char* cmd=get_cmd(buff,myargv);
if(cmd==NULL)
{
continue;
}
if(strcmp(cmd,"exit")==0)//内置命令
{
break;
}
if(strcmp(cmd,"cd")==0)//内置命令
{
//改变当前路径
if(myargv[1]!=NULL)
{
if(chdir(myargv[1])==-1)
{
printf("cd err:%s not find",myargv[1]);
}
}
continue;//只是路径发生变化
}
pid_t pid=fork();
if(pid==-1)
{
continue;
}
if(pid==0)
{
execvp(cmd,myargv);//进程替换
//exec()无返回值,替换成功直接执行子进程,否则继续执行main程序
printf("execvp err\n");
exit(1);
}
//父进程
wait(NULL);//解决僵死进程---->需要等 子进程 先结束 父进程阻塞
//模拟前台
}
}
内置命令(父进程):cd exit
普通命令(子进程):ls cp ps
/etc/passwd----->用户数据库
char* getcwd(char* buff,size_t size);//获取当前路径
实现ls
#include<stdio.h>
#include<unistd.h>
#include<wait.h>
#include<string.h>
#include<stdlib.h>
#include<dirent.h>
#include<sys/stat.h>
int main()
{
char buff[256]={0};
getcwd(buff,256);//获取当前位置
DIR* pdir=opendir(buff);
if(pdir==NULL)
{
printf("%s 目录流获取失败!!!\n",buff);
exit(1);
}
struct dirent* s=NULL;
while((s=readdir(pdir))!=NULL)
{
if(strncmp(s->d_name,".",1)==0)//以.开头的隐藏文件(文件打开状态) ls -a
{
continue;
}
struct stat st;
stat(s->d_name,&st);
if(S_ISDIR(st.st_mode))//文件类型---->目录
{
printf("\033[1;34m%s\033[0m ",s->d_name);
}
else
{
if(st.st_mode&(S_IXUSR|S_IXGRP|S_IXOTH))//文件是否有执行权限
{
printf("\033[1;32m%s\033[0m ",s->d_name);
}
printf("%s ",s->d_name);
}
}
printf("\n");
closedir(pdir);
}
stat----->man 2 stat------->文件的属性信息 #include<sys/stat.h>

总结

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐






所有评论(0)