听说C语言很难?怎么不来看看我这篇(八)UNIX系统调用

1.写在前面

前面我们已经介绍完了C语言的一些常用知识,以及标准的输入输出,这篇博客我们我们来介绍UNIX系统接口的调用。UNIX操作系统通过一系列的系统调用提供服务,这些系统调用用实际上是操作系统内的函数,它们可以被用户程序调用。本篇博客主要介绍C语言程序中使用一些重要的系统调用。

2.文件描述符

Linux中一切皆文件,所以我们需要了解一下文件描述符。

在UNIX操作系统中,所有的外围设备都被看作文件系统中的文件,因此,所有的输入/输出都要通过读文件或写文件完成。

通过情况下,在读或写文件之前,必须先将这个意图通知系统,该过程称为打开文件。

如果是写一个文件,则可能需要先创建该文件,也可能需要丢弃该文件中原先已存在的内容。系统检查你的权利,如果一切正常,操作系统将向程序返回一个小的非负整数,该整数称为文件描述符。

任何时候对文件的输入/输出都是通过文件描述符标识文件,而不是通过文件名标识文件。

因为大多数的输入/输出是通过键盘和显示器来完成的,为了方便起见,UNIX对此做了特别的安排。当命令解释程序运行一个程序的时候,它将打开3个文件,对应的文件描述符分别为0,1,2依次表示标准输入、标准输出、标准错误。如果程序从文件0中读,对1和2进行写,就可以进行输入/输出而不必关心打开文件的问题。

3.低级I/O–read和write

输入与输出时通过read和write系统调用实现的。在C语言中,可以通过函数read和write访问这两个系统调用

int n_read = read(int fd, char *buf, int n);
int n_written = write(int fd, char *buf, int n);

这两个函数中,第一个参数农户是文件描述符,第二个参数是程序中存放读或写的数据的字符数组,第三个参数是要传输的字节数。

每个调用返回实际传输的字节数。

在读文件时,函数的返回值可能会小于请求的字节数。如果返回值为0,则表示已到达文件的结尾;如果返回值为-1,则表示发生了某种错误。

在写文件时,返回值是实际写入的字节字节数。如果返回值与请求写入的字节数不相等,则说明发生了错误。

我们来看下如下的程序,具体的代码如下:

#include "syscalls.h"
main()  /* copy input to output */
{
  char buf[BUFSIZ];
  int n;
  while ((n = read(0, buf, BUFSIZ)) > 0)
    write(1, buf, n);
  return 0;
}

4.open、create、close和unlink

除了默认的标准输入、标准输出和标准错误文件外,其它文件都必须在读或写之前显示打开。

open和前面的fopen相似,不同的是,前者返回一个文件描述符,它仅仅知识一个int类型的数值。而后者返回一个文件指针。如果发生错误,open将返回-1。

#include <fcntl.h>

int fd;
int open(char *name, int flags, int perms);

fd = open(name, flags, perms);

参数name是一个包含文件名的字符串。第二个参数flags是一个int类型的值,它说明以何种方式打开文件,主要的几个值如下所示:

O_RDONLY 以只读方式打开文件
O_WRONLY 以只写方式打开文件
O_RDWR 以读写方式打开文件

如果用open打开一个不存在的文件,则将导致错误。可以使用creat系统调用创建新文件或覆盖已有的旧文件,如下所示:

int creat(char *name, int perms);
fd = creat(name, perms);

如果create成功创建了文件,它将返回一个文件描述符,否则返回-1。如果文件已存在,creat将该文件的长度截断为0,从而丢弃原先已有的内容。使用creat创建一个已经存在的文件不会导致错误。

如果要创建的文件不存在,则creat用参数perms指定的权限创建文件。我们可以看如下的程序,具体的如下:

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

#define PERMS 0666     /* RW for owner, group, others */

void error(char *, ...);

/* cp:  copy f1 to f2 */
main(int argc, char *argv[]) {
    int f1, f2, n;
    char buf[BUFSIZ];
    if (argc != 3)
        error("Usage: cp from to");
    if ((f1 = open(argv[1], O_RDONLY, 0)) == -1)
        error("cp: can't open %s", argv[1]);
    if ((f2 = creat(argv[2], PERMS)) == -1)
        error("cp: can't create %s, mode %03o",
              argv[2], PERMS);
    while ((n = read(f1, buf, BUFSIZ)) > 0)
        if (write(f2, buf, n) != n)
            error("cp: write error on file %s", argv[2]);
    return 0;
}

该程序创建的输出文件具有固定的权限0666。

注意,函数error类似于函数printf,在调用时可带变长参数表。

一个程序同时打开的文件数是有限制的(通常为20)。相应的,如果一个程序需要同时处理许多文件,那么它必须重用文件描述符。函数close(int fd) 用来断开文件描述符和已打开文件之间的连接,并释放此文件描述符,以供其他文件使用。close函数与标准库中的fclose函数相对应,但它不需要清洗(flush)缓冲区。如果程序通过exit函数退出或从主程序中返回,所有打开的文件将被关闭。

函数unlink(char * name)将文件nmae从文件系统中删除,它对应于标准库的函数remove。

5.随机访问–lseek

输入/输出通常是顺序进行的:每次调用read和write进行读写的位置紧跟在前一次操作的位置之后。但是,有时候需要以任意顺序访问文件,系统调用lseek可以在文件中任意移动位置而不实际读写任何数据。

long lseek(int fd, long offset, int origin);

将文件描述符为fd的文件的当前位置设置为offset,其中offset是相对于orgin指定的位置而言的。随后进行的读写操作将此位置开始,origin的值可以为0、1或2,分别用于指定offset从文件开始、从当前位置或从文件结束处开始算起。

使用lseek系统调用时,可以将文件视为一个大数组,其代价是访问速度会慢一些。例如,下面的函数将从文件的任意位置读入任意数目的字节,它返回读入的字节数,若发生错误,则返回-1。

#include <unistd.h>

/*get:  read n bytes from position pos */
int get(int fd, long pos, char *buf, int n) {
    if (lseek(fd, pos, 0) >= 0) /* get to pos */
        return read(fd, buf, n);
    else
        return -1;
}

lseek系统调用返回一个long类型的值,此值表示文件的新位置,若发生错误,则返回-1。

6.实例–fopen和getc函数的实现

标准库中文件不是通过文件描述符描述的,而是使用文件指针描述的。

文件指正是一个执行包含文件各种信息的结构的指针,该结构包含下列内容:

一个指向缓冲区的指针,通过它可以一次读入文件的一大块内容;一个记录缓冲区中剩余的字符数的计数器;一个指向缓冲区中下一个字符的指针;文件描述符;描述读/写模式的标志;描述错误的标志等。

下面我们来看fopen函数的主要功能就是打开文件,具体的代码如下:

typedef struct _iobuf {
  int  cnt;       /* characters left */
  char *ptr;      /* next character position */
  char *base;     /* location of buffer */
  int  flag;      /* mode of file access */
  int  fd;        /* file descriptor */
} FILE;

extern FILE _iob[OPEN_MAX];

#include <fcntl.h>
#include "syscalls.h"
#define PERMS 0666    /* RW for owner, group, others */
FILE *fopen(char *name, char *mode)
{
    int fd;
    FILE *fp;
    if (*mode != 'r' && *mode != 'w' && *mode != 'a')
        return NULL;
    for (fp = _iob; fp < _iob + OPEN_MAX; fp++)
        if ((fp->flag & (_READ | _WRITE)) == 0)
            break;        /* found free slot */
    if (fp >= _iob + OPEN_MAX)   /* no free slots */
        return NULL;
    if (*mode == 'w')
        fd = creat(name, PERMS);
    else if (*mode == 'a') {
        if ((fd = open(name, O_WRONLY, 0)) == -1)
            fd = creat(name, PERMS);
        lseek(fd, 0L, 2);
    } else
        fd = open(name, O_RDONLY, 0);
    if (fd == -1)         /* couldn't access name */
        return NULL;
    fp->fd = fd;
    fp->cnt = 0;
    fp->base = NULL;
    fp->flag = (*mode == 'r') ? _READ : _WRITE;
    return fp;
}

7.实例–存储分配程序

malloc并不是从一个编译时就确定的固定大小的数组中分配存储空间,而是从需要时向操作系统申请空间。malloc管理的空间不一定是连续的。这样,空闲存储空间以空闲链表的方式组织,每个块包含一个长度、一个指向下一块的指针以及一个指向自身存储空间的指针。这些块按照存储地址的升序组织,最后一块指向第一块

当有申请请求时,malloc将扫描空闲块链表,直到找到一个足够大的块为止。

如果该块恰好与请求的大小相符合,则将它从链表中移走并返回给用户。如果该块太大,则将它分成两部分:大小合适的块返回给用户,剩下的部分留在空闲块链表中。如果找不到一个足够大的块,则向操作系统申请一个大块并加入到空闲块链表中。

释放过程也是首先搜索空闲块链表,以找到可以插入被释放块的合适位置。如果与被释放块相邻的任一边是一个空闲块,则将这两个块合成一个更大的块,这样存储空间不会有太多的碎片。因为空闲块链表是以地址的递增顺序链接在一起的,所以很容易判断相邻的块是否空闲。

空闲块包含一个指向链表中下一块的指针、一个块大小的记录和一个指向空闲空间的本身指针。位于块开始处的控制信息称为头部。为了简化块的对齐,所有块的大小必须是头部大小的整数倍。且头部已正确地对齐。这是通过一个联合实现的,该联合包含所需的头部结构以及一个对齐要求最受限的类型的实例。

在malloc函数中,请求的长度将被舍入,以保证它是头部大小的整数倍。实际分配的块将多包含一个单元,用于头部本身。实际分配的块的大小将被记录在头部的size字段中。malloc函数返回的指引将指向空闲空间,而不是块的头部。用户可对获得的存储空间进行任何操作,但是,如果在分配的存储空间之外写入数据,则可能会破坏块链表。

其中size字段是必须的,因为由malloc函数控制的块不一定是连续的,这样就不可能通过指针算术运算计算其大小。

变量base表示空闲块链表的头部。第一次调用malloc函数时,freep为NULL,系统将创建一个退化的空闲块链表,它只包含一个大小为0的块,且该块指向它自己。任何情况下,当请求空闲空间时,都将搜索空闲块链表。搜索从上一次找到空闲块的地方开始。该策略可以保证链表是均匀的。如果找到的块太大,则将其尾部返回给用户,这样,初始块的头部只需要修改size字段即可。在任何情况下,返回给用户的指针都指向块内的空闲存储空间,即比指向头部的指针大一个单元。

static Header base;       /* empty list to get started */
static Header *freep = NULL;     /* start of free list */
/* malloc:  general-purpose storage allocator */
void *malloc(unsigned nbytes) {
    Header *p, *prevp;
    Header *moreroce(unsigned);
    unsigned nunits;
    nunits = (nbytes + sizeof(Header) - 1) / sizeof(header) + 1;
    if ((prevp = freep) == NULL) {   /* no free list yet */
        base.s.ptr = freeptr = prevptr = &base;
        base.s.size = 0;
    }
    for (p = prevp->s.ptr;; prevp = p, p = p->s.ptr) {
        if (p->s.size >= nunits) {  /* big enough */
            if (p->s.size == nunits)  /* exactly */
                prevp->s.ptr = p->s.ptr;
            else {              /* allocate tail end */
                p->s.size -= nunits;
                p += p->s.size;
                p->s.size = nunits;
            }
            freep = prevp;
            return (void *) (p + 1);
        }
        if (p == freep)  /* wrapped around free list */
            if ((p = morecore(nunits)) == NULL)
                return NULL;    /* none left */
    }
}

函数morecore用于向操作系统请求存储空间,其实现细节因系统的不同而不同。

最后我们来看下free函数。它从freep指向的地址开始,逐个扫描空闲块链表,寻找可以插入空闲块的地方。该位置可能在两个空闲块之前,也可能在链表的末尾。在任何一种情况下,如果被释放的块与另一空闲块相邻,则将这两个块合并起来。合并两个块的操作很简单,只需要设置指针指向正确的位置,并设置正确的块大小就可以了。

/* free:  put block ap in free list */
void free(void *ap)
{
  Header *bp, *p;
  bp = (Header *)ap - 1;    /* point to  block header */
  for (p = freep; !(bp > p && bp < p->s.ptr); p = p->s.ptr)
    if (p >= p->s.ptr && (bp > p || bp < p->s.ptr))
      break;  /* freed block at start or end of arena */
  if (bp + bp->size == p->s.ptr) {    /* join to upper nbr */
    bp->s.size += p->s.ptr->s.size;
    bp->s.ptr = p->s.ptr->s.ptr;
  } else
    bp->s.ptr = p->s.ptr;
  if (p + p->size == bp) {            /* join to lower nbr */
    p->s.size += bp->s.size;
    p->s.ptr = bp->s.ptr;
  } else
    p->s.ptr = bp;
  freep = p;
}

8.写在最后

至此整个C的入门就结束了,这后面还需要多加练习,不然还是不行,代码一定要多写。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值