【笔记】Linux应用编程随堂笔记

喏,这个是Liunx高级编程的学习哦!
笔记依然是自己一个字一个字手打的=.=
想要机酱的这些源码的话,后面我是你可以用链接丢上来的

授课老师:朱有鹏
听课辣鸡:宕机酱

唔,这种菜鸡的日子什么时候是个头呢~


@打开方式:Notepad++ 微软雅黑
Vi的各个模式编程命令:参考D盘,台湾下学期,嵌入式里面的 《嵌入式lin..应用开发详解.pdf》P70

0Linux应用编程 和 网络编程
嵌入式Linux核心课程の课程顺序:1裸机 2C高级 3uboot4linux应用编程 5驱动

典型的嵌入式产品开发的顺序
1、让Linux系统在硬件上跑起来(系统移植工作)
2、基于Linux系统来开发应用程序实现产品功能(应用编程属于这一步骤)

基于Linux做应用编程其实就是调用API来实现应用需要完成的一些任务。
注意:此课程是降低难度版
主要目的:使用linux内核提供的API和c库函数来实现一定的功能

层次:APP+OS 操作系统本身有一部分是驱动,下面是硬件
读写文件的案例中,文件是从硬盘中放着,当打开文件即将其读取到内存,这些硬件需要驱动。操作系统中有文件系统fs,
保证我们用目录+文件名的方式来访问他,给我们提供了一些访问接口,从而用这些“门”去操控硬件和文件。


1.2 文件操作的主要接口API

app | OS | 硬件
操作系统对外开的门,就是API,其实质是一些函数。由OS提供,应用层使用

常用的文件IO接口
open 打开一个文件
close 关闭一个文件
lseek 移动文件指针
write 向文件中写入内容
read 从文件中读内容

Linxu文件操作的一般步骤:
1、先open一个文件,得到一个文件描述符 //失败就GG
2、对文件进行读写操作
3、最后关闭文件 //不关可能导致损坏

文件平时是存在块设备中的文件系统中的,没有文件系统会直接去扇区中操作文件。
我们把存在块设备中没有打开的文件,称为静态文件

open一个文件时,Linux内核所做的:
1、内核在进程中建立了一个打开文件的数据结构,记录下来我们打开的文件。
2、申请一段内存,将静态文件从块设备读取到内存的一个特定地址存放,即动态文件
3、读写时内存中的动态文件就和块设备中的静态文件不同步了。close后更新块设备
//现象举例:不保存就关机/断电,文件内容会丢失。//这样设计的原因:块设备本身有读写限制。块设备的读写特点就是大片数据读写,而内存是可以很灵活的以字节读写、是可以随机访问的。

文件描述符:实质是一个数字。当我们open一个文件时,OS在内存中构建了一些数据结构来表示这个动态文件,返回一个数字作为文件描述符,以后我们应用程序要操作一个动态文件,就依靠文件描述符区分。
//简而言之:就是用爱区分一个程序打开的多个文件的。出了当前进程就会失去意义。
//A进程:打开文件3,B进程:打开文件3,这两个文件不同,是完全没有比较性的


1.3第一个程序

#include
int main(int argv, char *argv[])
{
    //打开文件
    //读写文件 
    //关闭文件
}

{ 
    int fd = -1; //fd: file descriptor
    char buf[100]={0};//read()所使用的缓冲区
    int ret_read = 0;//read()的返回值

    //打开文件
    fd = open("a.txt",O_RDWR);//这些定义很难记住,man 2 open。返回值是新fd
    if(-1 == fd) //fd的合法范围>0 ,错误时返回-1
    printf(文件打开错误);
    else 成功 , 输出fd                  

    //读取文件
    ret = read (fd, buf, 20);

    //关闭文件
    close(fd); 
}

注意:
1、close函数的传参是文件描述符。
2、open返回的文件描述符fd千万不能丢,最后关闭也需要fd去指定关闭这个文件,如果在关闭之前丢掉了 fd,那么就没法关闭、没法读写了。
3、ssize_t read (fd ,buf ,size_t count)
ssize_t是内核用typedef重定义的类型,其实就是int。返回值是成功读取的字节数。为了可移植
fd 是读取的文件,一般由open获得
buf 是应用程序自己提供的一段缓冲区
count 是我们要读取的字节数
关于man的使用
man 1 xxx shell
man 2 xxx API
man 3 xxx 库函数

1.4 open函数flag详解
Linux中文件有读写权限,我们在open函数的时候也可以附带一定权限打开
读写权限相关:
O_RDONLY 只读
O_WRONLY 只写
O_RDWR 读写
打开已经存在并且有内容的文件时:
可能会有三种可能:
1、新内容取代旧内容
2、新内容在原内容的前面 (很少有这种需求)
2、新内容在原内容的后面
4、不读不写的时候,原来的内容不变
O_ARREND 新内容接续到后面 【结果3】
O_TRUNC 不是空的,变成空的 【结果1】
不使用上述符号 【结果4】

退出进程:在打开/读写失败的时候,不应该继续让程序运行下去了
1、return -1; //只可以在main函数中使用
2、使用exit / _exit / _Exit //三个作用是一样的

exit(0); //使用的时候用一句代替return -1;

O_CREAT 有就清空,没有创建(有危险,怕弄错文件名)
O_EXCL | O_CREAT有就报错,没有创建 EXCL:不包括

O_CREAT单独使用的时候有文件被冲掉的威胁,但Windows不允许新文件与旧文件同名,会选择或覆盖
open函数在使用O_CREAT函数的时候,可以设定这个文件的权限(mode)。如0777
\
OS中阻塞与非阻塞的概念
阻塞式:当前我想干一件事,排队等候=.=
若一个函数是阻塞式的,调用这个函数时当前进程可能被卡住。(实质是函数内部条件不具备)
非阻塞式:嘿呀不能立马办,就先回去等一会儿再来
若一个函数是非阻塞式的,这个函数一定会立即返回,但不一定完成任务
操作系统提供的API和由API封装的库函数是区分阻塞和非阻塞式的,我们在调用的时候要非常清楚
API会说的非常清楚,默认的是阻塞式的7,

O_NONBLOCK 以非阻塞式的方式打开一个文件。注意:只用于设备文件

write写一个东西,如果是阻塞式,而且还不能写,那么进程就不动了。
举个应用中的例子:12306打开的时候默认去连网,如果手机没联网,他拼命的尝试去连网。这种设计不好,应该先显示一个前台页面,先让我们看到,然后在后台去联网。

O_SYNC 阻塞式:保证底层完成后,才返回应用层
OS内核中有缓冲区,默认情况下APP到缓冲区就算结束了

这个时候硬件设备(如写入的硬盘)可能尚未更新

======================================================
1.6 文件读写 的 一些细节

errno 和 perror
【errno】
记录系统的最后一次错误代码,看是发生什么错误了。代码是一个int型的值

注意:只有当一个库函数失败时,errno才会被设置。当函数成功运行时,errno的值不会被修改。这意味着我们不能通过测试errno的值来判断是否有错误存在。反之,只有当被调用的函数提示有错误发生时检查errno的值才有意义。
查看错误代码errno是调试程序的一个重要方法。当linux C api函数发生异常时,一般会将errno变量(需include errno.h)赋一个整数值,不同的值表示不同的含义,可以通过查看该值推测出错的原因。在实际编程中用这一招解决了不少原本看来莫名其妙的问题。

【perror】
perror(“Open_error: …. “);
返回输出:Open_error: No such file or diretory!

文件IO和标准IO
文件io是API组成的一套体系。可以很好的完成读写,但效率不高 openclose
标准io是应用层c库函数提供的一些函数。是库函数不是API fopenfclose
标准io也是由文件io封装而来,内部也是用文件io组成的。加这个封装的目的主要是在应用层添加一个缓冲机制,在应用层构建了一个buffer,再通过fwrite就不再是写到内核buffer,而是直接写入应用层buffer,帮我们协调运送的buffer大小,也就是说,记录多次写入内容,等应用层buf大小足够大,然后一并送入内核,提高了效率。

文件io写入ABC(直接依次进内核)
写入A 内核buffer:A
写入B 内核buffer:AB
写入C 内核buffer:ABC
写入硬件设备

标准io写入ABC(适当打包进内核)
写入A 应用层buffer:A内核buffer:NULL
写入B 应用层buffer:AB内核buffer:NULL
写入C 应用层buffer:ABC内核buffer:NULL
应用层buff写入: (A+B+C) → 内核buff收到(ABC)

操作系统知道一次写入buf最好是几个,知道应用层的buffer应该设置多大,提高效率

======================================================
1.7 Linux管理文件

文件分类:1静态文件 2动态文件
硬盘区域分类:1硬盘内容管理表、2真正的存储内容区域
OS访问硬盘时,先去读取硬盘内容管理表,从中找到要访问的文件的 扇区级别的信息,利用这个信息查询真正存储内容的区域,得到需要的文件

操作系统如何通过文件名得到文件内容:
查询硬盘内容管理表。该表中含有一个文件的多种信息,封装在结构体中
这个结构体也叫信息列表,即inode。
里面大概就是文件名、所在硬盘扇区、块号等等

硬盘管理,是以文件为单位的。每个文件有独有的inode,对于一个结构体。

关于格式化:格式化分为两种,快速格式化和普通格式化。前者只是删除了硬盘内容管理表,所以非常快。普通格式化也就是底层格式化,是比较慢的。
快速格式化的内容是有可能被找回的,只要恢复i节点就可以了。
底层格式化了基本就没戏了,但是一些国家机关是有一些办法的。

内存中文件打开
在程序中打开的文件就属于某个进程。每个进程都有一个数据结构,用来记录进程的所有信息,叫做进程信息表。进程信息表中有一个指针 指向 文件管理表。
当前进程能找到文件,正是因为有这个机制。
文件管理表中用来索引打开的文件的index值就是文件描述符fd
最终找到的,就是一个已经被打开的 文件的管理结构体 vnode

文件与流的概念
stream :流
文件读写只允许 一个字符一个字符的进行,那么多个字符被挨个读写时,就构成了一个字符流
流的概念是动态的,他不是一个东西,而是一个运动状态
编程中提到流的概念,一般都是IO相关的,所以经常称之为 IO流。

【玛德喂,老朱说的麻烦死了】
上述过程,简而言之就是:表表相连。
inode 用来表示一个静态文件
vnode 用来表示一个动态文件v

======================================================

1.8 lseek

光标:GUI模式下给人看的,标识当前正在操作的 位置
在动态文件中,会通过文件指针来表征正在操作的位置。lseek就是操作文件指针的
把光标移动的操作在内部就对应着lseek。

打开空文件的时候,默认情况下文件指针指向文件流的开始。所以write写入从头开始,每写一个就自动把文件指针后移,这样就不会让写入的东西被后来的写入覆盖

write和read是隐式的操作文件指针
lseek是显式的操作文件指针
lseek(fd, offset, whence(参照物))//哪个文件,从whence开始,往后偏移offset个
其中,whence就三个
SEEK_SET 开头
SEEK_CUR 当前
SEEK_END 结尾
【举例:写一个用lseek测定文件大小的程序】
思路: 1、打开文件,默认文件指针是在开头
2、lseek的返回值是相对于开头偏移的数量
3、lseek到文件末尾,取返回值打印输出
评语:666,比lseek一个读一个判断这种方法好了不知多少。

ret = lseek(fd,0,SEEK_END);
printf(“文件大小为:%d\n”,ret);

【关于argc,argv】

        if(argc != 2)
        {
                printf("usage: %s filename\n",argv[0]);
                _exit(1);
        }

argc 是 段的个数
argv[0] 代表第一段
argv[1] 代表第二段

======================================================

1.9 多文件打开与O_APPEND

清空缓冲区:memset(缓冲区,0,sizeof(buf));

在一个进程中,两个文件是可以同时打开的(分别读)
fd1, fd2均有不同的值。但是在操作两个的时候,有时候是分别写,有时候是接续写
fd1:写AB,fd2写CD
情景1:CDCDCDCD //ab被覆盖,实验验证,默认是这样分别写
情景2:ABCDABCD //AB未被覆盖,O_APPEND

TIPS:
dup2是分别写?还是接续写?这个我们测试得出的结果是接续写
这个知识是不需要记住的,面试的时候如果不知道,就应该说不知道但我能用方法测试出来
这种回答方法是最好的 ,甚至比知道答案本身更好。这是解决问题的能力。
最糟糕的就是不知道,想了半天还蒙了一个,然后一口咬定。

======================================================

1.10 文件共享以及dup()
文件共享的核心:是多个文件描述符指向同一个文件

进程表 文件表 vnode
文件标识符表
fd(索引) 文件表指针 文件指针seek位移量 文件信息
0 v节点指针 i节点信息
1 文件信息 当前文件长度
2 文件状态信息
fd1 p1指向文件表
fd2 p2指向文件表

这关系到一个进程open多个文件、或者dup、再或者多个进程共享一个文件的时候,是接续写还是分别写
举个栗子;
进程1,open;进程2,open,同时打开一个文件。会有两张进程表和两个文件表。
进程1,open1文件,open2文件,打开两个文件,只有一个fd表和两个文件表。
dup:复制一个fd(比如把文件描述符fd1,复制为fd2)

fd是系统自动分配的,很少见fd为012,是因为0/1/2已经默认被系统占用了。所以open的最小的就是3.
这三个文件是有意义的,他们就是stdin stdout stderr!
stdin 标准输入,一般对应的是键盘。//-0对应的是键盘的设备文件
stdout 标准输出,printf的默认输出//-1对应的是LCD显示器设备文件
fprintf:可以指定fd输出到其他文件中
stderr

fd2 = dup(fd1) //系统自动分配新的 fd
fd2 = dup2(fd1,233) //系统按照233复制得到新的fd2

我们可以把1close掉,这样就失去了标准输出,printf就看不到了,如果这时候dup重新分配得到fd = 1,那么此时打开的oldfd就把标准输出绑定起来了。这就叫标准输出的重定位。
本来1是跟stdout绑定的,现在可以用dup或者dup2重定位fd=1的文件

Linux重定义命令: >
ls > 1.txt stdout不再输出,此时1.txt被写入ls的结果

这个>的实现原理,其实就是open+close+dup

======================================================

1.12 fcntl函数介绍

fcntl是个多功能的工具箱,他 的功能很多。这个API是用来管理文件描述符的
int fcntl(int fd, int cmd, … /* arg */ );
操作哪个文件?进行哪个命令操作?…(变参是用来传递参数的 ,配合cmd使用)
cmd的格式是 F_xxx,不同的cmd有不同用法,不需要记住

举例:F_DUPFD
a = fcntl(3, F_DUPFD, 6 );
从可用的fd表中找一个大于等于arg的数,作为oldfd的复制——newfd,与dup2不同的是,dup2返回的不是指定值那么就是-1,而fcntl用于dup的时候是返回一个大于等于arg的最小的那个数

fcntl还有很多其他功能,比如更改文件的处理时间等等。


1.13
标准io C库函数 fopen
文件io API open
看起来都是函数,实际上本质是不一样的。
C库函数 = API + 封装。
因为多了封装,所以会比api更好用一点。API在不同的操作系统中是不通用的,C库函数是可以移植的。
性能上和易用性上看,文件IO是不带缓存的,标准IO比文件IO性能要好一些
【open】
int open(const char *pathname, int flags);

【fopen】
FILE *fopen(const char *path, const char *mode);
//其中,mode可以有r(可读模式),r+(可读可写,文件指针在开头,存在报错)
w+(可读可写,没有建立,有则清空),a(append)

fp = fopen(FILENAME , “w+”)
if (fp == NULL) //这里的fp是fopen返回值,文件指针,对应FILE *stream参数
{
perror(“fopen error”);
}
这些东西可以自学了,不需要讲太多

可以注意到,标准IO返回的不是一个文件描述符,而是一个FILE *,文件,文件指针类型
他所指向的内容,可以理解为一个打开的文件流。流是一种运动状态而非实质,是一个动态操作的概念。

【fwrite】=—fread与之相似,记得读之前最好memset(buf , 0 ,sizeof(buf))清空buffer
//fwrite
ret = fwrite(“Iam ur father”,1,sizeof(buf) / sizeof(buf[0]),fp);

size_t 写 *ptr 单个大小 写入成员数 文件指针

======================================================
2.1\

Linux——一切皆是文件。
目前还感受不到这种设计的好处,但学到驱动,控制蜂鸣器等等,与其他OS对比就可以感受它有多么优秀
普通文件 (-)
文本文件 二进制文件
目录文件 (d)
能vi打开,但是需要特殊api来读写的文件
字符设备文件 (c)
块设备文件 (b)
文件系统虚拟出来的文件,如/dev /sys /proc,大多数不直接读写,用特殊api,驱动时用
管道文件 (p)
管道通信
套接字文件 (s)
socket
符号链接文件 (l)

软链接、硬链接

======================================================
2.2 文件属性

Linux 命令: stat a.c获取文件属性
stat指令是根据stat这个api得到的应用

stat函数三种形态:
stat 不打开文件,读文件属性(在磁盘中读)
fstat 若文件打开,读已经打开的文件属性(效率更高,在内存读)
lstat 读符号链接文件本身的属性(stat、fstat会追溯指向文件的属性)

======================================================

2.3 文件操作的权限

一个./a.out 去判断是否有权限操作一个文件1.txt,是如何判断有操作权限的?
1、 1.txt有9个权限位,分别规定user、group、others操作权限
chown改变user权限
chgrp改变group权限

chmod不说了 1.txt要判断a.out是被谁执行了,然后对比权限是否合法 chmod指令 是由chmod这个 api得到d umask 是由umask这个 api得到的,为了安全有些时候不允许可执行。作用是设定默认权限。 access函数:判断是否可读可写可执行 ret = access( … , W_OK); if( ret < 0 ) 不可写 else 可写 2.6读取目录文件 opendir函数打开一个目录后,得到一个DIR指针给readdir使用 readdir返回一个struct dirent(dir entry)类型的指针,指向一个结构体变量,记录着目录中的子文件

readdir调用一次,只能读取一个文件。要读取全部必须多次调用。

======================================================

3.1时间api

GMT时间是格林尼治时间,世界统一,但是现在基本不用
UTC时间,原子钟时间,非常精确,现在一般用这个
时间在操作系统中的经历不是1s读一次RTC(实时时钟),RTC只在开机读取一次
后面的时间都是经过一个全局变量:jiffies
jiffies过1s则+1。记录的是1970年1月1日00:00距离现在的秒数,是一个段时间。(不是点时间)

相关API,自己man,有的传参是地址,有的是buf,有的是time_t,具体看api定义
time
ctime 打印时间字符串格式
gmtime 返回值是结构体,可自定义格式。//ps:从1900年开始,0-11月。
localtime

年月日分别打印的自定义格式函数:
strftime(buf , sizeof(buf) , “%Y - %m - %c , %H : %M : %S \n”,&tm);
printf(buf)即可得到 2016 - 11- 11 , 11:11:11的格式
未来产品需要自己在右上角显示一个时间,就需这样的方法

【以下是从初始时间得到localtime结构体,再根据结构体输出的格式化时间程序】

#include 
#include 
#include
int main(int argc,char *argv[])
{
        int newtime = 0;
        int ret;

        char buf[100] = {0};             //buf
        struct tm t1;                   //定义结构体,详细信息 man ctime
        time_t time_now = 0;             //int格式时间变量

        //---获取int类型时间
        time_now = time(NULL);
        printf("time_t tm = %ld  \n",time_now);

        //---localtime_r将time_t(也就是int)转换为结构体存放在t1中
        localtime_r(&time_now, &t1);
        printf("localtime转化 = %d%d%d日\n",t1.tm_year,t1.tm_mon,t1.tm_mday);

        //---获取格式自定义类型的时间并输出
        strftime(buf , sizeof(buf) , "%Y - %m - %c , %H%M%S \n",&t1);
        printf("strftime自定义格式时间  =  %s \n ",buf);
        printf("Code Access~\n");
        return 0;
}

这个时间是一步一步得来的,要注意函数之间传递的参数变化有所不同

======================================================

3.5 Linux中的随机数

【随机数和伪随机数】
我们一般用的是随机数是伪随机数,是由一定的算法得到的。它实际是有迹可循的,真正理想的完全随机数是不存在的。
rand(void); //多次调用返回野怪1-27亿之间的随机数序列。若想设定1-100,那么输出取余100即可。
srand(); //设置随机数的种子。

rand的一个缺点是:我们多次重复使用rand得到的数是相同的,是因为种子一直不变。
想要rand值发生变化,有效的方法就是换种子

种子可以设置为time_t time()的返回值,这样就按时间计算随机数。这几乎是随机度非常高的了
唯一的缺陷是1s才能变化一次。1s内调用了两次,printf输出的还是相同结果

Linux中其实是有获取真实随机数的方法的,这些方法与“中断”有关。我们知道中断是随机的,是无法预测的,Linux可以根据输入设备的操作,如点鼠标键盘触摸屏事件的触发,设置不同的种子和随机数。

======================================================

3.6 proc文件系统

操作系统级别的调试:
这里说的不是基于OS调试,而是调试操作系统

1、单步调试
next走一步,非常直观而简单
但需要一个环境,必须有gdd、eclipse、jlink等调试环境。
不适合熟练的技术工人

2、复杂程序printf打印信息调试
打印输出信息,不使用断点,观察一些中间值

3、框架体系日志记录调试
一个产品两天之内没问题,第三天开始吃内存,七八天就停了
将日志信息用重定向,打印到log文件中

4、操作系统内核调试
我们使用的内核都是别人写好的
但是调试linux内核的那些人是面临非常大困境的。经常会加一个功能导致影响其他已有的。
早期内核版本的调试尽管那些高手很麻烦,但是高手们还可以根据自己超凡脱俗的个人能力去驾驭。但与此同时是2.4版本,内核开发者自己也驾驭不住庞大的内核了,新手也难以融入。linux内核面临断代的考验。

emmmm于是有了proc….
【proc:让开发者“看”到内核的工具】
在内核中虚拟一个文件系统 /proc,内核运行时将一些关键内核数据结构 以文件的格式,呈现在/proc目录下,这样相当于把不可见的内核数据结构、变量等 “可视化”的方式呈现给开发者。通过实时观察/proc/xxx下的文件,观看内核中特定数据结构的值。当我们添加一个新功能以后,去与过去的值对比就可以知道新功能产生的影响是对还是不对。

在/proc目录下,有一些虚拟文件是ls无法看到,却可以cat得到的
比如你在那个目录下是找不到cmdline这个文件的,实际上它是一个虚拟文件。
“没有大小,没有block,但是它有内容,也可以编辑”
他并非一个真实存在于硬盘的文件,他只是一个接口。当我们读取这个文件时,内核不去硬盘上找而是直接去映射内核的数据结构,转化为一个字符串呈现给我们。所以我们看起来和普通文件是一样的。
但一定注意:他不是来自硬盘,而是映射于kernel!!

例如:
/proc/cmdline 命令行实时传过来的参数cat /proc/cmdline就可以。
/proc/cpuinfo
/proc/devices
/proc/version

fd = open /proc/version 是可行的,要注意用RDONLY,这样就可以安全读取信息了

【扩展:sys】
sys中的文件可以读写,proc中的文件只能读,两者都是内核的虚拟文件
它是因为控制proc过于凌乱而后来加上的,对我们的驱动设备学习很有用,以后再说=v=

======================================================
4.1 进程的开始和结束

进程状态:运行、等待、停止、就绪、僵尸

进程存在的标志:PCB,是内核中专门用来管理一个进程 的 数据结构
linux中查看当前进程的运行状态命令: ps
看系统所有的进程:ps -aux

argc , argv的实现是由于信号机制

[atexit()和exit()]
atexit (函数指针) 用于主函数执行结束后,调用指针指向的函数。
eg:
atexit(func1 );i//func1: printf(“23333\n”);
printf(“wupupupu\n”);
return 0;
输出结果:wupupupu
23333
原因就是atexit在return 0 之后吧func1调用。\

[exit和_exit]
exit时,函数被正常终止。_exit终止的 函数将不会执行atexit系统自动调用。
atexit在存储时参照LIFO , 也就是说先定义的 atexit后调用。

======================================================

4.2

getpid 获取进程id
getppid 获取父进程id
getuid 获取usrid
getgid 获取groupid

命令 export 查看环境变量
在每个进程中都有一个环境变量表,所以我们可以在进程中直接使用环境变量
注意“使用 环境变量,就注定和OS环境有关了,注意移植问题”
使用环境变量的方法:
environ

【举例:打印所有定义的环境变量】

#include
int main(int argc,char *argv[])
{
        int i = 0;
        extern char **environ;          //声明即可以引用

        while(NULL != environ[i])
        {
                printf("第%d个环境变量:  %s\n",i,environ[i]);
                i++;
        }

        printf("Code Access~\n");
        return 0;
}

~
获取指定环境变量函数:getenv

【虚拟内存技术】
是Linux系统设计的技术,一个进程认为4G的内存空间中,只有自己和操作系统。
实际也许你的设备只有512M,那该怎么实现呢?

1、虚拟地址映射。
与ucos不同,linux能将虚拟地址映射到真正的物理空间中,是一种强大的内存管理方式
2、分布映射
1G的内存每次存放一部分,分步骤依次放入运行时内存

这样做的好处:
1、进程隔离,做到各个进程不使用同一段内存
2、提供多进程同时运行,便于设备的更新和维护。(旧手机不能更新系统就是这样的道理)

======================================================

4.3进程的正式引入

多进程调度原理:宏观的并行,微观的串行。
实际现在OS的最小调度单位是线程,教科书认为是进程。

======================================================

4.4新进程诞生的方法

linux中的设计:新进程由老进程复制得出。复制之后要改pid等参数
新进程就是子进程,老进程就是父进程。

fork函数调用一次会返回2次,等于0的是子进程,大于0的就是父进程。

当我fork一返回,main函数在操作系统中就变成2份,而且轮番运行。
那么比如打印helloworld,父进程打印helloworld,子进程也在helloworld。
如果想限定某一个进程中执行某一段代码,那么就用if

【实例】

pid_t p1 = -1;
p1 = fork ();
if (p1 == 0)
    子进程操作
if (p1 > 0)
    父进程操作
if (p1 < 0)
perror
printf(“”helloworld , %d“”,getpid() ).
return 0;

输出结果:main()执行了两次,所以打印出两个helloworld,分别是父进程和子进程的。可以根据pid查看是谁打印的。
这就是宏观上的并行,微观上的 串行。。

======================================================

4.5子进程继承父进程的文件操作

子进程和父进程的文件指针是彼关联的,很像是O_APPEND标志后的位置
但实际测试时,有的时候会看到一个,有点像分别写,但是实际上不是。

有的时候,让父进程写1111,子进程写2222

open("xxx", O_RDWR | O_TRUNC)
pid = fork();
if (pid == 0)
    子进程操作1111
if (pid > 0)
    父进程操作2222

出现的结果有4种
1111
2222
11112222 (理想状况)
22221111

这个结果的成因是因为我们 的 程序太简短了,有可能open了之后,父进程写完就return了
return之前他会close掉我们的fd,导致子进程还没来得及写,文件就被关闭了。
如果想避免,很简单,sleep1就行了。

if (pid == 0)
子进程操作,打开xxx,写1111
if (pid > 0)
父进程操作,打开xxx,写2222

最后的结果只有2222,也就是分别写。

另一种操作——分别打开。。结果是分别写,很明显。
此时两个进程各自打开xxx,两个PCB已经独立了,文件表也独立了,因此2次读写是完全独立的 。
加o_APPEND标志仍然可以把文件指针关联起来实现接续写,真是神通广大。

[子进程的最终目的:要独立运行去执行另外的程序。]

======================================================

4.6 进程的诞生和消亡

进程0 系统态,内核启动的时候手动构成的
进程1 系统态,由进程0 ,fork产生
进程2 用户态,每个进程都是fork诞生的

进程之消亡
1、正常终止和异常终止
申请资源和释放资源。
内存丢失:内存泄漏
IO丢失:产生.swp文件而且不删是打不开的

linux的设计:父进程 给 子进程 收尸,每一个进程都有自己的父进程,一直往上一直往上,直到回收资源到进程0,这个时候就关机了。所以一个进程是不能没有父进程的,没有父进程就没法回收资源了

僵尸进程:
子进程先于父进程结束 //进程退出,回收资源,但进程本身的栈和描述结构体(task_struct)未回收
(而且父进程尚未收尸) //这两段8KB不是OS带来的,也不是OS能回收的,如果收尸就需要父进程
//linux的设计:父进程 给 子进程 收尸

收尸方法:
1、父进程可以使用wait()或waitpid回收子进程资源,并且获取子进程退出状态。
2、父进程结束时,自动回收子进程僵尸资源。(为了防止忘记调用wait/waitpid,防止内存泄漏)

孤儿进程:
父进程先于子进程结束。 子进程还没变僵尸,父进程先嗝屁了(hhhhh)
//子进程还没回收,父进程先死了,子进程成为孤儿进程
//系统规定:所有的孤儿进程都成为一个特殊进程(进程1,也就是init)
//”你们的父亲死了,就给你们安排一个新父亲。进程1死了也就关机了。”
//注意:ubuntu系统不是进程1,查询父进程的话可能不是1,但肯定是inti –user。

======================================================

4.7父进程wait回收子进程

wait的工作原理
子进程结束,系统向父进程发送SIGCHILD信号
父进程调用wait后阻塞
父进程被SGCHILD唤醒企业回收僵尸子进程
父进程没有子进程则返回错误

#include 
#include 
#include 
#include
int main(int argc,char *argv[])
{
        pid_t ret = -1;
        pid_t pid = -1;
        int status = -1; //

        pid = fork(); //创建子进程,返回两个pid,一个是父进程pid=0,一个是子进程pid>0

        if(pid == 0)
        {
                //子进程
                printf("child ready\n");
                printf("child pid = %d\n",getpid());
        }
        else if(pid > 0)
        {
                //父进程
                printf("parent ready\n");
                ret = wait(&status);        //wait是阻塞式的,不用sleep。【输出现象细思恐极】
                //status:提供给操作系统一个用户态指针,然后由内核来填充这个值
                printf("回收子进程,子进程pid=%d\n",ret);
        }
        else
        {
                //错误
                perror("fork");
        }
        printf("Code Access~\n");
        return 0;
}

输出结果:
parent ready —父进程先执行!由于wait产生阻塞,等子进程结束信号再回收【重要】
child ready —子进程执行
child pid = 4173
Code Access~ —子进程godie,系统发送SIGCHILD给父进程
回收子进程,子进程pid=4173 —父进程回收子进程
Code Access~ —父进程godie

获取子进程退出状态:WIFEXITED WIFSIGNALEDWEXITSTATUS 这几个宏
是否正常终止 是否非正常终止 得到正常终止main返回值

======================================================

4.8 waitpid指定回收哪一个子进程

waitpid支持阻塞和非阻塞两种工作方式。
+++++实验要求:使用waitpid。
非阻塞式:WNOHANG

竞争状态:多进程环境下,多个进程抢占系统资源(io内存CPU)

对操作系统来说是危险的。容易造成无法预料的结果。显而易见竞争状态是有害的,操作系统对竞争状态的出现设计了一些应对的方法,包括sleep也是一种,同步机制也是如此,以后会学到

======================================================

4.9 exec族函数:运行一个新的可执行程序

if(pid)的方法有缺陷,他在执行代码的时候有限制,不够灵活
我们必须知道源代码,而且源代码如果太长还不好控制。我们希望执行ls -la就不行。
exec族函数:用来运行一个新的可执行程序
可以直接把一个编译好的可执行程序直接加载运行。
使用exec族函数后,我们的父子进程是这样的:
子进程需要运行的程序被单独编写,生成一个a
主进程是父进程,fork创建子进程后在子进程中用exec来执行a,达到各自执行,同步运行。

execl execv

【举例:用子进程打印ls -l的信息】
命令:which ls 查询ls这个可执行程序 的 路径

else if(pid ==0)
{
    //子进程
    execl("/bin/ls" ,"ls", "-a", "-l", NULL);
    //执行ls -a -l 。注意执行的是路径指向的程序!
}

变参:可变参数,如果参数有多个字符串char *,那么后面要加一个NULL终止。

执行我们自己写的程序
execl(“./hello”, NULL );

这种程序我们一般比较少用,在设计框架的时候,做一个比较大的GUI 的时候我们会用到。只需要了解就行。

execl 使用全部路径 相对路径或绝对路径
execlp 使用相对路径 没有path参数,直接找$PATH对应的目录
execlp(“ls”, “-a”, “-l”, NULL);
execle e是环境变量
execlp(“/bin/ls” ,”ls”, “-a”, “-l”, NULL , envp);//改变这个进程的环境变量表
main的完全体: main(argc,**argv,**env)

关于环境变量:一个进程的环境变量表,是由父进程复制得来的,最早产自操作系统。在main函数中如果我们省略第三个参数,那么系统默认的环境变量就是父进程复制的。当然我们可以自己定义一个字符串数组envp,替换默认的环境变量数组。

======================================================

4.11 进程状态与system()

OS的进程调度:宏观之并行,微观之串行。

【进程状态】
进程链表: A-A-A-A-A-A-A-A…..A-A
1.就绪态:
0-0-0-0
已经准备好运行的进程
2.运行态

3.僵尸态
父进程先于子进程结束,进程的终止
4.等待态
并非是等待被运行,而是等一定的条件,然后进入就绪态
分为“浅度睡眠”“深度睡眠”。
其中浅度睡眠可被唤醒。类比我等饭等睡着了,朋友叫我我还能醒。
深度睡眠只能等待条件到达。类比我等饭睡着了,饭不做好我地震了都不会醒。
5.暂停态
并未进程的终止,而是被某些信号喊停了。

Linux的进程调度的目的是为了提高资源利用率,系统吞吐量。
一个进程的调度时间是有限的。多个进程同时运行的环境下,一旦规定的时间内单个进程没有结束,就意味着时间片用完,随后就进入就绪状态,重新等待着运行。

system()
执行shell 命令
用法贼几把简单:
system(“ls -l”);//这样就可以了,等同于打印ls -l信息。

======================================================

4.12 进程关系

1.无关系
当然不是完全的没关系。每一个进程都是0进程fork一步步得到的,但是这里的“无关系”
是说的不符合以下几种形式的 进程关系
2.父子进程关系
0 —–fork—->0

3.进程组 group
[组000] —–不可互通—– 0

4.回话 session
【回话| [组00][组11]】——-不可互通———【[xxx]】

======================================================

4.13 守护进程

守护进程daemon:长期运行在后台以提供服务,独立于控制台的进程。
举例:支持FTP的守护进程,syslog系统日志,cron时间管理守护进程

系统有很多d结尾的进程,其中很多是守护进程。ps -aux之后可以看到。里面有一项TTY项,他代表着”终端依赖”。
“比如pts/5”,代表着我们打开的终端窗口,显示?的就是不依赖终端的进程。

======================================================

4.14 建立一个守护进程

方法
create_daemon();

步骤:
1 子进程父进程退出
2 子进程setsid以创建新的 会话期目的:脱离控制台
3 定位目录到/
4 umask设0, 目的:取消文件权限屏蔽
5 关闭所有fd
6 进程0.1.2定位到/dev/null目的:输入输出丢到回收站

现象:ps -aux之后可以看到a.out正在后台运行,他没有依赖任何终端。ctrl+C不能结束他,只能kill

【程序例:创建一个守护进程】

#include 
#include 
#include
#include 
#include 
#include 
#include 


void create_daemon(void );

int main(int argc,char *argv[])
{
    create_daemon();//守护子进程

    while(1)
    {
        printf("F\n");
        sleep(1); 
    }

    printf("Code Access~\n");
    return 0;
}

void create_daemon()
{
        pid_t pid;
        pid_t sessionpid;
        pid = fork();

        if(pid > 0)
        {
                //父进程,退出,剩下 的 都是子进程
                exit(0);
        }
        if(pid < 0)
        {
                perror("fork");
                exit(-1);
        }

        sessionpid = setsid(); //设置会话,脱离控制台。不写会依赖pts/0
        printf("child process create\n"); 

        chdir("/");
        umask(0); //屏蔽权限要求

//以下操作是为了屏蔽显示,重定位输入输出
int i = 0;
int cnt = sysconf(_SC_OPEN_MAX);

for(i=0; i"/dev/null" , O_RDWR);
        open("/dev/null" , O_RDWR);
        open("/dev/null" , O_RDWR);
}

======================================================

4.15 使用Syslog记录调试日志

openlog
syslog
closelog

Ubuntu的syslog存在 /var/log/ 目录下,cat既可查询

======================================================

4.16不可重复执行的程序

1 创建文件,O_CREAT O_EXCL
2 创建失败则意味着,正在运行
3 atexit,记得删除这个文件

注意:ctrl+C 终止的进程将不会触发atexit

======================================================

4.17 IPC–进程间通讯

IPC是指:2个任意进程之间的通信。这是不容易的,因为Linux中使用了虚拟内存,进程与进程直接有屏障。
他们的地址不是相连的,由于安全性因素很难互相访问。
“他们彼此都不知道有对方,又谈何通讯呢?”

不过不用太担心,除了特别大的如GUI和大型服务器以外,99%都是不需要IPC的。

管道:无名管道和有名管道
原理:是使用内核管理的一段内存管理的

进程 2 3 4 11
(管道建立在这里)
内 核

3想要和4通讯,就 3-内核-4,这样通讯,就像管道一样。
管道可以理解为“公共缓冲区”,非常重要的:管道是单向通讯的。3给4发消息的管道,也只能从左往右。
这里的”只能从左往右”是不绝对的,他是半双工。同时只能走一个方向,“潮汐车道”。

无名管道:父进程创建管理后fork子进程,子进程继承父进程的管道fd。
必须是父子进程关系才能使用无名管道。

父进程 创建 管道,得到2个fdpipe
父进程fork,会继承父进程打开的fd,所以也有2个fd
父进程关掉读的,子进程关掉写的。成为单工
在父进程写,在子进程读。管道就ok了

pipe writeread close
无名管道缺陷: 1必须是父子进程关系
2管道的半双工通讯

有名管道:不是父子进程也能用的管道通信

固定一个文件名,2个进程分别使用mkfifo创建有名管道,分别open打开获取fd,然后一个读一个写。
mkfifo open write read close

有名管道多一个文件,是为了让两个非父子进程接头、对暗号的。

======================================================

4.18 SystemV IPC介绍

system5 ipc和管道的差别:
system5 ipc不仅仅使用open等文件操作方式,而是提供一些内核实现了的新的API。

管道有一个“一次性”的特点,使用管道的公共内存只能被读一次。读完之后就删掉了,所以有时候会出现抢读的问题

三个方法: 特点
信号量 实质是计数器。是一个可以计数 的变量。int a
A告诉a+1,B发现a加了1,就实现了通信
主要用于互斥和同步。比如A在调用公共资源,B就不能调用。比如signal=0,B不可以用。
同步:A走一步,signal+1。B看signal加了1,那么B走一步。
实际上A和B显而易见是异步的,但我们可以 建立这种同步机制。

消息队列 本质是队列,就是内核维护的fifo。排成一列的消息,类似结构体/链表。
A向队列放入消息,B从队列中读消息。

共享内存 大片内存直接映射。这里的共享内存要比其他两种数据信息量要大很多。
比如A和B之间交换一些图像信息
网络摄像机:A发送图像给B,B处理图像编解码,C上传到网络。
A复制一份给B的话,在共享内存中对图像进行一份复制是很占用时间的
那么更好的方法就是大段的内存映射,重点是映射
A只负责图像数据存到哪个区域,B只要去拿就好了,不管资源去哪、或资源怎么来

不同情况下,应对不同样的情景使用不同的方法

实质:和管道一样,是使用【内核维护的一段公共内存】

剩下的2种IPC
1信号 (第五课程讲)
2unix套接字 (网络编程讲)

/////———————————————————————

6.1 Linux 高级IO
Linux一切皆是文件。
高级IO里面也是读文件和写文件,高级IO要解决一些复杂的问题

非阻塞IO

阻塞IO:放在旁边照顾后面的进程,这是LINUX内核的一个常态。
父进程阻塞,等待回收子进程,收到SIGCHILD就去回收子进程。
常见的 阻塞式:wait pause sleep;

阻塞式的好处:非常有利于系统的性能发挥,他是Linux系统设计的默认方式。让CPU时刻在工作状态下。

为什么要实现非阻塞?
阻塞式在多路io时候会遇到问题
比如我同时读鼠标、键盘。
我希望动了键盘来唤醒一个进程,但是设计成了阻塞式鼠标触发,这样我鼠标不动可能会卡一辈子。

你可能感兴趣的:(纯真的自学笔记喵,编程,linux,应用)