1.系统调用
什么是系统调用:
由操作系统实现并提供给外部应用程序的编程接口。(Application Programming Interface,API)。是应用程序同系统之间数据交互的桥梁。
C标准函数和系统函数调用关系。一个helloworld如何打印到屏幕。
2.C标准库文件IO函数
fopen、fclose、fseek、fgets、fputs、fread、fwrite...... r 只读、 r+读写 w只写并截断为0、 w+读写并截断为0 a追加只写、 a+追加读写
3.open/close函数
3.1 函数原型
int open(const char *pathname, int flags); int open(const char *pathname, int flags, mode_t mode); int close(int fd);
3.2 常用参数
O_RDONLY、O_WRONLY、O_RDWR O_APPEND、O_CREAT、O_EXCL、 O_TRUNC、 O_NONBLOCK 创建文件时,指定文件访问权限。权限同时受umask影响。结论为: 文件权限 = mode & ~umask 都变成二进制,掩码取反,然后与运算. 使用头文件:<fcntl.h>
3.3 open常见错误
1. 打开文件不存在 2. 以写方式打开只读文件(打开文件没有对应权限) 3. 以只写方式打开目录
4.文件描述符
4.1 PCB进程控制块
PCB进程控制块:本质就是结构体,其中一个成员就是文件描述符表。文件描述符表中的每一个成员都是文件描述符。该表中能用的就是最小的。
0-STDIN_FILENO
1-STDOUT_FILENO
2-STDERR_FILENO
可使用命令locate sched.h查看位置: /usr/src/linux-headers-3.16.0-30/include/linux/sched.h struct task_struct { volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */ /* 表示进程的当前状态: TASK_RUNNING:正在运行或在就绪队列run-queue中准备运行的进程,实际参与进程调度。 TASK_INTERRUPTIBLE:处于等待队列中的进程,待资源有效时唤醒,也可由其它进程通过信号(signal)或定时中断唤醒后进入就绪队列run-queue。 TASK_UNINTERRUPTIBLE:处于等待队列中的进程,待资源有效时唤醒,不可由其它进程通过信号(signal)或定时中断唤醒。 TASK_ZOMBIE:表示进程结束但尚未消亡的一种状态(僵死状态)。此时,进程已经结束运行且释放大部分资源,但尚未释放进程控制块。 TASK_STOPPED:进程被暂停,通过其它进程的信号才能唤醒。导致这种状态的原因有二,或者是对收到SIGSTOP、SIGSTP、SIGTTIN或SIGTTOU信号的反应,或者是受其它进程的ptrace系统调用的控制而暂时将CPU交给控制进程。 TASK_SWAPPING: 进程页面被交换出内存的进程。 */ unsigned long flags; //进程标志,与管理有关,在调用fork()时给出 int sigpending; //进程上是否有待处理的信号 mm_segment_t addr_limit; //进程地址空间,区分内核进程与普通进程在内存存放的位置不同 /*用户线程空间地址: 0..0xBFFFFFFF。 内核线程空间地址: 0..0xFFFFFFFF */ struct exec_domain *exec_domain; //进程执行域 volatile long need_resched; //调度标志,表示该进程是否需要重新调度,若非0,则当从内核态返回到用户态,会发生调度 unsigned long ptrace; int lock_depth; //锁深度 long counter; //进程的基本时间片,在轮转法调度时表示进程当前还可运行多久,在进程开始运行是被赋为priority的值,以后每隔一个tick(时钟中断)递减1,减到0时引起新一轮调 度。重新调度将从run_queue队列选出counter值最大的就绪进程并给予CPU使用权,因此counter起到了进程的动态优先级的作用 long nice; //静态优先级 unsigned long policy; //进程的调度策略,有三种,实时进程:SCHED_FIFO,SCHED_RR,分时进程:SCHED_OTHER //在Linux 中, 采用按需分页的策略解决进程的内存需求。task_struct的数据成员mm 指向关于存储管理的mm_struct结构。 struct mm_struct *mm; //进程内存管理信息 int has_cpu, processor; unsigned long cpus_allowed; struct list_head run_list; //指向运行队列的指针 unsigned long sleep_time; //进程的睡眠时间 //用于将系统中所有的进程连成一个双向循环链表,其根是init_task //在Linux 中所有进程(以PCB 的形式)组成一个双向链表,next_task和prev_task是链表的前后向指针 struct task_struct *next_task, *prev_task; struct mm_struct *active_mm; //active_mm 指向活动地址空间。 struct linux_binfmt *binfmt; //进程所运行的可执行文件的格式 int exit_code, exit_signal; int pdeath_signal; //父进程终止是向子进程发送的信号 unsigned long personality; int dumpable:1; int did_exec:1; pid_t pid; //进程标识符,用来代表一个进程 pid_t pgrp; //进程组标识,表示进程所属的进程组 pid_t tty_old_pgrp; //进程控制终端所在的组标识 pid_t session; //进程的会话标识 pid_t tgid; int leader; //表示进程是否为会话主管 //指向最原始的进程任务指针,父进程任务指针,子进程任务指针,新兄弟进程任务指针,旧兄弟进程任务指针。 struct task_struct *p_opptr, *p_pptr, *p_cptr, *p_ysptr, *p_osptr; struct list_head thread_group; //线程链表 //用于将进程链入HASH表,系统进程除了链入双向链表外,还被加入到hash表中 struct task_struct *pidhash_next; struct task_struct **pidhash_pprev; wait_queue_head_t wait_chldexit; //供wait4()使用 struct semaphore *vfork_sem; //供vfork()使用 unsigned long rt_priority; //实时优先级,用它计算实时进程调度时的weight值 //it_real_value,it_real_incr用于REAL定时器,单位为jiffies,系统根据it_real_value //设置定时器的第一个终止时间.在定时器到期时,向进程发送SIGALRM信号,同时根据 //it_real_incr重置终止时间,it_prof_value,it_prof_incr用于Profile定时器,单位为jiffies。 //当进程运行时,不管在何种状态下,每个tick都使it_prof_value值减一,当减到0时,向进程发送信号SIGPROF,并根据it_prof_incr重置时间. //it_virt_value,it_virt_value用于Virtual定时器,单位为jiffies。当进程运行时,不管在何种 //状态下,每个tick都使it_virt_value值减一当减到0时,向进程发送信号SIGVTALRM,根据it_virt_incr重置初值 unsigned long it_real_value, it_prof_value, it_virt_value; unsigned long it_real_incr, it_prof_incr, it_virt_incr; struct timer_list real_timer; //指向实时定时器的指针 struct tms times; //记录进程消耗的时间 unsigned long start_time; //进程创建的时间 long per_cpu_utime[NR_CPUS], per_cpu_stime[NR_CPUS];//记录进程在每个CPU上所消耗的用户态时间和核心态时间 //内存缺页和交换信息: //min_flt, maj_flt累计进程的次缺页数(Copyon Write页和匿名页)和主缺页数(从映射文件或交换 //设备读入的页面数);nswap记录进程累计换出的页面数,即写到交换设备上的页面数。 //cmin_flt, cmaj_flt,cnswap记录本进程为祖先的所有子孙进程的累计次缺页数,主缺页数和换出页面数。 //在父进程回收终止的子进程时,父进程会将子进程的这些信息累计到自己结构的这些域中 unsigned long min_flt, maj_flt, nswap, cmin_flt, cmaj_flt, cnswap; int swappable:1; //表示进程的虚拟地址空间是否允许换出 //进程认证信息 //uid,gid为运行该进程的用户的用户标识符和组标识符,通常是进程创建者的uid,gid,euid,egid为有效uid,gid //fsuid,fsgid为文件系统uid,gid,这两个ID号通常与有效uid,gid相等,在检查对于文件系统的访问权限时使用他们。 //suid,sgid为备份uid,gid uid_t uid,euid,suid,fsuid; gid_t gid,egid,sgid,fsgid; int ngroups; //记录进程在多少个用户组中 gid_t groups[NGROUPS]; //记录进程所在的组 kernel_cap_t cap_effective, cap_inheritable, cap_permitted;//进程的权能,分别是有效位集合,继承位集合,允许位集合 int keep_capabilities:1; struct user_struct *user; //代表进程所属的用户 struct rlimit rlim[RLIM_NLIMITS]; //与进程相关的资源限制信息 unsigned short used_math; //是否使用FPU char comm[16]; //进程正在运行的可执行文件名 //文件系统信息 int link_count; struct tty_struct *tty; //进程所在的控制终端,如果不需要控制终端,则该指针为空 unsigned int locks; /* How many file locks are being held */ //进程间通信信息 struct sem_undo *semundo; //进程在信号量上的所有undo操作 struct sem_queue *semsleeping; //当进程因为信号量操作而挂起时,他在该队列中记录等待的操作 struct thread_struct thread; //进程的CPU状态,切换时,要保存到停止进程的task_struct中 struct fs_struct *fs; //文件系统信息,fs保存了进程本身与VFS(虚拟文件系统)的关系信息 struct files_struct *files; //打开文件信息,指向文件描述符号 //信号处理函数 spinlock_t sigmask_lock; /* Protects signal and blocked */ struct signal_struct *sig; //信号处理函数 sigset_t blocked; //进程当前要阻塞的信号,每个信号对应一位 struct sigpending pending; //进程上是否有待处理的信号 unsigned long sas_ss_sp; size_t sas_ss_size; int (*notifier)(void *priv); void *notifier_data; sigset_t *notifier_mask; /* Thread group tracking */ u32 parent_exec_id; u32 self_exec_id; spinlock_t alloc_lock; //用于申请空间时用的自旋锁。自旋锁的主要功能是临界区保护 };
4.2 文件描述图表
结构体PCB 的成员变量file_struct *file 指向文件描述符表。
从应用程序使用角度,该指针可理解记忆成一个字符指针数组,下标0/1/2/3/4…找到文件结构体。
本质是一个键值对0、1、2…都分别对应具体地址。但键值对使用的特性是自动映射,我们只操作键不直接使用值。
新打开文件返回文件描述符表中未使用的最小文件描述符。
STDIN_FILENO 0 STDOUT_FILENO 1 STDERR_FILENO 2
4.3 最大打开文件数
一个进程默认打开文件的个数1024。 命令查看ulimit -a 查看open files 对应值。默认为1024 可以使用ulimit -n 4096 修改 当然也可以通过修改系统配置文件永久修改该值,但是不建议这样操作。 cat /proc/sys/fs/file-max可以查看该电脑最大可以打开的文件个数。 受内存大小影响。
4.4 FIFE结构体
主要包含文件描述符、文件读写位置、IO缓冲区三部分内容。 struct file { ... 文件的偏移量; 文件的访问权限; 文件的打开标志; 文件内核缓冲区的首地址; struct operations * f_op; ... }; 查看方法: (1) /usr/src/linux-headers-3.16.0-30/include/linux/fs.h (2) lxr:百度 lxr → lxr.oss.org.cn → 选择内核版本(如3.10) → 点击File Search进行搜索 → 关键字:“include/linux/fs.h” → Ctrl+F 查找 “struct file {” → 得到文件内核中结构体定义 → “struct file_operations”文件内容操作函数指针 → “struct inode_operations”文件属性操作函数指针
5. read/write 函数
ssize_t read(int fd, void *buf, size_t count); ssize_t write(int fd, const void *buf, size_t count); read与write函数原型类似。使用时需注意:read/write函数的第三个参数。
练习:编写程序实现简单的cp功能。
程序比较:如果一个只读一个字节实现文件拷贝,使用read、write效率高,还是使用对应的标库函数(fgetc、fputc)效率高呢?
5.1 strace命令
shell中使用strace命令跟踪程序执行,查看调用的系统函数。
5.2 缓冲区
read、write函数常常被称为Unbuffered I/O。指的是无用户及缓冲区。但不保证不使用内核缓冲区。
5.3 预读入缓输出
6.错误处理函数
错误号:errno perror函数: void perror(const char *s); strerror函数: char *strerror(int errnum); 查看错误号: /usr/include/asm-generic/errno-base.h /usr/include/asm-generic/errno.h #define EPERM 1 /* Operation not permitted */ #define ENOENT 2 /* No such file or directory */ #define ESRCH 3 /* No such process */ #define EINTR 4 /* Interrupted system call */ #define EIO 5 /* I/O error */ #define ENXIO 6 /* No such device or address */ #define E2BIG 7 /* Argument list too long */ #define ENOEXEC 8 /* Exec format error */ #define EBADF 9 /* Bad file number */ #define ECHILD 10 /* No child processes */ #define EAGAIN 11 /* Try again */ #define ENOMEM 12 /* Out of memory */ #define EACCES 13 /* Permission denied */ #define EFAULT 14 /* Bad address */ #define ENOTBLK 15 /* Block device required */ #define EBUSY 16 /* Device or resource busy */ #define EEXIST 17 /* File exists */ #define EXDEV 18 /* Cross-device link */ #define ENODEV 19 /* No such device */ #define ENOTDIR 20 /* Not a directory */ #define EISDIR 21 /* Is a directory */ #define EINVAL 22 /* Invalid argument */ #define ENFILE 23 /* File table overflow */ #define EMFILE 24 /* Too many open files */ #define ENOTTY 25 /* Not a typewriter */ #define ETXTBSY 26 /* Text file busy */ #define EFBIG 27 /* File too large */ #define ENOSPC 28 /* No space left on device */ #define ESPIPE 29 /* Illegal seek */ #define EROFS 30 /* Read-only file system */ #define EMLINK 31 /* Too many links */ #define EPIPE 32 /* Broken pipe */ #define EDOM 33 /* Math argument out of domain of func */ #define ERANGE 34 /* Math result not representable */
7.阻塞、非阻塞
读常规文件是不会阻塞的,不管读多少字节,read一定会在有限的时间内返回。从终端设备或网络读则不一定,如果从终端输入的数据没有换行符,调用read读终端设备就会阻塞,如果网络上没有接收到数据包,调用read从网络读就会阻塞,至于会阻塞多长时间也是不确定的,如果一直没有数据到达就一直阻塞在那里。同样,写常规文件是不会阻塞的,而向终端设备或网络写则不一定。
现在明确一下阻塞(Block)这个概念。当进程调用一个阻塞的系统函数时,该进程被置于睡眠(Sleep)状态,这时内核调度其它进程运行,直到该进程等待的事件发生了(比如网络上接收到数据包,或者调用sleep指定的睡眠时间到了)它才有可能继续运行。与睡眠状态相对的是运行(Running)状态,在Linux内核中,处于运行状态的进程分为两种情况:
正在被调度执行。CPU处于该进程的上下文环境中,程序计数器(eip)里保存着该进程的指令地址,通用寄存器里保存着该进程运算过程的中间结果,正在执行该进程的指令,正在读写该进程的地址空间。
就绪状态。该进程不需要等待什么事件发生,随时都可以执行,但CPU暂时还在执行另一个进程,所以该进程在一个就绪队列中等待被内核调度。系统中可能同时有多个就绪的进程,那么该调度谁执行呢?内核的调度算法是基于优先级和时间片的,而且会根据每个进程的运行情况动态调整它的优先级和时间片,让每个进程都能比较公平地得到机会执行,同时要兼顾用户体验,不能让和用户交互的进程响应太慢。
阻塞读终端: 【block_readtty.c】 非阻塞读终端 【nonblock_readtty.c】 非阻塞读终端和等待超时 【nonblock_timeout.c】
注意,阻塞与非阻塞是对于文件而言的。而不是read、write等的属性。read终端,默认阻塞读。
总结read 函数返回值:
1. 返回非零值: 实际read到的字节数 2. 返回-1: 1):errno != EAGAIN (或!= EWOULDBLOCK) read出错 2):errno == EAGAIN (或== EWOULDBLOCK) 设置了非阻塞读,并且没有数据到达。 3. 返回0:读到文件末尾
附上测试代码:
阻塞方式打开设备文件:
#include <unistd.h> #include <stdlib.h> #include <stdio.h> int main(void) { char buf[10]; int n; n=read(STDIN_FILENO,buf,10); if(n<0){ perror("read STDIN_FILENO"); exit(1); } write(STDOUT_FILENO,buf,n); return 0; }
非阻塞方式打开设备文件:
#include <unistd.h> #include <fcntl.h> #include <errno.h> #include <stdio.h> #include <stdlib.h> #include <string.h> int main(void) { char buf[10]; int fd,n; fd=open("/dev/tty",O_RDONLY|O_NONBLOCK); if(fd<0){ perror("open /dev/tty"); exit(1); } tryagain: n = read(fd,buf,10); if(n<0){ if(errno != EAGAIN ){ //if(errno!=EWOULDBLOCK) perror("read /dev/tty"); exit(1); }else{ write(STDOUT_FILENO,"try again\n",strlen("try again\n")); sleep(2); goto tryagain; } } write(STDOUT_FILENO,buf,n); close(fd); return 0; }
非阻塞设置超时:
#include <unistd.h> #include <fcntl.h> #include <errno.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #define MSG_TRY "try again\n" #define MSG_TIMEOUT "time out\n" int main(void) { char buf[10]; int fd,n,i; fd=open("/dev/tty",O_RDONLY|O_NONBLOCK); if(fd<0){ perror("open /dev/tty"); exit(1); } printf("open /dev/tty ok... %d\n",fd); for(i=0;i<5;i++){ n = read(fd,buf,10); if(n<0){ if(errno != EAGAIN ){ //if(errno!=EWOULDBLOCK) perror("read /dev/tty"); exit(1); }else{ write(STDOUT_FILENO,MSG_TRY,strlen(MSG_TRY)); sleep(2); } } } if(i==5){ write(STDOUT_FILENO,MSG_TIMEOUT,strlen(MSG_TIMEOUT)); }else{ write(STDOUT_FILENO,buf,n); } close(fd); return 0; }
8.lseek函数
8.1 文件偏移
Linux中可使用系统函数lseek来修改文件偏移量(读写位置)
每个打开的文件都记录着当前读写位置,打开文件时读写位置是0,表示文件开头,通常读写多少个字节就会将读写位置往后移多少个字节。但是有一个例外,如果以O_APPEND方式打开,每次写操作都会在文件末尾追加数据,然后将读写位置移到新的文件末尾。lseek和标准I/O库的fseek函数类似,可以移动当前读写位置(或者叫偏移量)。
回忆fseek的作用及常用参数。 SEEK_SET、SEEK_CUR、SEEK_END int fseek(FILE *stream, long offset, int whence); 成功返回0;失败返回-1 特别的:超出文件末尾位置返回0;往回超出文件头位置,返回-1 off_t lseek(int fd, off_t offset, int whence); 失败返回-1;成功:返回的值是较文件起始位置向后的偏移量。 特别的:lseek允许超过文件结尾设置偏移量,文件会因此被拓展。 注意文件“读”和“写”使用同一偏移位置。
【lseek.c】
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <fcntl.h> int main(void){ int fd,n; char msg[]="it's a test foe lseek\n"; char ch; fd = open("lseek.txt",O_RDWR|O_CREAT,0644); if(fd<0) { perror("open lseek.txt error"); exit(1); } write(fd,msg,strlen(msg)); lseek(fd,0,SEEK_SET); while((n=read(fd,&ch,1))){ if(n<0){ perror("read error"); exit(1); } write(STDOUT_FILENO,&ch,n); } close(fd); return 0; } ~
8.2 lseek常用应用
1. 使用lseek拓展文件:write操作才能实质性的拓展文件。单lseek是不能进行拓展的。 一般:write(fd, "a", 1); od -tcx filename 查看文件的16进制表示形式 od -tcd filename 查看文件的10进制表示形式 使用truncate函数,直接拓展文件。 2. 通过lseek获取文件的大小:lseek(fd, 0, SEEK_END); 【lseek_test.c】 【最后注意】:lseek函数返回的偏移量总是相对于文件头而言。
【lseek_test.c】
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <pthread.h> #include <fcntl.h> int main(int argc,char *argv[]) { int fd = open(argv[1],O_RDWR); if(fd == -1) { perror("open error"); exit(1); } int lenth = lseek(fd,0,SEEK_END); printf("file size:%d\n",lenth); return 0; }
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <pthread.h> int main(int argc,char *argv[]) { //open/lseek(fd,249,SEEK_END)/write(fd,"\0",1); int ret =truncate("lseek.txt",250);//in there ,it must own an knowned file. printf("ret = %d\n",ret); return 0; }
9.fcntl函数
改变一个【已经打开】的文件的 访问控制属性。 重点掌握两个参数的使用, F_GETFL 和 F_SETFL。 fcntl : int flgs =fcntl(fd,F_GETFL); 获取文件状态:F_GETFL 设置文件状态:F_SETFL
位图:
位图就是使用一个比特位来进行表示数据存在与否的信息。
这里面的flags就是位图
【fcntl.c】
#include <unistd.h> #include <fcntl.h> #include <errno.h> #include <stdio.h> #include <string.h> #define MSG_TRY "try again\n" int main(void) { char buf[10]; int flags ,n; flags = fcntl(STDIN_FILENO,F_GETFL); if(flags == -1){ perror("fcntl error"); exir(1); } flags |= O_NONBLOCK; int ret = fcntl(STDIN_FILENO,F_SETFL,flags); if(ret == -1){ perror("fcntl error"); exit(1); } tryagain: n=read(STDIN_FILENO,buf,10); if(n<0){ if(errno != EAGAIN){ perror("read /dev/tty"); exit(1); } sleep(3); write(STDOUT_FILENO,MSG_TRY,strlen(MSG_TRY)); goto tryagain; } write(STDOUT_FILENO,buf,n); return 0; }
10.ioctl 函数
对设备的I/O通道进行管理,控制设备特性。(主要应用于设备驱动程序中)。 通常用来获取文件的【物理特性】(该特性,不同文件类型所含有的值各不相同)
11.传入输出参数
11.1传入参数
const 关键字修饰的 指针变量 在函数内部读操作。 char *strcpy(cnost char *src, char *dst);
11.2 传出参数
1. 指针做为函数参数 2. 函数调用前,指针指向的空间可以无意义,调用后指针指向的空间有意义,且作为函数的返回值传出 3. 在函数内部写操作。
11.3 传入传出参数
传入参数: 1.指针作为函数参数 2.通常const关键字修饰 3.指针指向有效区域,在函数内部做读操作 传出参数: 1.指针作为函数参数 2.在函数调用之前,指针指向的空间可以无意义,但必须有效 3.在函数内部,做写操作。 4.函数调用结束后,充当函数返回值。 传入传出参数: 1.指针作为函数参数 2.在函数调用之前,指针有实际意义 3.在函数内部,先做读操作,后做写操作 4.函数调用结束后,充当函数返回值。
拓展阅读
关于虚拟4G内存的描述和解析: 一个进程用到的虚拟地址是由内存区域表来管理的,实际用不了4G。而用到的内存区域,会通过页表映射到物理内存。
所以每个进程都可以使用同样的虚拟内存地址而不冲突,因为它们的物理地址实际上是不同的。内核用的是3G以上的1G虚拟内存地址, 其中896M是直接映射到物理地址的,128M按需映射896M以上的所谓高位内存。各进程使用的是同一个内核。
首先要分清“可以寻址”和“实际使用”的区别。
其实我们讲的每个进程都有4G虚拟地址空间,讲的都是“可以寻址”4G,意思是虚拟地址的0-3G对于一个进程的用户态和内核态来说是可以访问的,而3-4G是只有进程的内核态可以访问的。并不是说这个进程会用满这些空间。
其次,所谓“独立拥有的虚拟地址”是指对于每一个进程,都可以访问自己的0-4G的虚拟地址。虚拟地址是“虚拟”的,需要转化为“真实”的物理地址。
好比你有你的地址簿,我有我的地址簿。你和我的地址簿都有1、2、3、4页,但是每页里面的实际内容是不一样的,我的地址簿第1页写着3你的地址簿第1页写着4,对于你、我自己来说都是用第1页(虚拟),实际上用的分别是第3、4页(物理),不冲突。
内核用的896M虚拟地址是直接映射的,意思是只要把虚拟地址减去一个偏移量(3G)就等于物理地址。同样,这里指的还是寻址,实际使用前还是要分配内存。而且896M只是个最大值。如果物理内存小,内核能使用(分配)的可用内存也小。