Linux操作系统学习第四篇博客

1、程序、进程和并发

(1)程序

    程序,是指编译好的二进制文件,在磁盘上,不占用系统资源(cpu、内存、打开的文件、设备、锁....)

(2)进程

    是一个抽象的概念,与操作系统原理联系紧密。进程是活跃的程序,占用系统资源。在内存中执行。(程序运行起来,产生一个进程)

(3)并发

    在操作系统中,一个时间段中有多个进程都处于已启动运行到运行完毕之间的状态。但,任一个时刻点上仍只有一个进程在运行。

在这里插入图片描述

2、单道程序设计和多道程序设计

(1)单道程序设计

    所有进程一个一个排对执行。若A阻塞,B只能等待,即使CPU处于空闲状态。而在人机交互时阻塞的出现时必然的。所有这种模型在系统资源利用上及其不合理,在计算机发展历史上存在不久,大部分便被淘汰了。

(2)多道程序设计

    在计算机内存中同时存放几道相互独立的程序,它们在管理程序控制之下,相互穿插的运行。多道程序设计必须有硬件基础作为保证。

(3)时钟中断

    时钟中断即为多道程序设计模型的理论基础。 并发时,任意进程在执行期间都不希望放弃cpu。因此系统需要一种强制让进程让出cpu资源的手段。时钟中断有硬件基础作为保障,对进程而言不可抗拒。 操作系统中的中断处理函数,来负责调度程序执行。

(4)注意说明

    在多道程序设计模型中,多个进程轮流使用CPU (分时复用CPU资源)。而当下常见CPU为纳秒级,1秒可以执行大约10亿条指令。由于人眼的反应速度是毫秒级,所以看似同时在运行。

3、CPU和MMU

(1)CPU

(1)寄存器

    寄存器是中央处理器内的组成部分。寄存器是有限存贮容量的高速存贮部件,它们可用来暂存指令、数据和地址。在中央处理器的控制部件中,包含的寄存器有指令寄存器(IR)和程序计数器(PC)。在中央处理器的算术及逻辑部件中,寄存器有累加器(ACC)

(2)程序计数器

    程序计数器是计算机处理器中的寄存器,它包含当前正在执行的指令的地址(位置)。当每个指令被获取,程序计数器的存储地址加一。在每个指令被获取之后,程序计数器指向顺序中的下一个指令。当计算机重启或复位时,程序计数器通常恢复到零。

(3)译码器

    译码器是一类多输入多输出组合逻辑电路器件,其可以分为:变量译码和显示译码两类。 变量译码器一般是一种较少输入变为较多输出的器件,常见的有n线-2^n线译码和8421BCD码译码两类;显示译码器用来将二进制数转换成对应的七段码,一般其可分为驱动LED和驱动LCD两类。

(4)算数逻辑单元(ALU)

算术逻辑单元是中央处理器(CPU)的执行单元,是所有中央处理器的核心组成部分,由与门和或门构成的算术逻辑单元,主要功能是进行二位元的算术运算,如加减乘(不包括整数除法)。基本上,在所有现代CPU体系结构中,二进制都以补码的形式来表示。

(5)MMU

1|  MMU是Memory Management Unit的缩写,中文名是内存管理单元,它是中央处理器(CPU)中用来管理虚拟存储器、物理存储器的控制线路,同时也负责虚拟地址映射为物理地址,以及提供硬件机制的内存访问授权,多用户多进程操作系统。

2| 特点
   <1> 虚拟内存与物理内存的映射
   <2> 设置修改内存访问级别

在这里插入图片描述

(2)MMU

在这里插入图片描述

4、进程控制块PCB

(1)进程id

  系统中每个进程有唯一的id,在C语言中用pid_t类型表示,其实就是一个非负整数。

(2)进程的状态

  有就绪、运行、挂起、停止等状态。

(3)进程切换时需要保存和恢复的一些CPU寄存器。

(4)描述虚拟地址空间的信息和描述控制终端的信息。

(5)进程基本的状态有5种。分别为初始态,就绪态,运行态,挂起态与终止态。其中初始态为进程准备阶段,常与就绪态结合来看。

在这里插入图片描述

5、环境变量

(1)环境变量简介

   环境变量是指在操作系统中用来指定操作系统运行环境的一些参数。

(2)环境变量特征

1|  字符串(本质)

2|  有统一的格式:名=[:值] 

3|  值用来描述进程环境信息。

(4)存储形式

  与命令行参数类似。char *[]数组,数组名environ,内部存储字符串,NULL作为哨兵结尾。

(5)使用形式

  与命令行参数类似。

(6)加载位置

  与命令行参数类似。位于用户区,高于stack的起始位置。

(7)引入环境变量表

  须声明环境变量。extern char ** environ;	

(8)打印当前进程的所有环境变量

#include <stdio.h>

extern char ** environ;

int main(void)
{
	int i;
	for(i=0;environ[i];i++)
	{
		printf("%s\n",environ[i]);
	}
	return 0;
 } 

6、环境变量操作函数

(1)getenv函数

1|  作用:获取环境变量值

2|  函数原型: char *getenv(const char *name);

3|  返回值:
              如果成功,则返回环境变量的值
              如果失败,则返回NULL (name不存在)

(2)setenv函数

1|  作用:设置环境或者修改环境变量变量的值

2|  函数原型:int setenv(const char *name, const char *value, int overwrite); 

3|  返回值:
               成功:0;失败:-1

4|  参数overwrite取值
                      1:覆盖原环境变量 
                      0:不覆盖。(该参数常用于设置新环境变量,如:ABC = haha-day-night)

(3)unsetenv函数

1|  作用:删除环境变量name的定义

2|  函数原型:int unsetenv(const char *name); 

3|  返回值:
                成功:0;失败:-1

7、创建单个子进程

(1)fork函数

1|  函数原型:pid_t fork(void)

2|  返回值(与普通的返回值不同,有两个返回值):
      ① 父进程返回子进程的id(大于0)        —————— 返回子进程的id
      ② 子进程返回值为0                     —————— 返回0

(2)循环创建n个子进程

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
	int i;
	pid_t pid;
	printf("xxxxxxxxxxxxx\n");
	
	for(i=0;i<5;i++)
	{
		pid = fork;
		if(pid == -1)
		{
			perror("fork error");
			exit(1);	
		}
		else if(pid == 0)
		{
			break;	
		}	
	}
	if(i<5)
	{
		sleep(i);
		printf("I am %d child , pid = %u\n",i+1,getpid());	
	}
	else
	{
		sleep(i);
		printf("I am parent");	
	}	
} 

8、exec函数族

fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
将当前进程的.text、.data替换为所要加载的程序的.text、.data,然后让进程从新的.text第一条指令开始执行,但进程ID不变,换核不换壳。
其实有六种以exec开头的函数,统称exec函数:
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);

(1)execlp函数

1|  函数原型:int execlp(const char *file, const char *arg, ...);

2|  作用:加载一个进程,借助PATH环境变量	 

3|  返回值:成功:无返回;失败:-1

4|  const char *file:表示要加载的程序的名字。

5|  该函数通常用来调用系统程序。如:ls、date、cp、cat等命令。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
	pid_t pid;
	pid = fork;
	if(pid == -1)
	{
		perror("fork error");
		exit(1);
	}
	else if(pid > 0)
	{
		sleep(2);
		printf("parent\n");
	}
	else
	{
		execlp("ls","ls","-l","-a",NULL);
	}
	return 0;
} 

(2)execl函数

1|  函数原型: int execl(const char *path, const char *arg, ...);

2|  作用:加载一个进程, 通过 路径+程序名 来加载。 

3|  返回值:成功:无返回;失败:-1

4|  对比execlp,如加载"ls"命令带有-l,-F参数
          execlp("ls", "ls", "-l", "-F", NULL);	     使用程序名在PATH中搜索。
          execl("/bin/ls", "ls", "-l", "-F", NULL);    使用参数1给出的绝对路径搜索。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
	pid_t pid;
	pid = fork;
	if(pid == -1)
	{
		perror("fork error");
		exit(1);
	}
	else if(pid > 0)
	{
		sleep(2);
		printf("parent\n");
	}
	else
	{
		execlp("/bin/ls","ls","-l","-a",NULL);
		//execlp("./while","while",NULL);
		//while为可执行文件名   ./while 为执行程序 
	}
	return 0;
} 

9、僵尸进程和孤儿进程

(1)孤儿进程

   孤儿进程: 父进程先于子进程结束,则子进程成为孤儿进程,子进程的父进程成为init进程,称为init进程领养孤儿进程。

(2)僵尸进程

   僵尸进程: 进程终止,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程。 

(2)特别注意

  僵尸进程是不能使用kill命令清除掉的。因为kill命令只是用来终止进程的,而僵尸进程已经终止。

10、wait回收子进程 ——只能回收一个子进程

(1)函数原型

    pid_t wait(int *status);

(2)返回值

    成功:返回清理掉的子进程ID                失败:返回-1(没有子进程)

(3)作用

1|  阻塞等待子进程

2|  回收子进程资源

3|  获取子进程结束状态

(4)其他注意

可使用wait函数传出参数status来保存进程的退出状态。借助宏函数来进一步判断进程终止的具体原因。宏函数可分为如下三组:

 1.  WIFEXITED(status) 为非0	                        → 进程正常结束
	 WEXITSTATUS(status)                               如上宏为真,使用此宏 → 获取进程退出状态 (exit的参数)
	 
 2. 	WIFSIGNALED(status) 为非0                       → 进程异常终止
	    WTERMSIG(status)                               如上宏为真,使用此宏 → 取得使进程终止的那个信号的编号。
	    
*3. 	WIFSTOPPED(status) 为非0                        → 进程处于暂停状态
	WSTOPSIG(status)                                   如上宏为真,使用此宏 → 取得使进程暂停的那个信号的编号。
	WIFCONTINUED(status) 为真 →                         进程暂停后已经继续运行

(5)有关代码

#include <stdio.h> 
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main()
{
	pid_t pid,wpid;
	int status;
	pid = fork();
	
	if(pid == 0)
	{
		printf("----child,my parent = %d,going to sleep 3s\n",getppid());
		sleep(3);
		printf("-----------------child die---------------");
		return 100;	
	}
	else if(pid >0)
	{
		wpid = wait(&status);
		if(wpid == -1)
		{
			perror("wait error:");
			exit(1);	
		}
		if(WIFEXITED(status)!=0)
		{
			printf("child exit with %d\n",WEXITSTATUS(status));	
		}
		if(WIFSIGNALED(status)!=0)
		{
			printf("child killed by %d\n",WTERMSIG(status));	
		}	
	}
	
	while(1)
	{
		printf("I am parent,pid = %d,myson = %d\n",getpid(),pid);
		sleep(1);	
	} 
}

11、waitpid回收子进程

(1)函数原型

     pid_t waitpid(pid_t pid, int *status, in options);	

(2)有关参数

1|  pid_t pid:指定回收进程的ID

2|  int *status:回收进程退出状态

3|  in options:0或者WNOHANG

(3)返回值

    成功:返回清理掉的子进程ID;                   失败:-1(无子进程)
    返回0值:参3为WNOHANG,且子进程正在运行

(4)参数

1|  参数1:
    pid > 0  表示指定进程ID回收                            pid = 0  表示回收本组任意子进程
    pid = -1 表示回收任意子进程                            pid < -1 表示回收改进程组的任意子进程

2|  参数2:
    status

3|  参数3:
    0  代表(wait)阻塞回收                              WNOHANG:非阻塞回收(轮询)

(5)有关代码

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>

int main(int argc,char *argv[])
{
	int n=5,i;                        //默认创建5个进程 
	pid_t p,q;
	pid_t wpid;
	
	if(argc == 2)
	{
		n = atoi(argv[1]);	
	}
	
	for(i=0;i<n;i++)          //出口1,父进程专用出口 
	{
		p = fork();
		if(p == 0)
		{
			break;	
		}                      //出口2,子进程专用出口,i不自增 
		else if(i == 3)
		{
			q = p;
		}	
	}
	
	if(n==i)
	{
		sleep(n);
		printf("I am parent,pid = %d\n",getpid());
		waitpid(q,NULL,0);
		//while(waitepid(-1,NULL,0));  回收所有子进程,相当于wait(NULL)
	    /*do
		{
			wpid = waitpid(-1,NULL,WNOHANG);     回收所有子进程,相当于wait(NULL)
			if(wpid > 0)
			{
				n--;
			} 
			if wpid == 0 说明子进程正在运行 
		}while(n>0);*/
		while(1);	
	}
	
	else
	{
		sleep(i);
		printf("I am %dth child,pid = %d,gpid = %d\n",i+1,getpid(),getgid());
		while(1);	
	}
	
	return 0;	
} 

12、管道

(1)概念

    管道是一种最基本的IPC机制,作用于有血缘关系的进程之间,完成数据传递。调用pipe系统函数即可创建一个管道。

(2)特征

1|  其本质是一个伪文件(实为内核缓冲区) 

2|  由两个文件描述符引用,一个表示读端,一个表示写端。

3|  规定数据从管道的写端流入管道,从读端流出。

(3)原理和局限性

1|  原理
    管道实为内核使用环形队列机制,借助内核缓冲区(4k)实现。

2|  局限性
    <1> 数据自己读不能自己写。
    <2> 数据一旦被读走,便不在管道中存在,不可反复读取。
    <3> 由于管道采用半双工通信方式。因此,数据只能在一个方向上流动。
    <4> 只能在有公共祖先的进程间使用管道。

3| 常见的通信方式有,单工通信、半双工通信、全双工通信。

14、pipe函数

(1)函数原型

     int pipe(int pipefd[2]);

(2)返回值

    成功:0  失败:-1  设置:errmo

(3)使用说明

    函数调用成功返回r/w两个文件描述符。无需open,但需手动close。规定:fd[0] → r; fd[1] → w,就像0对应标准输入,1对应标准输出一样。向管道文件读写数据其实是在读写内核缓冲区。

1|  父进程调用pipe函数创建管道,得到两个文件描述符fd[0]、fd[1]指向管道的读端和写端。

2|  父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道。

3|  父进程关闭管道读端,子进程关闭管道写端。父进程可以向管道中写入数据,子进程将管道中的数据读出。由于管道是利用环形队列实现的,数据从写端流入管道,从读端流出,这样就实现了进程间通信。
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/wait.h>

void sys_err(const char *str)
{
    perror(str);
    exit(1);
}

int main(void)
{
    pid_t pid;
    char buf[1024];
    int fd[2];
    char *p = "test for pipe\n";
    
   if (pipe(fd) == -1) 
       sys_err("pipe");

   pid = fork();
   if (pid < 0) {
       sys_err("fork err");
   } else if (pid == 0) {
        close(fd[1]);
        int len = read(fd[0], buf, sizeof(buf));
        write(STDOUT_FILENO, buf, len);
        close(fd[0]);
   } else {
       close(fd[0]);
       write(fd[1], p, strlen(p));
       wait(NULL);
       close(fd[1]);
   }
    
    return 0;
}

(4)管道的优劣

1| 优点:简单,相比信号,套接字实现进程间通信,简单很多。

2| 缺点:
   <1> 只能单向通信,双向通信需建立两个管道。
   <2> 只能用于父子、兄弟进程(有共同祖先)间通信。该问题后来使用fifo有名管道解决。

15、mmap函数

(1)作用

    创建共享内存

(2)函数原型

    void *mmap(void *adrr, size_t length, int prot, int flags, int fd, off_t offset); 

(3)参数说明

1|  addr: 	建立映射区的首地址,由Linux内核指定。使用时,直接传递NULL

2|  length: 欲创建映射区的大小
	
3|  prot:	映射区权限PROT_READ、PROT_WRITE、PROT_READ|PROT_WRITE
	
4|  flags:	标志位参数(常用于设定更新物理区域、设置共享、创建匿名映射区)
		  	MAP_SHARED:  会将映射区所做的操作反映到物理设备(磁盘)上。
		  	MAP_PRIVATE: 映射区所做的修改不会反映到物理设备。

5|  fd: 	用来建立映射区的文件描述符
	
6|  offset: 	映射文件的偏移(4k的整数倍)

(4)返回值

    成功:返回创建的映射区首地址                  失败:MAP_FAILED宏

(5)mmap函数创建映射区

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/mman.h>

int main()
{
	int len,ret;
	char *p = NULL;
	int fd = open("mytest.txt",O_CREAT|O_RDWR,0644);
	if(fd == -1)
	{
		perror("open error:");
		exit(1);	
	}
	len = ftruncate(fd,4);
	if(len == -1)
	{
		perror("ftruncate error:");
		exit(1);	
	}	
	p = mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
	if(p == MAP_FAILED)
	{
		perror("mmap error:");
		exit(1);
	}
	strcpy(p,"abc");
	
	ret = munmap(p,4);
	if(ret == -1)
	{
		perror("munmap error:");
		exit(1);
	}
	close(fd);
	
	return 0;
} 

(6)mmap函数注意事项

1|  创建映射区的过程中,隐含着一次对映射文件的读操作。

2|  当MAP_SHARED时,要求:映射区的权限应 <=文件打开的权限(出于对映射区的保护)。而MAP_PRIVATE则无所谓,因为mmap中的权限是对内存的限制。

3|  映射区的释放与文件关闭无关。只要映射建立成功,文件可以立即关闭。

4|  特别注意,当映射文件大小为0时,不能创建映射区。所以:用于映射的文件必须要有实际大小!!	mmap使用时常常会出现总线错误,通常是由于共享文件存储空间大小引起的。

5|  munmap传入的地址一定是mmap的返回地址。坚决杜绝指针++操作。

6|  文件偏移量必须为4K的整数倍

7|  mmap创建映射区出错概率非常高,一定要检查返回值,确保映射区建立成功再进行后续操作。

16、mmap父子进程间的通信

    父子等有血缘关系的进程之间也可以通过mmap建立的映射区来完成数据通信。但相应的要在创建映射区的时候指定对应的标志位参数flags:
    
     MAP_PRIVATE:  (私有映射)  父子进程各自独占映射区;
	 MAP_SHARED:  (共享映射)  父子进程共享映射区;

    结论:父子进程共享:1. 打开的文件  2. mmap建立的映射区(但必须要使用MAP_SHARED)

17、匿名映射区

(1)作用

    无需依赖文件创建映射区

(2)方法1

    int *p = mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANON,-1,0);
    新增参数:MAP_ANON                     "4":随意举例,根据实际情况来确定大小
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/mman.h>

int main()
{
	int ret;
	char *p = NULL;
	p = mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANON,-1,0);
	if(p == MAP_FAILED)
	{
		perror("mmap error:");
		exit(1);
	}
	strcpy(p,"abc");
	
	ret = munmap(p,4);
	if(ret == -1)
	{
		perror("munmap error:");
		exit(1);
	}
	close(fd);
	
	return 0;
} 

(3)方法2

    fd = open(“/dev/zero”,O_RDWR);              —————— 创建伪文件
    int *p = mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/mman.h>

int main()
{
	int len,ret;
	char *p = NULL;
	int fd = open("/dev/zero",O_RDWR);
	if(fd == -1)
	{
		perror("open error:");
		exit(1);	
	}
	len = ftruncate(fd,4);
	if(len == -1)
	{
		perror("ftruncate error:");
		exit(1);	
	}	
	p = mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
	if(p == MAP_FAILED)
	{
		perror("mmap error:");
		exit(1);
	}
	strcpy(p,"abc");
	
	ret = munmap(p,4);
	if(ret == -1)
	{
		perror("munmap error:");
		exit(1);
	}
	close(fd);
	
	return 0;
} 

18、非血缘关系进程间mmap通信

(1)编写一个程序进行读操作

//读操作文件1 

#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <string.h>

struct STU{
	int id;
	char name[20];
	char sex;
}; 

int main(int argc,char *argv[])
{
	int fd;
	struct STU student;
	struct STU *mm;
	
	if(argc < 2)
	{
		printf("./a.out file_shared\n");
		exit(1);
	}
	
	fd = open(argv[1],O_RDONLY);
	if(fd == -1)
	{
		perror("open error:");
		exit(1);
	}
	mm = mmap(NULL,sizeof(student),PROT_READ,MAP_SHARED,fd,0);
	if(mm == MAP_FAILED)
	{
		perror("mmap error:");
		exit(1);
	}
	close(fd);
	
    while(1)
	{
		printf("id = %d\tname = %s\t%c\n",mm->id,mm->name,mm->sex);
	}
	munmap(mm,sizeof(student));
	return 0;
}

(2)编写一个程序进行写操作

//写操作文件2 

#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <string.h>

struct STU{
	int id;
	char name[20];
	char sex;
}; 

int main(int argc,char *argv[])
{
	int fd;
	struct STU student = {10,"xiaoming",'m'};
	char *mm;
	
	if(argc < 2)
	{
		printf("./a.out file_shared\n");
		exit(1);
	}
	
	fd = open(argv[1],O_RDWR|O_CREAT,0644);
	if(fd == -1)
	{
		perror("open error:");
		exit(1);
	}
	ftruncate(fd,sizeof(student));
	
	mm = mmap(NULL,sizeof(student),PROT_READ,MAP_SHARED,fd,0);
	if(mm == MAP_FAILED)
	{
		perror("mmap error:");
		exit(1);
	}
	close(fd);
	
	while(1)
	{
		memcpy(mm,&student,sizeof(student));
		student.id++;
		sleep(1);
	}
	munmap(mm,sizeof(student));
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值