一.文件IO和标准IO特点
库函数:方便程序员开发,可能会存在一些问题(C库除外)。
系统调用:有内核提供的函数接口, 更加的稳定高效。
文件IO:无缓冲/低级磁盘IO
无缓冲:增加磁盘的访问次数,不利于保护磁盘。
优点:可以保证数据的实时写入。
IO的操作方式:看程序的应用场合。
标准IO:带缓冲的/高级磁盘IO
缓冲IO就是可以减少对磁盘的访问次数,提高系统效率,有利于保护磁盘。
缺点:数据不会实时写入。
标准IO的缓冲方式:全缓冲、行缓冲、无缓冲。
全缓冲:默认对文件的操作都是全缓冲。当填满标准I/O缓冲区后才进行实际I/O操作
数据何时写入磁盘:
1、缓冲区满了。
2、程序正常退出(ctrl +c, 断电,属于不正常退出)。
3、执行刷新操作,fflush()
行缓冲:与终端相关的都是行缓冲
刷新条件:
1、缓冲区满了。
2、程序正常退出(ctrl +c, 断电,属于不正常退出)。
3、执行刷新操作,fflush()
4、遇到 就会刷新。
无缓冲:标准出错,比如人机交互。不对I/O的操作进行缓冲,即对流的读写时会立刻操作实际的文件。
二.标准I/O库的流(stream)
标准I/O库的所有操作都是围绕流(stream)来进行。
FILE 类型在 /usr/include/libio.h中。描述了与文件相关的信息。
在标准I/O中,流用FILE *来描述。
如何获得文件流:
标准IO中,默认开启三个流, stdin stdout stderr
fprintf函数:向一个流中写入数据。
int fprintf(FILE *stream, const char *format, ...);
stream:格式化输出的流。
fprintf(stdout, "helloworld ") === printf("helloworld ");
fprintf(xxx, "helloworld ");
⑴获取文件流
FILE *fopen (const char *path, const char *mode);
功能:获得流
参数:path: 要打开的文件的路径、名字, "printf.c"
mode:
r:表示打开的文件只能进行读操作, 而且必须保证该文件已经存在。
如: fopen("printf.c", "r");
r+:表示打开的文件可读可写操作, 而且必须保证该文件已经存在。
如: fopen("printf.c", "r+");
W:表示打开的文件只能进行写操作,但是如果文件已经存在,那么会将文件内容清空。
如果文件不存在,那么会创建一个文件。
如: fopen("file", "w");
w+: 表示打开的文件可读可写操作,但是如果文件已经存在,那么会将文件内容清空。
如果文件不存在,那么会创建一个文件。
a: 表示以追加的方式对文件进行只写操作,如果文件已经存在,那么会保留文件内容;
如果文件不存在,那么会创建一个文件。
a+: 表示以追加的方式对文件进行读写操作,如果文件已经存在,那么会保留文件内容;
如果文件不存在,那么会创建一个文件。
返回值:FILE* , 如果出错,返回NULL。
⑵关闭文件流
关闭一个打开的流:如果没有显式的关闭时,程序结束后,系统会关闭该流。
fclose(FILE*);成功返回0。
限制:就是不能多重关闭一个流。
问题①:测试在一个程序中最多可以获得多少个流(1024)。
获得数值多少。
通过循环fopen实现。
⑶
FILE *freopen(const char *restrict pathname, const char *restrict type, FILE* restrict fp)
功能:就是对stdin stdout进行重定向。将文件流fp重定向到文件pathname中
⑷刷新文件流
fflush(FILE *)
刷新流。
fflush(stdout); 指定要刷新stdout
fflush(NULL);刷新当前程序中所有的流。
⑸流操作:
每次一个字符的I/O:
读函数:
int getc(FILE *stream);
int fgetc(FILE *stream);
int getchar(void);
功能:就是从一个流中读入一个字符。
返回值:读到的字符, EOF结束。
ctrl + D 模拟EOF
问题②:实现一个简单的cat命令 getc读到文件结尾返回EOF
a、fopen获得文件流
b、循环的getc/fgetc来读取这个流。
printf()
c、getc读到文件结尾返回EOF
d、fclose(fp);
问题③:统计一个文件有多少行?
统计' '
每次一个字符的写:
int putc(int c, FILE *stream);
int fputc(int c, FILE *stream);
int putchar(int c);
功能:就是向一个流中写入一个字符。
返回:写入的字符,出错返回EOF
问题④:使用fgetc fputc实现cat命令
问题⑸:使用fgetc、fputc实现文件拷贝。
file 拷贝一份file1
a、读源文件、写目标文件
b、需要读、写两个流
c、选择对应的打开模式
判断:文件结尾或者出错
int feof(FILE *stream);
判断是否是到达文件末尾
返回:非0 是到达文件末尾
int ferror(FILE *stream);
判断是否是出错
返回 非0 是出错
int clearerr(FILE *stream);
清空文件操作标志, 一旦清空,那么就没办法用feof和ferror测试。
每次一行的IO操作:
读操作:
char *gets(char *s);//禁用, 不会检查数组长度。
char *fgets(char *s, int size, FILE * stream);
功能:就是从一个流中读入一个字符串。
参数:s:就是存放读到的字符串
size: 表示用户希望读到的字节个数
stream:要读的流
返回值:读到的字符串,出错返回NULL。
fgets的特点:
①、执行一次最多可以读到 size - 1个字符,最后一个位置存放' ';
fgets读到的都是字符串。
②、fgets函数读到 会立刻返回。
③、当从终端上读入时, 如果读入的字节个数小于 size -1, 那么' ‘也会被读走。
如果读入的字节个数大于 size -1, 那么' ‘就不会被读走。
问题⑥:使用fgets和printf来实现cat命令
fgets读到文件结尾返回NULL。
每次一行的写函数:
int puts(const char *s);
功能:向标准输出写入数据
特点:自带换行符' ’;
只会输出第一个' '之前的内容。
int fputs(const char *s, FILE * stream);
功能:向一个流中写入数据, fputs没有自带换行符。
返回:EOF失败, 非负代表成功。
特点:只会输出第一个' '之前的内容。
问题⑦:使用fgets fputs实现文件拷贝?
直接IO方式:按记录个数进行操作。
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
功能:从一个流中读数据
参数:ptr 存放都到的数据。
size: 每一条记录的字节个数。
nmemb: 执行一次fread最多要读的记录的个数。
最多可以读到字节个数 = size * nmemb;
stream: 要读的流。
每一条记录:可以是任何数据,比如struct 、int、 字符串。
返回值:表示正确读到的记录个数。
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)
功能:向一个流中写数据
参数:ptr 存放要写入的数据。
size: 每一条记录的字节个数。
nmemb: 执行一次fread最多要读的记录的个数。
最多可以写入字节个数 = size * nmemb;
stream: 要写的流。
每一条记录:可以是任何数据,比如struct 、int、 字符串。
返回值:表示正确写入的记录个数。
对文件的偏移 和定位操作:
int fseek(FILE *stream, long offset, int whence);
功能:对一个文件的访问位置进行偏移。
stream:要进行操作的流
offset: 偏移量
whence:要偏移的参考点,基准点。
SEEK_SET 代表文件的起始位置
SEEK_END 代表文件的结尾
SEEK_CUR 代表文件当前的访问位置。
fseek(fp, 0, SEEK_END); 代表偏移到文件末尾位置
fseek(fp, 0, SEEK_SET); 代表偏移到文件开始位置
fseek(fp, 100, SEEK_SET); 表示向文件结尾的方向偏移100个字节
fseek(fp, -100, SEEK_END); 表示向文件开头的方向偏移100个字节
返回值:成功返回0, 失败-1.
long ftell(FILE *stream);
功能:就是定位当前的访问位置
返回值:返回从开头到现在位置的字节个数。
void rewind(FILE *stream);
功能:将文件位置偏移到开头。
+---------------------------------------------------------------------------+
作业:
编程读写一个文件test.txt,每隔1秒向文件中写入一行数据,类似这样:
1, 2007-7-30 15:16:42
2, 2007-7-30 15:16:43
该程序应该无限循环,直到按Ctrl-C中断程序。
再次启动程序写文件时可以追加到原文件之后,并且序号能够接续上次的序号,比如:
1, 2007-7-30 15:16:42
2, 2007-7-30 15:16:43
3, 2007-7-30 15:19:02
4, 2007-7-30 15:19:03
5, 2007-7-30 15:19:04
提示:
要追加写入文件,同时要读取该文件的内容以决定下一个序号是几,应该用什么模式打开文件?
首先判断一下打开的文件是否为新文件,如果是新文件,就从序号1开始写入;如果不是新文件,则统计原来有多少行,比如有n行,然后从序号n+1开始写入。以后每写一行就把行号加1。
获取当前的系统时间需要调用函数time(),得到的结果是一个time_t类型,其实就是一个大整数,其值表示从UTC时间1970年1月1日00:00:00(称为UNIX的Epoch时间)到当前时刻的秒钟数。然后调用localtime()将time_t所表示的UTC时间转换为本地时间(我们是+8区,比UTC多8个小时)并转成struct tm类型,该类型的各数据成员分别表示年月日时分秒,请自己写出转换格式的代码,不要使用ctime()或asctime()函数。具体用法请查阅man page。time和localtime函数需要头文件time.h。
调用sleep(n)可使程序睡眠n秒,该函数需要头文件unistd.h。
time()获得自 1970-1-1 0:0:0: 距离当前系统的秒数。
time_t mytime;
mytime = time()
localtime()用来将秒数转换成对应的时间格式。
struct tm *localtime(const time_t *timep);
struct tm * mytm;
mytm = localtime(&mytime);
sleep(1);睡眠
fprintf(fp, "");
出错判断:
strerror() - 映射errno对应的错误信息
char *strerror(int errnum);
perror() – 输出用户信息及errno对应的错误信息
void perror(const char *s);
自带换行符。
三.文件IO:系统调用
默认打开文件描述符:标准输入 标准输出 标准出错
0 1 2
STDIN_FILENO STDOUT_FILENO STDERR_FILENO
文件描述符是一个非负整数。
int open(const char *pathname, int flags, mode_t mode)
功能:就是打开一个文件,获得一个文件描述符。
参数:pathname 文件名
flags:
O_RDONLY 表示对文件只读
O_WRONLY 表示对文件只写
O_RDWR 表示对文件可读可写 ====》这三个标志位是互斥的,有且只能有一个。
O_CREAT 表示文件不存在时,会创建文件。
O_EXCL 配合O_CREAT使用,当文件已经存在时还要创建,那么就会出错。
O_TRUNC 如文件已经存在,那么打开文件时先删除文件中原有数据。
O_APPEND 以添加方式打开文件,所以对文件的写操作都在文件的末尾进行。
如: open("file", O_WRONLY|O_TRUNC);
mode: 只有添加了O_CREAT 标志位时,才需要使用。
指定创建的文件的权限。
S_IRWXU S_IRUSR S_IWUSR S_IXUSR S_IRWXG S_IRGRP S_IWGRP S_IXGRP S_IRWXO S_IROTH S_IWOTH S_IXOTH
如:open("file", O_WRONLY|O_CREAT|O_EXCL, 0666);
返回值:得到的当前程序中最小的未使用的非负整数文件描述符。
open函数创建的文件权限: 指定权限 & (~umask)
比如: 0666 & (~0002)
问题:测试在一个程序中最多可以获得多少个文件描述符?
关闭文件描述符:
close(fd);
读写文件:
ssize_t read(int fd, void *buf, size_t count);
功能:从文件描述符中读取数据
参数:fd 要读的文件描述符
buf 存放读取到的数据
count 期望执行一次read要读取的字节个数。
返回值:就是实际读到的字节个数。
读到文件末尾返回0.
特点:read读取数据和’ ’, ‘ ’没有任何关系。
ssize_t write(int fd, const void *buf, size_t count);
功能:向文件描述符中写入数据
参数: fd 要写的文件描述符
buf 存放要写入到的数据
count 期望执行一次write要写入的字节个数。
返回值:就是实际写入的字节个数。
特点:write 读取数据和' ‘, ' '没有任何关系
od -c 以字符形式显示文件内容。
问题:使用read write函数实现文件拷贝?
当read和write配合使用时,write要写入的字节个数必须是read实际读到的字节个数。
bytes = read(fd_r, buf, sizeof(buf));
write(fd_w, buf, bytes);
lseek:
off_t lseek(int fd, off_t offset, int whence);
功能:就是对文件访问位置进行定位和偏移
参数:fd 要操作的描述符
offset 指定的偏移量
whence 偏移的参考点
SEEK_SET
SEEK_END
SEEK_CUR
返回值:就是偏移之后的位置。
如: lseek(fd, 0, SEEK_END);偏移到文件末尾, 返回值就是文件的大小(字节)。
lseek(fd, 0, SEEK_SET); 偏移到文件开头。
lseek(fd, 100, SEEK_SET);向文件末尾方向偏移100个字节
lseek(fd, -100, SEEK_END);向文件开头方向偏移100个字节
lseek(fd, 0, SEEK_CUR);返回的当前的访问位置。
限制: lseek()只对常规文件有效,对socket、管道、FIFO等进行lseek()操作失败。
产生空洞文件:可以起到抢占磁盘空间的作用。
在文件末尾使用lseek继续向后进行偏移时,并且在偏移只有需要写入数据(文件大小会增加)
在文件末尾使用lseek继续向后进行偏移时 ,如果偏移之后没有写入任何数据,那么文件大小不会变。
lseek和O_APPEND:
当使用O_APPEND标志位进行写操作时, lseek的偏移对于写操作无效, 只对读操作有效。
补充:
获取文件属性:
int stat(const char *path, struct stat *buf);
int fstat(int fd, struct stat *buf);
int lstat(const char *path, struct stat *buf);
参数 path: 要操作的文件
buf: 存放得到文件属性。
目录操作相关:
DIR *opendir(const char *name);
功能:就是获得一个结构体指针。
struct dirent *readdir(DIR *dirp);
功能:读取一个目录
返回值:
int closedir(DIR *dirp);
关闭一个目录流
四.静态库和动态库的分析
本质上来说库是一种可执行代码的二进制形式。
linux下的库有两种:静态库和共享库(动态库)
静态库在程序编译时会被连接到目标代码中:程序运行时将不再需要该静态库,因此体积较大。
优点:程序运行时将不再需要该静态库
缺点:可执行文件的体积较大。
相同的库可能会需要被多次加载。
静态库: libxxxxx.a
动态库:动态库在程序编译时并不会被连接到目标代码中,
优点: 在程序运行时动态库才会被载入内存,因此代码体积较小。
缺点: 因此在程序运行时还需要动态库存在。
静态库的制作:将功能函数编译成库。
1、先生成目标文件
gcc -c -Wall fun.c -o fun.o
2、ar crs libfun.a fun.o
将fun.o文件 打包生成libfun.a的静态库
库的命名:lib库名.a
使用:
-L:指定库的路径
-l :指定需要连接的库的名字
gcc test.c -o test -L . -lfun
动态库的制作和使用:
1、需要生成目录文件
gcc -c -fPIC -Wall fun.c -o fun.o
fPIC:说明库可以被加载到内存的任意位置
2、 gcc -Wl,-soname,libfun.so -shared fun.o -o libfun.so.1
-Wl,-soname,libfun.so 需要连接的库
libfun.so.1 实际生成的库。
库的命名:lib库名.so
3、 ln -s 绝对路径/libfun.so.1 libfun.so
4、gcc test.c -o test -L . -lfun
共享库的加载方法:
1、动态库需要被放置到/usr/lib 或者 /lib目录下。
只需要将软连接移动过去。
2、将库的路径添加到系统环境变量中
LD_LIBRARY_PATH
exprot LD_LIBRARY_PATH=库的路径
3、将库的路径添加到 /etc/ld.so.conf/xxx.conf 的配置文件中
sudo ldconfig 来重启配置文件
用户id转换成用户名
struct passwd *getpwuid(uid_t uid);
passwd-> pw_name
组id转换成组名
struct group *getgrgid(gid_t gid);
group->gr_name
char *ctime(const time_t *timep);
返回值:"Wed Jun 30 21:49:08 1993 "
printf("%.12s", buf + 4);
五.进程
1.特点:
动态、占用内存资源、cpu资源、有生命周期、具有独立的IO状态。
内核中有struct task_struct 结构体来记录一个进程的所有信息。
程序:静态的、占用磁盘空间。
PID: 唯一的标识一个进程
⒉ 进程的类型:
交互进程
守护进程:该类进程在后台运行。它一般在Linux启动时开始执行,系统关闭时才结束。
进程的时间片:内核会为每个进程分配时间片。
进程都是宏观并行,微观串行。
进程的状态:运行态、就绪态、 阻塞态(睡眠等待)
进程运行与操作系统之上:
进程有用户空间模式、内核空间模式。
32位的操作系统上:进程的寻址空间4G(虚拟地址)
通常的空间划分: 3G(用户空间) 1G(内核空间)
进程启动方式: 手动启动、 调度启动 (/etc/crontab 定时运行脚本)
3.进程常用命令:
ps ajx
ps aux
"?" 就是守护进程, 不受终端的控制。
stat: 状态
S: interruptible sleep (waiting for an event to complete)可中断的睡眠状态
R: running or runnable (on run queue)可执行状态
Z: 退出状态,成为僵尸进程
D:uninterruptible sleep (usually IO)不可中断的睡眠状态
T:stopped, either by a job control signal or because it is being traced暂停或跟踪状态
X:dead (should never be seen)退出状态,进程即将被销毁
对于BSD格式,传统的状态关键字符:
<:high-priority (not nice to other users)
N: low-priority (nice to other users)
L: has pages locked into memory
s: is a session leader
l: is multi-threaded (using CLONE_THREAD, like NPTL pthreads do)
+:is in the foreground process group前台进程
top:动态显示系统资源使用情况
shift + <
shift + >
q: 结束
nice 按用户指定的优先级运行进程
nice的值 -20 ~ 19(优先级最低)
nice ./a.out 默认nice值是10.
nice --11 ./a.out 将a.out的nice值设置为 -11
nice -11 ./a.out 将a.out的nice值设置为 11
renice 改变正在运行进程的优先级
renice num pid 如: renice -9 1200 , 改变进程的nicenice值为-9.
kill: 给进程发送信号。
kill -l 列出当前系统支持的信号类型。
kill -9 pid
bg 将挂起的进程在后台执行
fg 把挂起的进程放到前台运行
ctrl + Z 用来挂起一个进程。
+-------------------------------------------------+
ctags的使用:
sudo ctags -R 创建索引文件tags
vi -t pid_t
ctrl + ] 光标的位置,前进查找该单词
ctrl + O/T 回退
+-------------------------------------------------+
4.进程:创建、控制、销毁、通信机制。
pid_t fork(void);
功能:创建一个子进程
返回值:fork函数会返回两个值。
如果返回值 0: 表示进入的是子进程的代码
如果返回值 大于0 : 表示进入的是父进程的代码
返回-1, 表示创建子进程失败。
fork函数的返回顺序不一定。
现代版的fork特点:这就是著名的“写操作时拷贝”(copy-on-write)技术。
如果其中任何一个进程试图修改数据,那么进程才会具有各自不同的物理空间。
pid_t getpid(void);获得当前进程的pid号
pid_t getppid(void);获得当前进程的父进程的pid号。
注意: 必须要注意两个函数的使用场合。
exec函数族 :一系列函数
功能:用于执行一个可执行文件。
int execl(const char *path, const char *arg, ...);
参数:path: 可执行文件路径
arg: 传递给可执行文件的参数。
第一个参数必须和可执行文件的名字一样。
最后一个参数必须是NULL。
比如: execl("/bin/ls", "ls", "-l", "-a", NULL);
返回值:成功返回0, 失败返回-1.
int execlp(const char *file, const char *arg, ...);
如: execlp("ls", "ls", "-l", "-a", NULL);
int execle(const char *path, const char *arg, ..., char * const envp[]);
如:char *env[3] = {"PATH=/bin/", NULL};
execle("a.out", "a.out", env);
int execv(const char *path, char *const argv[]);
如:char *arg[4] = {"ls", "-l", "-a", NULL};
execv("/bin/ls", arg);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
l: list,列表, 就是以列表的形式传参。
v: 传递参数时, 使用数组传递。
p: 表示可以执行存在于系统环境变量中的可执行文件。
e:环境变量的意思
void exit(int status);
功能:正常退出一个进程。
参数: status: 用来传递进程的退出状态。
exit(0);通常表示正常退出
exit(1);表示失败退出。
exit 在退出进程时会刷新标准IO的缓冲区
void _exit(int status);
功能:正常退出一个进程。
参数: status: 用来传递进程的退出状态。
_exit 在退出进程时会不会刷新标准IO的缓冲区
孤儿进程:父进程先退出,子进程仍然执行。
这样会导致子进程被系统的1号进程收养。
僵尸进程:父进程仍然运行,子进程先退出。
当父进程退出时,僵尸进程也会消失。
处理僵尸进程的方法:
wait 、waitpid 用来处理退出的子进程。
第一种方法:
pid_t wait(int *status);
功能:就是阻塞等待当前进程中任意一个子进程退出
参数: status:用来保存退出进程的退出状态。
返回值:成功就返回退出进程的pid
失败,返回-1.
exit(status);
可以通过 WEXITSTAT(status)来得到子进程退出时的真实状态。
第二种方法:
pid_t waitpid(pid_t pid, int *status, int options);
参数:pid:
常用: pid>0:只等待进程ID等于pid的子进程,不管已经有其他子进程
常用: pid=-1:等待任何一个子进程退出,此时和wait作用一样。
pid=0:等待其组ID等于调用进程的组ID的任一子进程。
pid < -1: :等待其组ID等于pid的绝对值的任一子进程。
status: 等价于wait中的status
options:
常用: WNOHANG:若由pid指定的子进程并不立刻退出,则waitpid不阻塞等待,此时返回值为0 。
WUNTRACED:如果子进程进入暂停执行则马上返回,但终止状态不予理睬。
WCONTINUED:
返回值:如果有子进程退出,那么返回退出的pid号
使用选项WNOHANG且没有子进程结束时:0
调用失败,返回-1.
wait(status) ==== waitpid(-1, status, 0);
第三种方法:???
问题:使用父子进程实现文件拷贝?
父进程拷贝前一半, 子进程拷贝后一半。
1、fork产生一个子进程
2、open获得源文件、目标文件描述符
3、统计源文件的字节数
4、实现为目标文件分配与源文件大小相同的空间
5、在子进程的代码中先关闭之前继承过来的描述符,然后重新打开两个文件,获得自己的描述符。
6、两个进程在目标文件的不同位置开始拷贝。
read write函数实现拷贝。
7、父进程要等待子进程退出,避免僵尸进程。
守护进程
创建方法⑴步骤:
①、创建子进程,父进程退出 (先获得孤儿进程)
②、在子进程中创建新会话
setsid();
创建一个新会话,并且当前进程变为会话组组长。
函数能够使进程完全独立出来,从而脱离所有其他进程的控制。
③、改变当前目录为根目录
chdir("/"); 通常将守护进程的工作目录设置为根目录。
④、重设文件权限掩码
umask(0);
⑤、关闭不需要的文件描述符
int file_num = getdtablesize(); 获得最大的描述符值 + 1;
for (fd = 0; fd < fdtablesize; fd++)
close(fd);
创建方法⑵:daemon()函数
六.线程:
是一种轻量级的进程
使用多线程的好处:
(1)大大提高了任务切换的效率;
(2)避免了额外的TLB & cache的刷新
TLB(Translation Lookaside Buffer)传输后备缓冲器是一个内存管理单元用于改进虚拟地址到物理地址转换速度的缓存;TLB是一个小的,虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块。如果没有TLB,则每次取数据都需要两次访问内存,即查页表获得物理地址和取数据。
高速缓冲存储器是存在于主存与CPU之间的一级存储器, 由静态存储芯片(SRAM)组成,容量比较小但速度比主存高得多, 接近于CPU的速度。
Linux里同样用task_struct来描述一个线程。线程和进程都参与统一的调度
一个进程中的多个线程:共享一部分资源的, 也有自己的私有资源。
线程基本操作:
a.创建线程
b.删除线程
c.控制线程
线程头文件:#include <pthread.h>
多线程通过第三方的线程库来实现
gcc pthread_create.c -lpthread
线程操作函数:
(1) 创建线程
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
功能:就是创建一个线程。
参数:thread: 线程的标识符。
attr: 用来设置线程的属性(通常不需要设置,直接设置为NULL)attribute
start_routine: 回调函数,就是线程要执行的函数。
arg:传递给 start_routine的参数
返回:成功返回0, 失败返回非负的errno值。
(2) 回收(释放)线程相关资源
int pthread_join(pthread_t thread, void **retval);
功能:就是阻塞等待一个线程的退出,回收其资源。
参数:thread:指定要等待退出的线程id。
retval: 接收退出线程的状态。
返回:成功返回0, 失败返回非负的errno值。
(3) 退出线程
void pthread_exit(void *retval);
功能:线程退出
参数:retval 退出状态。
线程间同步和互斥机制:
信号量
互斥锁
条件变量
互斥锁:
(1) 初始化互斥锁
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
功能:就是初始化一把锁
参数:mutex: 是锁的标识符。
attr: 用来设置线程锁的属性(通常为NULL)
返回:成功返回0, 失败返回非负的errno值。
用法:pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
(2) 互斥锁加锁
int pthread_mutex_lock(pthread_mutex_t *mutex)
功能:加锁,是阻塞等待,直到该锁可用为止。
参数:就是初始化后的锁。
返回:成功:0
出错:-1
(3) 互斥锁解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex)
功能:解锁
参数:就是初始化后的锁。
返回:成功:0
出错:-1
(4)互斥锁销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
功能:就是销毁一把锁
参数:mutex,将要被销毁的锁。
返回:成功:0
出错:-1
信号量:
基于POSIX的无名信号量:
头文件:#include <semaphore.h>
(1) 信号量初始化
int sem_init(sem_t *sem, int pshared, unsigned int value);
功能: 信号量值的初始化
参数: sem: 信号量的标识符
pshared: 通常为0, 表示该信号量应用于线程之间。
1, 表示该信号量应用于进程之间。
value:信号量的初始值。
(2) 申请信号量资源(阻塞)
int sem_wait(sem_t *sem);
功能:用于阻塞申请信号量资源, P操作。
每执行一次,信号量的值减 1.
参数:要申请的信号量
(3) 释放资源
int sem_post(sem_t *sem);
功能:释放资源 , V操作
每执行一次,信号量的值加1.
(4) 申请信号量资源(非阻塞)
int sem_trywait(sem_t *sem);
功能:非阻塞申请资源。
(5) 获取当前信号量的值
int sem_getvalue(sem_t *sem, int *svalue);
功能:获得当前信号值的值。
参数:sem, 要操作的信号量
svalue, 存放得到的值。
(6) 信号量销毁
int sem_destroy(sem_t *sem);
功能:销毁一个信号量
条件变量:睡眠、唤醒
说明:条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。
(1) 使线程睡眠----等待
说明:必须和一个互斥锁配合,以防止多个线程同时请求pthread_cond_wait()的竞争条件(Race Condition)。mutex互斥锁必须是普通锁(PTHREAD_MUTEX_TIMED_NP)或者适应锁(PTHREAD_MUTEX_ADAPTIVE_NP),且在调用pthread_cond_wait()前必须由本线程加锁(pthread_mutex_lock()),而在更新条件等待队列以前,mutex保持锁定状态,并在线程挂起进入等待前解锁。在条件满足从而离开pthread_cond_wait()之前,mutex将被重新加锁,以与进入pthread_cond_wait()前的加锁动作对应。
函数声明:
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
功能:使线程的睡眠函数
参数:cond :睡眠的条件。
mutex: 互斥锁
特点:线程睡眠时需要得到一把锁,在睡眠之后有会将锁释放。
但是,当线程被唤醒时需要重新上锁,如果该锁已经被其他线程占用,那么该线程继续睡眠。
内部操作:
pthread_mutex_lock(&mutex)
pthread_cond_wait()
{
pthread_mutex_unloct(&mutex);
if(条件不满足)
睡觉
else
{
pthread_mutex_lock(&mutex);
return;
}
}
do something();
pthread_mutex_unlock(&mutex);
(2) 唤醒一个指定条件线程----激发
说明:激活一个等待该条件的线程,存在多个等待线程时按入队顺序激活其中一个。
int pthread_cond_signal(pthread_cond_t *cond);
功能:只能唤醒一个指定条件线程。
参数:cond
(3) 唤醒所有指定条件线程----激发
说明:激活所有等待线程
int pthread_cond_broadcast(pthread_cond_t *cond);
功能:唤醒所有指定条件线程。
参数: cond
(4) 销毁条件变量
说明:只有在没有线程在该条件变量上等待的时候才能注销这个条件变量,否则返回EBUSY。注销动作只包括检查是否有等待线程
int pthread_cond_destroy(pthread_cond_t *cond);
功能: 销毁一个条件变量
(5)初始化条件变量
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
功能:初始化一个条件变量。
参数:cond: 条件变量
attr:属性,通常为NULL.
临界资源:多道程序系统中存在许多进程,它们共享各种资源,然而有很多资源一次只能供一个进程使用。一次仅允许一个进程使用的资源称为临界资源。
七.进程间通信机制:
⑴system V IPC 其通信进程主要局限在单个计算机内
⑵BSD 形成了基于套接字(socket)的进程间通信机制
⑶POSIX 进程间通信机制。
system V IPC:①共享内存(share memory)
②消息队列(message queue)
③信号灯(semaphore)
传统的进程间通信方式:④无名管道(pipe)
⑤有名管道(fifo)
⑥信号(signal)
⑦套接字
㈠.无名管道:存在于内存中。
特点: 只能用于具有亲缘关系的进程之间的通信
半双工的通信模式,具有固定的读端和写端
管道可以看成是一种特殊的文件,对于它的读写可以使用文件IO如read、write函数。
int pipe(int fd[2])
功能:创建一个无名管道。
参数:fd.
fd[0] 是无名管道的读端
fd[1] 是无名管道的写端
管道的大小有限制:64KB
测试无名管道的大小?
无名管道的特性:当管道被填满时, 写操作会被阻塞即write函数不会返回。
pipe在父子进程之间通信:
㈡.有名管道(fifo):可以在没有亲缘关系的进程间通信。
有名管道可以通过路径名来指出,并且在文件系统中可见。
有名管道的创建:双向的读写。
第一种方法: mkfifo 命令,即mkfifo 文件名
第二种方法: int mkfifo(const char *filename,mode_t mode);
功能:创建有名管道
参数:filename:要创建的管道名;mode:管道的访问权限
返回值:成功返回0, 失败返回-1.
头文件:#include <sys/types.h>
#include <sys/stat.h>
特点:
open函数打开fifo文件时:如果一个进程指定的O_RDONLY或者O_WRONLY,那么open此时会阻塞,直到另外一个进程以对应的方式打开为止。
在一个进程中直接打开,可以用O_RDWR标志位。
fifo它是随着进程维护,当所有操作该管道的进程都结束时,那么管道中的数据就会被丢弃。
open("fifo", O_WRONLY|O_NONBLOCK);
O_NONBLOCK:以非阻塞的方式打开特殊文件, 以后对该文件的操作都是非阻塞模式。
㈢.信号 signal:
信号是在软件层次上对中断机制的一种模拟(内核模拟),是唯一一种异步通信机制;信号是由内核产生。信号可以直接进行用户空间进程和内核进程之间的交互,内核进程也可以利用它来通知用户空间进程发生了哪些系列事件。内核进程可以在任何时刻发送信号给某一进程,无需知道该进程的状态。若该进程处于未执行态,那么内核就会保存该信号,直到该进程恢复执行态再传给它;若信号被进程设置为阻塞,那么该信号的传递被延迟,直到其阻塞被取消才被传递给进程。
linux支持的信号类型:不可靠信号(有默认处理方式,也可以自定义)、可靠信号(用户自定义处理方式)。由命令kill –l在终端上显示的序号1-31这31个信号为不可靠信号;序号34-64这31个信号为可靠信号。
常用信号:
不可以被忽略、阻塞、处理:
SIGKILL:不可以被忽略、阻塞、处理, 接收到该信号的进程只能退出。默认终止。
SIGSTOP:该信号用于暂停一个进程,且不能被阻塞、处理或忽略。
SIGALRM:该信号当一个定时器到时的时候发出, 默认操作时终止进程。
SIGCHLD:子进程改变状态时,父进程会收到这个信号,默认忽略。
SIGABORT:该信号用于结束进程。默认终止。
SIGHUP:与终端相关,该信号在用户终端连接结束时发出,通常在终端的控制进程结束时,通知同一会话内的各个进程与控制终端不再联系。默认终止。
SIGILL:该信号在一个进程企图执行一条非法指令时(可执行文本本身出现错误,或企图执行数据段 堆栈溢出时)发出。默认终止。
SIGFPE:该信号在发生致命的运算错误时发出。默认终止。
按键产生信号:
SIGTSTP:该信号用于暂停交互进程,用户可键入SUSP字符(通常是Ctrl+Z)发出这个信号。
SIGINT:该信号在用户键入INTR字符(通常是Ctrl+C)时发出,终端驱动程序发送此信号并送到前台进程中的每一个进程。默认终止。
SIGQUIT:该信号和SIGINT类似,但由QUIT字符(通常是Ctrl+)来控制。默认终止。
用户进程对信号的响应方式:
①忽略信号:对信号不做任何处理,但是有两个信号不能忽略:即SIGKILL及SIGSTOP。
②捕捉信号(用户自定义):定义信号处理函数,当信号发生时,执行相应的处理函数。
③执行缺省操作:Linux对每种信号都规定了默认操作
与信号相关的函数:
⑴int kill(pid_t pid, int sig);
功能:给进程发送信号
参数:pid:正数:值就是指定要接收信号的进程的进程号
0:信号被发送到所有和当前进程在同一个进程组的进程
-1:信号发给所有的进程表中的进程(除了进程号最大的进程外)
<-1:信号发送给进程阻号为-pid的每一个进程
sig: 要发送的信号类型
头文件:#include<signal.h>
#include<sys/types.h>
kill(getpid(), xxx); 可以给自己发送信号。
返回:成功返回0, 失败返回-1。
如果sig的值小于0或sig大于64,那么函数kill不能发送信号,执行出错;如果sig等于0,那么函数kill没有发送信号(发送空信号),但是函数执行成功,不会报错,这样的操作可以用于检查一个进程或进程组是否存在。
⑵int raise(int sig);
功能:给自身发送信号
参数:sig:信号类型
返回值:成功:0 ,出错:-1
头文件:#include<signal.h>
⑶unsigned int alarm(unsigned int seconds)
该函数不会阻塞。
功能:给当前进程设置一个闹钟, 默认在定时时间到达之后进程会退出。
alarm: 在一个进程中只能有一个(最后一个)定时有效。
返回值:成功:如果调用此alarm()前,进程中已经设置了闹钟时间,则
返回上一个闹钟时间的剩余时间,否则返回0。
失败: 返回-1.
int pause(void);
功能:阻塞等待任意一个信号。
头文件:#include <unistd.h>
对信号捕捉处理:
signal函数
sigaction函数
signal函数:
⑷typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
功能:用于捕捉、处理信号。
参数: signum: 指定要捕捉的信号类型
handler; 代表对捕捉到的信号进行自定义处理。
SIG_IGN, 表示对捕捉到的信号进行忽略。
SIG_DFL, 表示对该信号进行默认处理。
signal(SIGINT, handler) == SIG_DFL
返回值: 成功返回上一次对信号的处理方式
失败, SIG_ERR
头文件:#include <signal.h>
system V 的IPC机制(3种):
①共享内存(效率最高)
②消息队列
③信号灯集
ipcs: 用来查看system V的IPC机制标识符的命令
ipcs -m shared memory segments显示当前系统中 共享内存的使用
-q message queues显示当前系统中 消息队列的使用
-s semaphores显示当前系统中 信号灯集的使用
-a all (default)显示当前系统中 共享内存,消息队列,信号灯集的使用
-h 显示帮助
-t time显示时间
-p pid显示进程号
-c creator显示创建者
-l limits显示容量限制,最大或最小
-u summary显示使用状态
ipcrm 用来删除 当前系统中 system V的IPC机制标识符的命令
-m 删除当前系统中 共享内存的标识符
-q 删除当前系统中 消息队列的标识符
-s 删除当前系统中 信号灯集的标识符
如: ipcrm -m 共享内存的id
⑴.将文件名转换成key值
说明:在IPC(InterProcess Communication)的通信模式下,不管是使用消息队列还是共享内存,甚至是信号量,每个IPC的对象(object)都有唯一的名字,称为“键”(key)。通过“键”,进程能够识别所用的对象。“键”与IPC对象的关系就如同文件名称之于文件,通过文件名,进程能够读写文件内的数据,甚至多个进程能够共用一个文件。而在IPC的通讯模式下,通过“键”的使用也使得一个IPC对象能为多个进程所共用。
key_t ftok(const char *pathname, int proj_id);
功能:获得一个key值。
参数: pathname 文件名,文件索引号,一般由命令ls –i 查询文件索引号
proj_id 就是一个整数(低八为不能为0),由于该int型大小范围0-255,所以一般传入字符。
头文件:#include <sys/types.h>
#include <sys/ipc.h>
如:key_t key;
key = ftok(".", 'a');
㈣.共享内存:
内存模型:在 Linux 系统中,每个进程的虚拟内存是被分为许多页面的。这些内存页面中包含了实际的数据。每个进程都会维护一个从内存地址到虚拟内存页面之间的映射关系。尽管每个进程都有自己的内存地址,不同的进程可以同时将同一个内存页面映射到自己的地址空间中,从而达到共享内存的目的。
分配一个新的共享内存块会创建新的内存页面。因为所有进程都希望共享对同一块内存的访问,只应由一个进程创建一块新的共享内存。再次分配一块已经存在的内存块不会创建新的页面,而只是会返回一个标识该内存块的标识符。一个进程如需使用这个共享内存块,则首先需要将它绑定到自己的地址空间中。这样会创建一个从进程本身虚拟地址到共享页面的映射关系。当对共享内存的使用结束之后,这个映射关系将被删除。当再也没有进程需要使用这个共享内存块的时候,必须有一个(且只能是一个)进程负责释放这个被共享的内存页面。
所有共享内存块的大小都必须是系统页面大小的整数倍。系统页面大小指的是系统中单个内存页面包含的字节数。在 Linux 系统中,内存页面大小是4KB,不过您仍然应该通过调用 getpagesize 获取这个值。
共享存储器的执行方式:是将一个储存器区段标记为共用,这时各进程可以把这个区段映射到该进程本身的虚拟地址里。建立共享存储器可通过shmget系统调用,shmget执行后,核心程序就保留一块指定大小的空间,同时关于此共享存储器的一切数据,如区段的长度,区段的存取权,区段建立者的进程识别码等存入一个叫shmid_ds的结构。现在共享存储器虽然已经建立了,可是仍无法连上它,这时就须通过shmat系统调用得到一个指向共享存储器基址的指针,通过此指针,就可以如同于操作一般存储器似的取用共享存储器。shmdt进行相反的工作,用来脱离已连上的共享存储器。(数据保护)
⑵创建/打开共享内存:
int shmget(key_t key, size_t size, int shmflg);
功能:分配获得一片共享内存,实际只告诉内核,而内核并没有分配,只是同意你得到一个毫无意义的标识符。
参数:key, 用来使用不同进程使用相同的共享内存。
IPC_PRIVATE:表示申请的共享内存是私用的。
key = ftok(xxx,yyy); 需要通过ftok函数来获得。
size:进程要申请的共享内存的大小(字节)如果一段进程只申请一块只有一个字节的内存,内存也会分配整整一页(在i386机器中一页的缺省大小PACE_SIZE=4096字节)这样,新创建的共享内存的大小实际上是从size这个参数调整而来的页面大小。即如果size为1至4096,则实际申请到的共享内存大小为4K(一页);4097到8192,则实际申请到的共享内存大小为8K(两页)。size---/proc/sys/kernel/shmmax
shmflg: IPC_CREAT :这个标志表示应创建一个新的共享内存块。通过指定这个标志,我们可以创建一个具有指定键值的新共享内存块。
IPC_EXCL:这个标志只能与 IPC_CREAT 同时使用。当指定这个标志的时候,如果已有一个具有这个键值的共享内存块存在,则shmget会调用失败。
指定的共享内存的权限:IPC_CREAT|IPC_EXCL| 0666
返回值:成功返回 就是获得的共享内存的标识符。
失败, 返回-1.
头文件:#include <sys/ipc.h>
#include <sys/shm.h>
用法:
int shmid;
key = ftok(".", 'a');
shmid = shmget(key, 1000,IPC_CREAT|IPC_EXCL|0666); 创建
shmid = shmget(key, 1000,0666)打开共享内存:
⑶共享内存的映射
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数: shmid 共享内存标识符
shmaddr:一个指针,指向您希望用于映射该共享内存块的进程内存地址,若为 NULL表示让系统为用户自动分配内存
非NULL表示用户自定义空间
shmflg: SHM_RDONLY 表示对共享内存的操作,只可以读。
SHM_RND表示第二个参数指定的地址应被向下靠拢到内存页面大小的整数倍
0 表示可读可写
返回值:成功返回映射后的共享内存块对应的地址。
失败返回NULL。
头文件:#include <sys/ipc.h>
#include <sys/shm.h>
⑷解除映射:
int shmdt(const void *shmaddr);
功能:解除映射。
参数:就是shmat的返回值
成功返回0, 失败返回-1.
头文件:#include <sys/types.h>
#include <sys/shm.h>
⑸控制共享内存:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
功能:共享内存的控制函数
参数:shmid: 要操作的共享内存的标识符
cmd: IPC_STAT (获取对象属性), 传递一个指向一个 struct shmid_ds 对象的指针作为第三个参数
IPC_SET (设置对象属性),第三个参数存放将要设置的属性
IPC_RMID (删除对象), 此时第三个参数为NULL。
返回值:成功返回0, 失败返回-1.
头文件:#include <sys/ipc.h>
#include <sys/shm.h>
system函数:用于执行shell命令
int system(const char *command);
参数:command 要执行的命令
system("ipcs -m");
system V消息队列:
#include <sys/msg.h>
1、消息队列的创建、打开
int msgget(key_t key, int flag);
参数:key, ftok获得值。
flag: IPC_CREAT
IPC_EXCL
msgget(key, IPC_CREAT|IPC_EXCL|0666);
返回值:消息队列的标识符。
2、int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
功能:往一个消息队里中发送消息。
参数: msqid: 要操作的消息队列的标识符--(对象)
msgp: 存放要发送的消息。指向要发送的消息所在的内存
msgp的定义要求: struc msgbuf
{
long type;//必须有,表示消息的类型
消息的数据
}
msgsz:表示发送的消息的正文的长度(除去消息类型之外)。
sizeof(struct msgbuf) - sizeof(long);
msgflg:决定了消息的发送是否是阻塞方式
IPC_NOWAIT 非阻塞方式发送
0 阻塞等待直到发送成功
返回值:成功返回0, 失败返回-1
3、ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
功能:从消息队列中读取一条消息。
参数: msqid: 要读取的消息队列
msgp: 接收消息的缓冲区
struc msgbuf
{
long type;//必须有,表示消息的类型
消息的数据
}
msgsz: 要接收的消息的正文长度。
msgtyp: 要读取的消息队列中的消息类型。
0, 表示读取消息队列中的第一条消息。
> 0, 表示读取消息队列中类型为msgtyp的消息。
< 0, 接收消息队列中类型值不小于msgtyp的绝对值且类型值又最小的消息。
msgflg:决定了消息的接收是否是阻塞方式
IPC_NOWAIT 非阻塞方式接收
0 阻塞等待直到有对应消息
返回值:
失败返回-1
成功返回读取到消息的正文的字节个数。
4、int msgctl ( int msgqid, int cmd, struct msqid_ds *buf );
功能:消息队列的控制函数
参数:msgqid 要操作的消息队列的标识符
cmd:
IPC_STAT (获取对象属性), 存放在第三参数中
IPC_SET (设置对象属性),第三个参数存放将要设置的属性
IPC_RMID (删除对象), 此时第三个参数为NULL。
成功返回0, 失败返回-1.
system V的 信号灯集:以一种计数信号灯
#incldue <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
功能:创建或者打开一个信号灯集合
参数: key ftok获得值。
nsems: 表示要申请的信号灯集合中包含的信号量的个数。
semflg: IPC_CREAT
IPC_EXCL
semget(key, 2, IPC_CREAT|IPC_EXCL|0666);
返回值:返回一个信号灯集合的标识符。
int semctl(int semid, int semnum, int cmd, ...);
功能:对信号灯集合的控制
参数: semid
semnum: 要操作该集合中信号量的编号, 信号量的编号从0开始。
cmd:
GETVAL:获取信号灯的值
SETVAL:设置信号灯的值 semclt(semid, 0, SETVAL, 共用体);
IPC_RMID:从系统中删除信号灯集合 semctl(semid, 0, IPC_RMID);
第四个参数:
union semun {
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO
(Linux-specific) */
};
返回值:成功返回0,失败返回-1.
int semop ( int semid, struct sembuf *opsptr, size_t nops);
功能:信号灯 集合的PV操作。
参数:semid
struct sembuf{
unsigned short sem_num; /* semaphore number */ 要操作的信号量的编号。
short sem_op; /* semaphore operation */ 要执行的PV操作
short sem_flg;/* operation flags */
};
sem_num成员:要操作的信号量的编号。
semop成员: 0 等待,直到信号灯的值变成0
: > 0 表示信号量的值要增加semop个, 相当于V
: < 0 表示信号量的值要较少semop个, 相当于P
flag:
0, semop函数阻塞操作。
IPC_NOWAIT, semop函数阻塞操作。
SEM_UNDO(不常用)
nops:semop函数执行一次要操作的信号量的个数。
返回值:成功返回0,失败返回-1.
问题:
使用syetem V的信号灯集合实现对共享内存的互斥操作?
要求:两个C文件(一个读内存、一个写内存)
需要两个信号量:一个控制读内存, 要申请读的信号量资源(0)。
一个控制写内存 ,要申请写的信号量资源(1)。
处理僵尸进程的第三种方法(可移植性较差):
SIGCHLD
signal(SIGCHLD, SIG_IGN);