printf函数以及缓冲区
先打印hello 在睡眠3s钟
去掉 \n 之后 先睡眠3s在打印hello
正常认知:代码是顺序执行 printf执行完后才能执行sleep. 由于缓冲区的作用,退出程序才把缓冲去的内容显示到了屏幕上.
打印的内容是如何显示到屏幕上的
当有以下三种情况时,缓冲区才会把数据传输到标准输出设备(显示器)中进行输出
1.缓冲区满
2.程序结束时
3.强制刷新缓冲区 “\n” fflush(stdout)可以达到此效果
缓冲区的内容提交给内核 利用write()打印到屏幕上
fork()复制进程
进程:一个正在运行的程序
内核把进程存放在叫做任务队列(task list)的双向循环链表中。链表中的每一项都是类型为task_struct、称为进程描述符(process descriptor)的结构,该结构定于在<linux/sched.h>文件中。进程描述符中包含一个具体进程的所有信息。
fork 函数会新生成一个进程,调用 fork 函数的进程为父进程,新生成的进程为子进程。
在父进程中返回子进程的 pid,在子进程中返回 0,失败返回-1。
一个理解fork()小例子
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
int main() {
int n = 0;
char* s = NULL;
pid_t id = fork();//复制进程
assert(id != -1);
if (id == 0) {//子进程
n = 3;
s = "child";
}
else {//父进程
n = 7;
s = "parent";
}
//子进程和父进程打印次数分别为3、7
for (int i = 0; i < n; ++i) {
printf("s=%s,pid=%d\n",s,getpid());
sleep(1);
}
exit(0);
}
父进程先于子进程结束
修改 n 的次数也就相当于修改了子进程以及父进程执行for()的次数
fork笔试题
1.
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main(){
fork()||fork();
printf("A\n");
exit(0);
}
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main(){
fork()&&fork();
printf("A\n");
exit(0);
}
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main(){
for(int i=0;i<2;++i){
fork();
printf("A\n");
}
exit(0);
}
4.去掉3中的\n
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main(){
for(int i=0;i<2;++i){
fork();
printf("A");
}
exit(0);
}
父进程先终止
系统保证每个进程都有一个父进程,若父进程比子进程先终止,则该父进程的所有子进程的父进程都改变为init进程。我们称这些进程由init进程领养。其执行顺序大致如下:在一个进程终止时,内核逐个检查所有活动的进程,以判断它是否是正要终止的进程的子进程,如果是,则该进程的父进程ID就更改为1(init进程的ID)。
僵死进程及处理方法
(1) 僵死进程概念:子进程先于父进程结束,父进程没有调用 wait 获取子进程退出码。
(2)僵死进程的危害:如果父进程不调用wait()/waitpid()的话,那么存在于那个位置的信息就不会被释放,它的PCB就会被永远占用,但是系统的进程表的容量是有限的,所能使用的进程号也是有限的,所以如果产生了大量的僵死进程,将因为没有可用的进程而导致系统不能产生新的进程,也就无法fork();所以,僵死进程不仅占用系统的内存信息,还影响了系统的性能,如果大量产生,还会导致系统瘫痪.
(3) 如何处理僵死进程:父进程通过调用 wait()完成。
(4) Init 进程收养孤儿进程
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
int main() {
int n = 0;
char* s = NULL;
pid_t id = fork();//复制进程
assert(id != -1);
if (id == 0) {//子进程
n = 3;
s = "child";
}
else {//父进程
n = 7;
s = "parent";
}
//子进程和父进程打印次数分别为3、7
for (int i = 0; i < n; ++i) {
printf("s=%s,pid=%d\n",s,getpid());
sleep(1);
}
exit(0);
}
父进程通过调用 wait()获取子进程的退出码解决了僵死进程
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<sys/wait.h>
int main() {
int n = 0;
char* s = NULL;
pid_t id = fork();//复制进程
assert(id != -1);
if (id == 0) {//子进程
n = 3;
s = "child";
}
else {//父进程
n = 7;
s = "parent";
int val=0;
wait(&val);
printf("val=%d\n",val);
}
//子进程和父进程打印次数分别为3、7
for (int i = 0; i < n; ++i) {
printf("s=%s,pid=%d\n",s,getpid());
sleep(1);
}
exit(0);
}
pid_t wait(int *status); *status 是子进程的结束状态值(exit(0)中的 0 在系统中会通过左移8位显示出),返回值为子进程PID.
如果子进程没结束那么wait会阻塞
从上图可以看到子进程结束后,彻底从系统中消失,并没有变成僵死进程.
*int wait(int status) 函数的宏
1.WIFEXITED(status)这个宏用来指出子进程是否为正常退出的,如果是,它会返回一个非零值。(此处status是个整数).
2.WEXITSTATUS(status)当WITEXITED返回非零值时,我们可以用这个宏来提取子进程的返回值,如果子进程调用exit(3)退出,WEXITSTATUS(status)就会返回3;如果子进程调用exit(4)退出,WEXITSTATUS(status)就会返回4.如果进程不是正常退出,也就是说,WIFEXITED返回0,这个值就毫无意义.
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<sys/wait.h>
int main() {
int n = 0;
char* s = NULL;
pid_t id = fork();//复制进程
assert(id != -1);
if (id == 0) {//子进程
n = 3;
s = "child";
}
else {//父进程
n = 7;
s = "parent";
int val=0;
int id=wait(&val);
if(WIFEXITED(val)){
printf("id=%d,val=%d\n",id,WEXITSTATUS(val));
}
}
//子进程和父进程打印次数分别为3、7
for (int i = 0; i < n; ++i) {
printf("s=%s,pid=%d\n",s,getpid());
sleep(1);
}
exit(0);
}
操作文件的系统调用:open,read,write,close 返回值为文件描述符(int)
open read write close 系统调用,实现在内核中
PCB(进程描述符\进程控制块)用struct task_struct;实现.文件描述符表是此结构体的成员
每次打开文件(open操作时 ),会产生struct file这样一个结构体在操作系统内核中,用来表示打开的这个文件,记录着文件偏移量(起始从0开始,随着写入数据进行增加),引用计数(使用此文件的进程个数),inode节点(存放进程的属性信息:创建者,存储信息。通过inode节点,能找到对应的文件)
1.先打开文件在复制进程(先open在fork)
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<unistd.h>
#include<fcntl.h>
#include<string.h>
int main(){
int fd=open("file1.txt",O_RDONLY);//abcdef
assert(fd!=-1);
pid_t pid=fork();
assert(pid!=-1);
if(pid==0){//子进程
//每次读一个字符并输出
char buff[8]={0};
read(fd,buff,1);
printf("child buff=%s\n",buff);
sleep(1);
read(fd,buff,1);
printf("child buff=%s\n",buff);
}
else{//父进程
//每次读一个字符并输出
char buff[8]={0};
read(fd,buff,1);
printf("parent buff=%s\n",buff);
sleep(1);
read(fd,buff,1);
printf("parent buff=%s\n",buff);
}
close(fd);
exit(0);
}
运行结果
结论:子进程可以使用fork之前open返回的文件描述符。调用fork之后,只拷贝了PCB本身,拷贝的只是指针,没有拷贝指针所指向的内容,这种情况叫做浅拷贝。子进程的指针依旧指向struct file,所以父子进程对于文件描述符和文件偏移量是共享的。
此时父子进程共享此文件的属性,引用计数==2,父子进程都执行close()才可以彻底关闭文件.
2.先复制进程在打开文件(先fork在open)
父子进程各自打开各自的文件
示意图如下:
替换进程
进程的产生:fork+exec
exec函数作用:把当前进程替换为一个新进程,且新进程与原进程有相同的PID.并从新程序的main第一行开始执行.替换成功,原程序就会被替换为新程序,并执行新程序.
exec系列
int execl(const char *pathname, const char arg, … / (char *) NULL */);
int execlp(const char *file, const char arg, … / (char *) NULL */);
int execle(const char *pathname, const char arg, …/, (char *) NULL, char *const envp[] */);
int execv(const char *pathname, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
函数返回值: 成功不返回,失败返回-1.
分为两大类:execl()系列和execv()系列
path参数表示你要启动程序的名称包括路径名
l:list要求将新进程的每个命令行参数都说名为一个单独的参数,以空指针结尾.
v: 是指把所有参数放到容器(数组, vector)中再一次性传入.
p: 是指第1个参数位于默认的环境变量PATH中,忽略pathname,仅用文件(file)指出文件名即可.
e:是指第1个参数位于给定的envp环境变量中., 用绝对路径(path)给出待执行文件.
使用ps替换main
execl()
替换之后进程name是ps,替换后的进程的pid和main的pid相同,这说明替换进程并不改变原有pid.
其它execl系列的使用(只列出了较上面程序不同的部分)
*****可以不用给全路径只给命令名字
execlp("ps","ps","-f",(char*)NULL);
//有e需要环境变量
execle("/usr/bin/ps","ps","-f",(char*)NULL,envp);
execv系列
其它execv系列的使用(只列出了较上面程序不同的部分)
***可以不用给全路径只给命令名字
execvp("ps",myargv);
***有p可以只给命令名字
execvpe("ps",myargv,envp);
//有e需要环境变量
execve("/usr/bin/ps",myargv,envp);