库函数和系统调用的区别:
1.库函数可以理解为是对系统调用的一层封装。
2.系统调用作为内核提供给用户程序的接口,它的执行效率是比较高效而精简的。
3.但有时我们需要对获取的信息进行更复杂的处理,或更人性化的需要,我们把这些处理过程封装成一个函数再提供给程序员,更方便于程序编码。
Linux系统中进程状态转换:
Linux操作系统有三个最基本的状态:
运行态:进程占用CPU,并在CPU上运行
就绪态:进程已经具备运行条件,但是CPU还没有分配过来
阻塞态:进程因等待某件事发生而暂时不能运行
进程在一生中,都处于上述3中状态之一。
三种状态之间转换分为四种情况:
运行---》就绪:这是调度引起的,主要是进程占用CPU的时间过长
就绪---》运行:运行进程的时间片用完,调度就转到就绪队列中选择合适的进程分配CPU
运行---》阻塞:发生了I/O请求或等待某件事的发生
阻塞---》就绪:进程所等待的事件发生,就进入就绪队列
以上4种情况可以相互正常转换。
文件
相关函数:
//创建 返回一个文件描述符 或者 -1:不成功
open(pathname, O_WRONLY|O_CREAT|O_TRUNC, mode);
or
create(const char *pathname, mode_t mode);
//读 返回值有整数:请求读取的字节数 或者 0 或者 -1:出错
read(int fd, void *buf, size_t count);
//写 返回写入的字节数或者 -1
write(int fd, void *buf, size_t count);
//修改文件偏移量 返回新的偏移量 或者 -1:不成功
lseek(int fd, off_t offset, int whence);
//关闭文件 返回0:成功 或者 -1:不成功
int close(int fd);
//获取文件属性 返回0:成功 或者 -1:不成功
stat(const char *path, struct stat *buf);
//测试权限 返回0:成功 或者 -1:不成功
access(const char *pathname, int mode);
//修改权限 返回0:成功 或者 -1:不成功
chmod(const char *path, mode_t mode);
//修改文件大小 返回0:成功 或者 -1:不成功
truncate(const char *path, off_t length);
//从文件读取一行送到缓冲区的首地址s,size是缓冲区的长度
fgets(char *s,int size,FILE *stream)
//向指定文件写入一个字符串
fputs(const char *s,FILE *stream)
mode第一位默认0,和suid和guid相关特殊权限需要更改,后面三组权限,第一组是u所有者,第二组是g所属组,第三组是o其他人 ,r :读4,w: 写2 ,x: 执行1。
open函数创建一个新文件时,如果指定是0777,实际创建的文件权限是0775(rwxrwxr-x),因为收到文件权限掩码的限制,限制其它用户的写权限,防止文件被篡改。
Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0、标准输出1、标准错误2,因此新创建的文件返回的文件描述符大于2。
读取时出现找不到命令,由于输入内容超过读取内容,剩余内容保存在内核的终端设备输入缓冲区
,读取进程结束,Shell进程继续从终端读走剩下的内容和换行符,把它当成一条命令,出现找不到命令。
进程
相关函数:
//创建进程 父进程的fork函数会返回子进程的pid,子进程的fork函数会返回0.不成功:若子进程创建失败,原程序不会复制,父进程的fork函数返回-1。
fork(void);
//exe函数族
execl(const char *path, const char *arg, ...);
execlp(const char *file, const char *arg, ...);
execle(const char *path, const char *arg, ..., char * const envp[]);
execv(const char *path, char * const argv[]);
execvp(const char *file, char * const argv[]);
execve(const char *path, char * const argv[], char * const envp[]);
//示例
char *arg[]={"-a","-l","test.c", NULL};
execvp("ls", arg);
or
execlp("ls","-a","-l","test_exec.c",NULL);
//进程退出
_exit(int status);
//对_exit进行了包装 先检查文件的打开情况,将缓冲区中的内容写回文件。相对来说exit比_exit更为安全
exit(int status);
//挂起进程 使其阻塞 返回子进程id:成功 或者 -1:不成功
wait(int *status);
//判断子进程是否正常退出,若是,返回非0值,否则返回0
WIFEXITED(int status);
//和WIFEXITED配合使用,WIFEXITED返回非0值,则使用该宏提取子进程的返回值。
WEXITSTATUS(int status);
//等待指定的子进程,也可以在父进程不阻塞的情况下获取子进程的状态。
//返回捕捉到的子进程id:成功 或者 0:options = WNOHANG, waitpid发现没有已退出的子进程可回收 或者-1:出错
waitpid(pid_t pid, int *status, int options);
在多次运行后出现终端出现后依旧输出进程信息,因为终端与其它进程公平竞争CPU因此终端出现后可能仍然有进程信息输出。
多次执行fork会发现,child process后输出的ppid不等于parent process的pid,而等于1。
出现这种情况,是因为父进程先于子进程终止,子进程变成“孤儿进程”,后面由init进程来接收。
在Linux系统中,子进程应由父进程回收,但是当子进程被创建后,它与它的父进程及其它进程共同竞争系统资源,所以父子进程执行的顺序是不确定的,终止的先后顺序也是不确定的。所以会导致子进程的编号不递增。
孤儿进程:
父进程负责回收子进程,如果父进程在子进程退出之前退出,子进程就会变成孤儿进程,此时init进程将代替父进程完成子进程的回收工作。
僵尸进程:
调用exit函数后,该进程不会马上消失,而是留下一个称为僵尸进程的数据结构。它几乎放弃进程退出前占用的所有内存,既没有可执行代码也不能被调度,只是在进程列表中保留一个位置,记载进程的退出状态等信息供父进程回收。若父进程没有回收子进程的代码,子进程将会一直处于僵尸态。
僵尸进程不能再次被运行,会占用一定的内存空间,并占据进程编号,当僵尸进程较多时,将会消耗系统内存,新进程可能因内存不足或无法获取pid而无法创建。
当僵尸进程的父进程被终止后,僵尸进程将作为孤儿进程被init进程接收,init进程会不断调用wait()函数获取子进程状态,对已处于僵尸态的进程进行处理。
孤儿进程永远不会成为僵尸进程。
终端、会话、进程组和进程之间的关系:
1、一个会话对应一个控制终端。
2、一个会话又可以包含多个进程组。
3、进程组中的这些进程之间不是孤立的,彼此之间或者存在者父子、兄弟关系,或者在功能有相近的联系。
4、每个进程必定属于一个进程组,也只能属于一个进程组。一个进程除了有进程ID外,还有一个进程组ID,每个进程组也有唯一的进程组ID。每个进程组有一个进程组组长,进程组组长的进程ID和组ID相同。
进程间通信(管道)
相关函数:
//创建匿名管道 返回0:成功 或者 -1:不成功 有读端、写端所以文件描述符为fd[2]
pipe(int pipefd[2]);
//重定向函数 返回newfd:成功 或者 -1:不成功
dup2(int oldfd, int newfd);
//创建管道,创建子进程,调用shell命令 返回管道文件的描述符:成功 或者 -1:不成功
popen(const char *command, const char *type);
//示例
Rfp = popen("ls","r"); //读取命令执行结果
Wfp = popen("wc -l","w"); //将管道中的数据传递给进程
//关闭popen打开的I/O流,等待子进程执行结束,返回终止状态 返回0:成功 或者 -1:不成功
pclose(FILE *stream);
//创建命名管道 返回0:成功 或者 -1:不成功
mkfifo(const char *pathname, mode_t mode);
可以将管道抽象成文件,进行文件相关操作。
匿名管道没有名字,只能用于有亲缘关系的进程间通信。
命名管道与系统中的路径名关联,以文件形式存在,系统中的不同进程可以通过路径名访问文件实现通信。
FIFO文件和普通文件的区别
1.FIFO文件是对内存进行操作
2.普通文件是存储在硬盘
3.对内存的读取会比硬盘的读写要快很多
4.两个进程通过普通文件也可以通信
匿名管道和命名管道的异同:
1、匿名管道是由pipe函数创建并打开的
2、命名管道是由mkfifo函数创建的 ,打开用open
3、命名管道和匿名管道唯一的区别就是它们创建和打开的方式不同,一旦这些工作完成后,它们有相同的语义
线程操作
相关函数:
// 创建线程 返回0:成功或者error:不成功
pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
//获取线程ID
pthread_self
//示例
void *tfn(void *arg) {
printf("tfn--pid=%d,tid=%lu\n", getpid(), pthread_self());
return (void*)0;
}
pthread_t Tid;
int Ret = pthread_create(&Tid, NULL, tfn, NULL);
//线程退出
pthread_exit(void *retval);
//线程中止 返回0:成功或者error:不成功
pthread_cancel(pthread_t thread);
//挂起线程 返回0:成功或者error:不成功
pthread_join(pthread_t thread, void **retval);
//线程分离 返回0:成功或者error:不成功 被分离的线程在执行结束后将会自动释放,不再等待其它线程回收。
pthread_detach(pthread_t thread);
进程和线程的联系:
当一个进程创建一个线程时,原有的进程就会变成线程,两个线程共用一段地址空间。
对内核而言,线程和进程没有区别,CPU会为每个线程与进程分配时间片,通过进程控制块来调度不同的线程和进程。
进程和线程的区别:
进程拥有独立的地址空间,当使用fork函数创建新进程时,若其中一个进程要对fork之前的数据进行修改,进程会根据“写时复制”原则,先复制一份该数据到子进程的地址空间,再修改数据。即便是全局变量,在进程间也不是共享的。
线程间共享地址空间,一个线程对全局取的数据进行了修改,其它线程访问到的也是修改后的数据。
多线程运行未成功其它线程未执行原因:
线程与进程不同,若作为程序入口的原线程退出,系统内部会调用exit函数,导致同一进程中的所有线程都退出。解决方法:使用sleep函数使原线程阻塞,保证新创建的线程顺利执行。或者使用线程挂起函数。
原线程的退出之所以会导致其它线程退出,是因为原线程执行完毕后,main函数会隐式调用exit函数,而pthread_exit函数可以只使调用该函数的线程退出。若在原线程调用return之前调用pthread_exit,同样可以保证其它线程的正常运行。