当前位置:   article > 正文

wjl的正点原子应用笔记_o_trunc

o_trunc

2.4写文件

 #include <sys/types.h>
 #include <sys/stat.h>
 #include <fcntl.h>
 #include <unistd.h>
#include <stdio.h>
int main(void)
{
    int fd;
    int count;

    fd = open("./test.txt", O_WRONLY | O_CREAT | O_EXCL,0644);
%./是当前目录  二参是宏 只写方式打开 创建  检验是否存在 不存在再创建
% sugo super user group other  
%000 000 000 000   读写执行排列 读42执行1
    if(-1==fd)
    {
        printf("Error");
        return 1;
    }
    printf("open ok");

    count = write(fd,"hello world",11);

    if(-1=count)
    {
        printf("Error");
        close(fd);
    }

    printf("%d",count);
    close(fd);
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

2.5读文件

#include <sys/types.h>
 #include <sys/stat.h>
 #include <fcntl.h>
 #include <unistd.h>
#include <stdio.h>
int main(void)
{
    int fd;
    int ret;
    char buffer[128]={0};

    fd = open("./test.txt",O_RDONLY);%要先打开文件
    if(-1 == fd )
    {
        printf("open error");
        return 1;
    }

    printf("open ok");

    ret =  read(fd,buffer,11);%读进一个buffer 
    if(-1==ret)
    {
        printf("read error");
        close(fd);
        return 1;
    }

    printf("read ok");
    printf("read %d byte is %s",ret,buffer);
    return 0;


}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

2.7lseek

函数 一参fd 二参 偏移量 三餐 相对什么位置的偏移量
可以设置偏移量为0 相对文件尾 计算 文件长度
返回值是 执行lseek之后相对文件头的偏移量

2.8作业 复制文件

#include <sys/types.h>
 #include <sys/stat.h>
 #include <fcntl.h>
 #include <unistd.h>
#include <stdio.h>
int main(int argc,char *argv[])
%一参 传参个数 注意执行时的./copy.c 也算一个参数
%二参 传参的文本 一参就是 ./copy.c
{
    int fd1,fd2,length,set;
    int ret1, ret2;
    char buf[128]={0};

    fd1 = open(argv[1],O_RDONLY);%只读
    if(-1==fd1)
    {
        printf("open source file failed\n");
        return 1;
    }

    length=lseek(fd1,0,SEEK_END);%计算源文件长度方便设置后续读写 
    %但此方式移动了读写位置到文件尾 所以加一句 设置回到文件头
    set=lseek(fd1, 0, SEEK_SET);%回到文件头
    ret1=read(fd1,buf,length);%看读了几个字节
    printf("buffer is %s",buf);%看读进来了什么
    if(-1 ==ret1)
    {
        printf("read source file failed\n");
        close(fd1);
        return 1;
    }

    fd2=open(argv[2],O_WRONLY);%这里argv 2就是目标文件,在执行时写下
    if(-1==fd2)
    {
        printf("open target file failed\n");
        return 1;
    }

    ret2=write(fd2,buf,length);
    if(-1 ==ret2)
    {
        printf("write  target file failed\n");
        close(fd2);
        return 1;
    }
    printf("ret1 %d  ret2 %d  length %d\n",ret1,ret2,length);
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

3.1 linux文件管理

硬盘最小单位为扇区,一个扇区512B 0.5kb
系统读文件会一次性读8个块,块是文件存取最小单位,一个块4kb

硬盘分区分为数据区和inode区(用于存放inode表)
inode表中放的是一个一个的inode(也叫inode节点)
来自正点原子开发手册

不同的 inode 就可以表示不同的文件,每一个文件都必须对应一个 inode,inode 实质上是一个结构
体,这个结构体中有很多的元素,不同的元素记录了文件了不同信息,譬如文件字节大小、文件所有者、文
件对应的读/写/执行权限、文件时间戳(创建时间、更新时间等)、文件类型、文件数据存储的 block(块)
位置等等信息

每个文件对应一个唯一的inode,ls -i可以看到文件inode

3.2errno

err错误编号


```c
#include <errno.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main(void) {
    int fd;
    printf("errno = %d\n",errno);
    fd = open("./tets.txt", O_WRONLY); 
    if(-1 == fd) {
        printf("errno = %d\n",errno);
    }
    printf("strerror = %s\n",strerror(errno));
     perror("");//输出No such file or directory
    perror("open error");//该句输出open error: No such file or directory
    return 0;
}//错误编号是大于0的整数 0是正常的
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

大部分的系统调用库函数会设置errno
可以man 2 open 等看一下return value 有无errno设置

strerror输入为errno 输出为字符串 错误原因

perror函数可以直接得到错误编号的字符串描述信息

3.3exit和_exit

exit()是一个标准 C 库函数,而_exit()和_Exit()是系统调用。

执行 exit()会执行一些清理工作,最后调用_exit()函数。
_exit()和_Exit()两者等价

一般原则程序执行正常退出 return 0,而执行函数出错退出 return -1,

main 函数中使用 return 后返回,return 执行后把控制权交给调用函数,结束该进程。

调用_exit()函数会清除其使用的内存空间,并销毁其在内核中的各种数据结构,

关闭进程的所有文件描述符,并结束进程、将控制权交给操作系统。

3.4 空洞文件

文件读写偏移量可以大于文件本身
在这里插入图片描述
用lseek函数跳过文件尾 到2k位置读写了,中间产生了空洞
空洞文件应用场景,多线程下对文件的写操作。
单线程是从头写到尾,多线程是一个文件分几个区域,每个部分又每个线程负责各自的区域,同时写入互不干扰,所有线程写完各自的部分 才不是空洞文件。

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

int main(void) 
{
    int ret,fd,i;
    char buf[1024];
    fd = open("./hole.txt",O_WRONLY|O_CREAT,0644);
    if(-1==fd)
    {
        perror("open");
        return 1;
    }

    lseek(fd,4096,SEEK_SET);

    memset(buf,0xff,1024);

    for(i=0; i<4; i++)
    {
        ret = write(fd,buf,sizeof(buf));
        if(ret==-1)
        {
            perror("write");
            return 1;
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

3.5 open函数的O_APPEND O_TRUNC标志

O_TRUNC 这个标志的,如果使用了这个标志,调用 open 函数打开文件的时候会将文件
原本的内容全部丢弃,文件大小变为 0;

如果 open 函数携带了 O_APPEND 标志,调用 open 函数打开文件,
当每次使用 write()函数对文件进行写操作时,都会自动把文件当前位置偏移量移动到文件末尾,从文件末
尾开始写入数据,也就是意味着每次写入数据都是从文件末尾开始。

带了这个标志写操作,用lseek更改读指针的位置没用的
但可以改读的位置,读不受该标志的影响

3.6 多次打开同一个文件

三次调用 open 函数打开同一个文件得到的文件描述符fd1 fd2 fd3分别为 6、7、8,最小的是3,
如果用vscode就是6开始,345被vscode用了

通过任何一个文件描述符对
文件进行 IO 操作都是可以的,但是需要注意是,调用 open 函数打开文件使用的是什么权限,
则返回的文
件描述符就拥有什么权限

3.7复制文件描述符

dup 和 dup2函数
dup只能由系统分配文件描述符,dup2可以指定
如 fd2=dup(fd1);
fd3 = dup2(fd1,100);//这样print出来的fd3就是100

利用复制文件描述符来实现文件接续写 不使用O_APPEND

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

int main(void) 
{
    int ret,fd1,i,fd2;
    char buf1[4],buf2[4],buf3[4];

    strcpy(buf1, "good");
    strcpy(buf2, "milk");
    fd1 = open("./hole.txt",O_TRUNC|O_RDWR,0644);//开放读写执行权限
    if(-1==fd1)
    {
        perror("open");
        return 1;
    }
     
    fd2 = dup(fd1);

    for(i=0; i<4; i++)
    {
        ret=write(fd1, buf1,sizeof(buf1));

        if(ret==-1)
        {
            perror("write");
            return 1;
        }

         ret=write(fd2, buf2,sizeof(buf2));

        if(ret==-1)
        {
            perror("write");
            return 1;
        }
    }

    ret=read(fd1, buf3,sizeof(buf3));
    if(ret==-1)
        {
            perror("read");
            return 1;
        }
    
    printf("%x %x %x %x",buf3[0],buf3[1],buf3[2],buf3[3]);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52

3.8文件共享

一个文件被多个不同的文件描述符同时进行IO操作
多进程或多线程时可以同时操作一个大文件节省时间。

多次调用open 使用dup dup2函数得到多个不同的文件描述符
进程可以理解为应用程序
在这里插入图片描述
(2)不同进程中分别使用 open 函数打开同一个文件,其数据结构关系图如下所示:
在这里插入图片描述
在这里插入图片描述
复制的文件描述符不会产生新的文件表

3.9原子操作 竞争冒险

1 O_APPEND标志实现不会多个进程同时写入覆盖写入的情况,永远从文件尾部开始
原子操作要么一步也不执行,一旦执行,必须要执行完所有步骤

进程 A 和进程 B 都对同一个文件进行追加写操作,导致进程 A 写入的数据覆盖了进程 B 写入的数据,

解决办法就是将“先定位到文件末尾,然后写”这两个步骤组成一个原子操作即可,那如何使其变成一个原子操作呢?

答案就是 O_APPEND 标志。

2 系统调用pread pwrite
调用 pread pwrite 相当于调用 lseek 后再调用 read write;
在这里插入图片描述
offset是从文件头开始算的
调用不会改变 文件表 的offset
但是调用 read write 会改变 文件表里面的偏移量

3文件创建竞争
可以用O_EXCL标志 ,存在就不创建并报错返回。

3.10 fcntl 和 ioctl

fcntl (助记fdcontrol,相当于管理fd的工具箱)

*fcntl()函数可以对一个已经打开的文件描述符执行一系列控制操作,

  • 复制一个文件描述符(与 dup、dup2 作用相同)、

当 cmd=F_DUPFD 时,它的作用会根据 fd 复制出一个新的文件描述符,此时需要传入第三个参数,第
三个参数用于指出新复制出的文件描述符是一个大于或等于该参数的可用文件描述符(没有使用的文件描
述符)
/* 使用 fcntl 函数复制一个文件描述符 */

 fd2 = fcntl(fd1, F_DUPFD, 0);
  • 1
  • 获取/设置文件描述符标志、
    文件描述符标志(目前就只有一个close-on-exec):

它仅仅是一个标志,当进程fork一个子进程的时候,在子进程中调用了exec函数时就用到了这个标志。意义是执行exec前是否要关闭这个文件描述符。要把文件描述符标志和文件状态标志区分开来。

F_GETFD F_SETFD

  • 获取/设置文件状态标志等*
/* 获取文件状态标志 */
 flag = fcntl(fd, F_GETFL);
/* 设置文件状态标志,添加 O_APPEND 标志 */
 ret = fcntl(fd, F_SETFL, flag | O_APPEND);

  • 1
  • 2
  • 3
  • 4
  • 5

ioctl
(助记iocontrol,相当于管理fd的工具箱)进阶篇介绍

3.11截断文件

ftruncate()使用文件描述符 fd 来指定目标文件,而 truncate()则直接使用文件路径 path 来指定目标文件,其功能一样。
将文件截断为参数 length 指定的字节长度

如果文件目前的大小小于参数 length 所指定的大小,则将其进行扩展,对扩展部分进行读取将得到空字节"\0

使用 ftruncate()函数进行文件截断操作之前,必须调用 open()函数打开该文件得到文件描述符,并且必须要具有可写权限

也就是调用 open()打开文件时需要指定 O_WRONLY 或 O_RDWR。

调用这两个函数并不会导致文件读写位置偏移量发生改变,所以截断之后一般需要重新设置文件当前的读写位置偏移量,

以免由于之前所指向的位置已经不存在而发生错误(譬如文件长度变短了,文件当前所指向的读写位置已不存在)

调用成功返回 0,失败将返回-1,并设置 errno 以指示错误原因。

 /* 使用 ftruncate 将 file1 文件截断为长度 0 字节 */
 if (0 > ftruncate(fd, 0)) {
 perror("ftruncate error");
 exit(-1);
 }
 /* 使用 truncate 将 file2 文件截断为长度 1024 字节 */
 if (0 > truncate("./file2", 1024)) {
 perror("truncate error");
exit(-1);
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

4.1 标准IO库 4.2 FILE 指针

  • 标准 I/O 库在用户空间维护了自己的 stdio 缓冲区,所以标准 I/O 是带有缓存的,

    而文件 I/O 在用户空间是不带有缓存的,所以在性能、效率上,标准 I/O 要优于文件 I/O。

    标准 I/O 是标准 C 库函数标准 I/O 在不同操作系统之间相比于文件 I/O 具有更好的可移植性。

  • 在文件IO中所有的操作围绕fd文件描述符去完成,而标准IO围绕文件指针完成
    FILE 是一个结构体数据类型,它包含了标准 I/O 库函数为管理文件所需要的所有信息,
    包括用于实际I/O 的文件描述符、指向文件缓冲区的指针、缓冲区的长度、当前缓冲区中的字节数以及出错标志等。
    FILE数据结构定义在标准 I/O 库函数头文件 stdio.h 中。

4.3 标准输入、标准输出和标准错误

每个进程启动之后都会默认打开标准输入、标准输出以及标准错误,得到三个文件描述符,
即 0、1、 2,其中 0 代表标准输入、1 代表标准输出、2 代表标准错误;
在应用编程中可以使用
宏 STDIN_FILENO、STDOUT_FILENO 和 STDERR_FILENO 分别代表 0、1、2,

*返回值:调用成功返回一个指向 FILE 类型对象的指针(FILE ),该指针与打开或创建的文件相关联,
后续的标准 I/O 操作将围绕 FILE 指针进行。如果失败则返回 NULL,并设置 errno 以指示错误原因
这些宏定义在 unistd.h 头文件中:

4.4 打开文件
打开文件fopen(path,mode)
在这里插入图片描述
fclose()关闭文件
int fclose(FILE *stream);

参数 stream 为 FILE 类型指针,调用成功返回 0;失败将返回 EOF(也就是-1),并且会设置 errno 来指示错误原因。

4.5 读文件和写文件 4.6 fseek

ptr:fread()将读取到的数据存放在参数 ptr 指向的缓冲区中;
size:fread()从文件读 nmemb 个数据项,每个数据项大小为 size 个字节,所以共读取的数据大小为 nmemb * size 个字节。
nmemb:参数 nmemb 指定了读取数据项的个数。
stream:FILE 指针。

如果发生错误,则 fwrite()返回的值将小于参数 nmemb(或者等于 0)。

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

int main(void) 
{
   char buf[]="hello world";
   FILE *fp = NULL;

   //open the file

   if(NULL == (fp= fopen("./hole.txt", "r+")))
   {
       perror("fopen");
       exit(-1);
   }
   printf("file is open successfully\n");

   if(sizeof(buf)>fwrite(buf, 1, sizeof(buf), fp))
   {
       printf("fwrite failed\n");
       fclose(fp);
       exit(-1);
   }

   printf("data written successfully\n");
   fclose(fp);
   exit(0);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

fseek返回值:成功返回 0;发生错误将返回-1,并且会设置 errno 以指示错误原因;
与 lseek()函数的返回值意义不同,这里要注意!

在这里插入图片描述
在这里插入图片描述

4.7 检查或复位状态

  • feof

当文件的读写位置移动到了文件末尾时,end-of-file 标志将会被设置。

库函数 feof()用于测试参数 stream 所指文件的 end-of-file 标志,如果 end-of-file 标志被设置了,
则调用feof()函数将返回一个非零值,如果 end-of-file 标志没有被设置,则返回 0。
在这里插入图片描述

  • ferror

库函数 ferror()用于测试参数 stream 所指文件的错误标志,如果错误标志被设置了,则调用 ferror()函数
将返回一个非零值,如果错误标志没有被设置,则返回 0。

在这里插入图片描述

- clearerr()函数

库函数 clearerr()用于清除 end-of-file 标志和错误标志,当调用 feof()或 ferror()校验这些标志后,通常需
要清除这些标志,避免下次校验时使用到的是上一次设置的值,此时可以手动调用 clearerr()函数清除标志。

此函数没有返回值,调用将总是会成功!
对于 end-of-file 标志,除了使用 clearerr()显式清除之外,当调用 fseek()成功时也会清除文件的 end-of file 标志。

4.8 格式化IO

C库有五个格式化输出函数 printf fprintf sprintf dprintf snprintf

  • fprintf()

可将格式化数据写入到由 FILE 指针指定的文件中,
譬如将字符串“Hello World”写入到标准错误:fprintf(stderr, “Hello World!\n”);
向标准错误写入数字 5:fprintf(stderr, “%d\n”, 5);
函数调用成功返回写入到文件中的字符数;失败将返回一个负值!

  • dprintf()

将格式化数据写入到由文件描述符 fd 指定的文件中,譬如将字符串“Hello World”写入到标准错误:
dprintf(STDERR_FILENO, “Hello World!\n”);

  • sprintf()函数

一般会使用这个函数进行格式化转换,并将转换后的字符串存在缓冲区中
譬如将数字 100 转换为字符串"100",将转换后得到的字符串存放在 buf 中:

格式化数据存储在由参数 buf 所指定的缓冲区中

char buf[20] = {0};
sprintf(buf, "%d", 100);
  • 1
  • 2
  • snprintf()函数
    为了解决这个问题,引入了 snprintf()函数;
    在该函数中,使用参数 size 显式的指定缓冲区的大小,如果写入到缓冲区的字节数大于参数 size 指定的大
    小,超出的部分将会被丢弃!如果缓冲区空间足够大,snprintf()函数就会返回写入到缓冲区的字符数,与
    sprintf()函数相同,也会在字符串末尾自动添加终止字符’\0’。若发生错误,snprintf()将返回一个负值!

4.9 内核IO缓冲

4.9.1 刷新内核缓冲

系统调用 fsync()将参数 fd 所指文件的内容数据和元数据写入磁盘,只有在对磁盘设备的写入操作完成之后,fsync()函数才会返回
参数 fd 表示文件描述符,函数调用成功将返回 0,失败返回-1 并设置 errno 以指示错误原因。

元数据并不是文件内容本身的数据,而是一些用于记录文件属性相关的数
据信息,譬如文件大小、时间戳、权限等等信息,这里统称为文件的元数据

系统调用 fdatasync()与 fsync()类似,不同之处在于 fdatasync()仅将参数 fd 所指文件的内容数据写入磁
盘,并不包括文件的元数据

#include <unistd.h>
int fsync(int fd);
  • 1
  • 2

4.9.3 绕过内核缓冲

内核 2.4 版本开始,Linux 允许应用程序在执行文件 I/O 操作时绕过内核缓冲区,从用户空间
直接将数据传递到文件或磁盘设备,把这种操作也称为直接 I/O(direct I/O)或裸 I/O(raw I/O)。

例如,某应用程序的作用是测试磁盘设备的读写速率,那么在这种应用需要下,
我们就需要保证 read/write 操作是直接访问磁盘设备,而不经过内核缓冲,如果不能得到这样的保证,
必然会导致测试结果出现比较大的误差。

对于大多数应用程序而言,使用直接 I/O 可能会大大降低性能,这是因为为了提高 I/O 性能,
内核针对文件 I/O 内核缓冲区做了不少的优化,譬如包括按顺序预读取、在成簇磁盘块上执行 I/O、允许访问
同一文件的多个进程共享高速缓存的缓冲区。如果应用程序使用直接 I/O 方式,将无法享受到这些优化措施所带来的性能上的提升,
直接 I/O 只在一些特定的需求场合,譬如磁盘速率测试工具、数据库系统等。
我们可针对某一文件或块设备执行直接 I/O,要做到这一点,需要在调用 open()函数打开文件时,
指定O_DIRECT 标志,该标志至 Linux 内核 2.4.10 版本开始生效,譬如:

fd = open(filepath, O_WRONLY | O_DIRECT)
  • 1

8.4 进程对信号的处理signal与 sigaction

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

static void int_sign(int sig)
{
    printf("receive: %d\n", sig);
    printf("capture signal");
}

int main(int argc, char **argv)
{
    
    sig_t ret=NULL;

    ret = signal(SIGINT,(sig_t)int_sign);
    if(SIG_ERR == ret)//注意这里的报错条件
    {
        perror("signl");
        return -1;
    }

    for(;;){}循环卡死

    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

9.1 进程与程序

shell 进程会将这些参数传递给加载器,加载器加载应用程序时会将其传递给应用程序引导代码,
当引导程序调用 main()函数时,在由它最终传递给 main()函数,如此一来,在我们的应用程序当中便可以获取到命令行参数了

操作系统下的应用程序在运行 main()函数之前需要先执行一段引导代码,最终由这段引导代码去调用应用程序的 main()函数
在编译链接时,由链接器将引导代码链接到我们的应用程序当中,一起构成最终的可执行文件。

加载器是操作系统中的程序,当执行程序时,加载器负责将此应用程序加载内存中去执行。

#include <stdio.h>
#include <stdlib.h>
static void bye(void)
{
 puts("Goodbye!");
}
int main(int argc, char *argv[])
{
 if (atexit(bye)) {
 fprintf(stderr, "cannot set exit function\n");//失败返回非0 值 成功返回0
 exit(-1);
 }
 exit(0);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

atexit()库函数用于注册一个进程在正常终止时要调用的函数

进程:进程是一个动态过程,它是程序的一次运行过程,当应用程序被加载到内存中运行之后
它就称为了一个进程,当程序运行结束后也就意味着进程终止,这就是进程的一个生命周期。

进程pid获取函数

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
 pid_t pid = getpid(); //获取本进程 pid
 printf("本进程的 PID 为: %d\n", pid);
 pid = getppid(); //获取父进程 pid
 printf("父进程的 PID 为: %d\n", pid);
 exit(0);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

9.2 环境变量

获取环境变量

进程的环境变量是从其父进程中继承过来的,譬如在 shell 终端下执行一个应用程序,
那么该进程的环境变量就是从其父进程(shell 进程)中继承过来的。新的进程在创建之前,会继承其父进程的环境变量副本。
环境变量存放在一个字符串数组中,在应用程序中,通过 environ 变量指向它,environ 是一个全局变量,
在我们的应用程序中只需申明它即可使用,如下所示:

extern char **environ; // 申明外部全局变量 environ
int main(int argc, char *argv[])
{
 int i;
 /* 打印进程的环境变量 */
 for (i = 0; NULL != environ[i]; i++)//通过字符串数组元素是否等于 NULL 来判断是否已经到了数组的末尾。
{
	 puts(environ[i]);
     exit(0);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

获取环境变量值 ,vscode终端没用,argv【1】传PATH

#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
 const char *str_val = NULL;
 if (2 > argc) {
 fprintf(stderr, "Error: 请传入环境变量名称\n");
 exit(-1);
 }
 /* 获取环境变量 */
 str_val = getenv(argv[1]);
 if (NULL == str_val) {
 fprintf(stderr, "Error: 不存在[%s]环境变量\n", argv[1]);
 exit(-1);
 }
 /* 打印环境变量的值 */
 printf("环境变量的值: %s\n", str_val);
 exit(0);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

增环境变量

putenv

#include <stdlib.h>
int putenv(char *string);
  • 1
  • 2

string:参数 string 是一个字符串指针,指向 name=value 形式的字符串。
返回值:成功返回 0;失败将返回非 0 值,并设置 errno。

该函数调用成功之后,参数 string 所指向的字符串就成为了进程环境变量的一部分了
不能随意修改参数 string 所指向的内容,这将影响进程的环境变量,出于这种原因,
参数 string 不应为自动变量(即在栈中分配的字符数组)。因为程序退出后会消失 所以设置的变量没用了

setenv将环境变量字符串拷贝到自己的缓冲区中,而putenv直接使用原来的 用户定义的
setenv 可以使用自动变量
返回值:成功返回 0;失败将返回-1,并设置 errno。

int setenv(const char *name, const char *value, int overwrite)

写入环境变量已存在 ,overwrite为0 则不覆盖 为1则重写覆盖。

我们还可以通过一种更简单地方式向进程环境变量表中添加环境变
量,用法如下:
NAME=value ./app
在执行程序的时候,在其路径前面添加环境变量,以 name=value 的形式添加,如果是多个环境变量,
则在./app 前面放置多对 name=value 即可,以空格分隔

删环境变量

unsetenv()函数可以从环境变量表中移除参数 name 标识的环境变量

清空
可以通过将全局变量 environ 赋值为 NULL来清空所有变量。
environ = NULL;

clearenv()函数内部的做法其实就是将environ赋值为NULL。在某些情况下,使用setenv()函数和clearenv()
函数可能会导致程序内存泄漏,前面提到过,setenv()函数会为环境变量分配一块内存缓冲区,随之称为进
程的一部分;而调用 clearenv()函数时没有释放该缓冲区(clearenv()调用并不知晓该缓冲区的存在,故而也
无法将其释放)

9.5fork创建子进程

理解 fork()系统调用的关键在于,完成对其调用后将存在两个进程,一个是原进程(父进程)、另一个
则是创建出来的子进程,并且每个进程都会从 fork()函数的返回处继续执行,会导致调用 fork()返回两次值,
子进程返回一个值、父进程返回一个值。在程序代码中,可通过返回值来区分是子进程还是父进程。
fork()调用成功后,将会在父进程中返回子进程的 PID,而在子进程中返回值是 0;如果调用失败,父进程返回值-1,不创建子进程,并设置 errno。
fork()调用成功后,子进程和父进程会继续执行 fork()调用之后的指令,子进程、父进程各自在自己的进
程空间中运行。事实上,子进程是父进程的一个副本,譬如子进程拷贝了父进程的数据段、堆、栈以及继承
了父进程打开的文件描述符,父进程与子进程并不共享这些存储空间,这是子进程对父进程相应部分存储
空间的完全复制,执行 fork()之后,每个进程均可修改各自的栈数据以及堆段中的变量,而并不影响另一个进程。
虽然子进程是父进程的一个副本,但是对于程序代码段(文本段)来说,两个进程执行相同的代码段,
因为代码段是只读的,也就是说父子进程共享代码段,在内存中只存在一份代码段数据。

C 库函数 exit()建立在系统调用_exit()之上,这两个函数在 3.3 小节中向大家介绍过,这里我们强
调,在调用了 fork()之后,
父、子进程中一般只有一个会通过调用 exit()退出进程,而另一个则应使用_exit()退出

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

int main(int argc, char **argv)
{
    pid_t pid;%pid_t类型

    pid = fork();

    switch (pid)
    {
         case -1:
         perror("fork failed");break;

         case 0:
         printf("i am child process pid:%d\
         parent pid %d \n",getpid(),getppid());
        _exit(0);
        
        default:
        printf("i am parent process: %dchild pid:%d\n",getpid(),pid);%父进程的fork返回的是子进程的pid
        exit(0);
    }
   
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

9.6 父子进程间文件共享

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

int main(int argc, char **argv)
{
    
    int i;int fd;
    pid_t pid;
    fd=open("./x.txt",O_TRUNC|O_WRONLY);废弃之前的文件内容TRUNC
    if(0>fd)
    {
        perror("open");
        exit(-1);
    }
    pid = fork();
    switch (pid)
    {
    case -1:
        perror("fork");
        close(fd);
        exit(-1);
    case 0:
        for(i=0; i<4; i++)
        {    
            if(write(fd,"1122",4)<0)
            perror("write");
        }
        close(fd);
        _exit(0);
    default:
        for(i=0; i<4; i++)
        {    
            if(write(fd,"AABB",4)<0)
            perror("write");
        }
        close(fd);
        exit(0);
    }   
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

⚫ 父进程希望子进程复制自己,使父进程和子进程同时执行不同的代码段。这在网络服务进程中是常
见的,父进程等待客户端的服务请求,当接收到客户端发送的请求事件后,调用 fork()创建一个子
进程,使子进程去处理此请求、而父进程可以继续等待下一个服务请求。
⚫ 一个进程要执行不同的程序。譬如在程序 app1 中调用 fork()函数创建了子进程,此时子进程是要
去执行另一个程序 app2,也就是子进程需要执行的代码是 app2 程序对应的代码,子进程将从 app2
程序的 main 函数开始运行。这种情况,通常在子进程从 fork()函数返回之后立即调用 exec 族函数
来实现

9.7 vfork

最原始的fork函数执行之后会复制父进程的数据段堆段等,耗费很多时间,因为子进程之后多会调用exec()函数执行别的程序,
,exec()后从新的程序main开始执行,初始化新程序的堆段数据段等,此时复制了父进程的数据就是无用的。

Linux引入Copy-On-Write(写时复制)技术后,在子进程只需要读父进程的东西时,并不复制,共享同一空间,
而需要写,修改数据时,才去复制父进程的数据。

COW 原理
fork()之后,kernel把父进程中所有的内存页的权限都设为read-only,然后子进程的地址空间指向父进程。当父子进程都只读内存时,相安无事。当其中某个进程写内存时,CPU硬件检测到内存页是read-only的,于是触发页异常中断(page-fault),陷入kernel的一个中断例程。中断例程中,kernel就会把触发的异常的页复制一份,于是父子进程各自持有独立的一份。
fork()的实际开销就是复制父进程的页表以及给子进程创建惟一的进程描述符,创建虚拟空间,指向了父进程的物理空间。

vfork()更激进,不创建虚拟空间。
虽然 vfork()系统调用在效率上要优于 fork(),但是 vfork()可能会导致一些难以察觉的程序 bug,
所以尽量避免使用 vfork()来创建子进程,除非要求速度极快场合。

9.8父子进程竞争关系

用fork之后无法确定谁先访问CPU,可以使用信号的方式阻塞父进程让子进程优先运行如下

sigsuspend()函数会将参数 mask 所指向的信号集来替换进程的信号掩码,也就是将进程的信号掩码设置
为参数 mask 所指向的信号集,然后挂起进程,直到捕获到信号被唤醒(如果捕获的信号是 mask 信号集中
的成员,将不会唤醒、继续挂起)、并从信号处理函数返回,一旦从信号处理函数返回,sigsuspend()会将进
程的信号掩码恢复成调用前的值。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
static void sig_handler(int sig)
{
    printf("signal received\n");
}

int main(void)
{
    struct sigaction act={0};
    act.sa_flags = 0;
    act.sa_handler = sig_handler;
    sigset_t wait_mask;
    sigemptyset(&wait_mask);

    if(-1 == sigaction(SIGUSR1,&act,NULL))
    {
        perror("sigaction");
        exit(-1);
    }

    switch (fork())
    {
    case -1:
        perror("fork");
        exit(-1);
    
    case 0:
        printf("紫禁城");
        sleep(1);
        kill(getppid(),SIGUSR1);
        _exit(0);

    default:
        if(-1 != sigsuspend(&wait_mask))
        {
            printf("suspend failed");
            exit(-1);
        }
        printf("parent!!!!!!\n");
        exit(0);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46

sigsuspend()始终返回-1,并设置 errno 来指示错误(通常为 EINTR),表示被信号所中断,如果调用失败,将 errno 设置为 EFAULT。

13.1非阻塞IO

NONBLOCK方式打开,轮询方式查看鼠标动作,不能用perror否则第一次没动鼠标直接抛出异常程序结束。

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

int main(int argc, char **argv)
{
    char buf[100];
    int fd ,ret;

    fd = open("/dev/input/event2",O_RDONLY|O_NONBLOCK);
    if(-1 == fd)
    {
        perror("open file failed\n");
        exit(-1);
    }

    
    memset(buf,0,sizeof(buf));
    for(;;)
    {
        ret = read(fd,buf,sizeof(buf));
        if(0 < ret)
        {
            printf("read %d bytes is %s\n",ret,buf);
            close(fd);
            exit(0);  
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

非阻塞方式读取鼠标键盘数据,0是键盘文件描述符,这样鼠标键盘无论谁先有数据都可以读取到

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

int main(int argc, char **argv)
{
    char buf[100];
    int fd ,ret,flag;

    flag =  fcntl(0,F_GETFL);//先获取原来的标志位
    flag |= O_NONBLOCK;//加上非阻塞IO标志位
    fcntl(0,F_SETFL,flag);

    fd = open("/dev/input/event2",O_RDONLY|O_NONBLOCK);
    if(-1 == fd)
    {
        perror("open file failed\n");
        exit(-1);
    }

    
    memset(buf,0,sizeof(buf));

    for(;;)
    {
        ret = read(fd,buf,sizeof(buf));
        if(ret>0)
            printf("mouse read %d bytes is %s\n",ret,buf);

        memset(buf,0,sizeof(buf));

        ret = read(0,buf,sizeof(buf));
        if(ret>0)
            printf("keyboard read %d bytes is %s\n",ret,buf);
    }
   
    
    close(fd);
    exit(0);  
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家小花儿/article/detail/186477
推荐阅读
相关标签
  

闽ICP备14008679号