Linux环境编程之一

0. 前言

本文主要是想整理一下linux环境编程的一些接口,包括一些系统调用,以及更多的glibc的库函数,并不对其中原理进行限制,属于工具字典类型,面向使用。

本文主要参考:《Linux环境编程:从应用到内核》

1. 文件I/O

打开open

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

第1个参数pathname:表示要打开的文件路径;

第2个参数flags:用于指示打开文件的选项,常用的有O_RDONLY(0:只读)、 O_WRONLY(1:只写)和O_RDWR(2:读写),另外还有:

选项含义
O_APPEND每次写操作,内核都会先定位到文件尾
O_ASYNC使用异步I/O模式
O_CLOEXECfork时,子进程关闭文件
O_CREATE当文件不存在时,就创建文件
O_EXCL标志本次操作是创建,与O_CREATE连用;当文件存在,返回失败
O_NONBLOCK将文件描述符设置为非阻塞的
O_SYNC设置为I/O同步模式,每次写操作都会将数据同步到磁盘
O_TRUNC打开文件时,文件长度截断为0

第3个参数mode:只有在创建文件时需要,用于指定所创建文件的权限位

创建create

int creat(const char * pathname, mode_tmode);

create函数用于创建新文件,其等价于open(pathname,O_WRONLY|O_CREAT|O_TRUNC,mode)

关闭close

int close(int fd);

成功返回0,失败返回-1;用于关闭文件描述符。文件描述符可以是普通文件,也可以是设备,还可以是socket,VFS会根据不同的文件类型,执行不同的操作。

文件偏移lseek

off_t lseek(int fd, off_t offset, int whence);

描述:将fd的文件偏移量设置为以whence为起点,偏移为offset的位置。

第3个参数whence:可以为SEEK_SET(文件起始位置)、SEEK_CUR( 文件当前位置)、SEEK_END(文件的末尾)。

返回新的文件偏移量。当执行成功时,它会返回最终以文件起始位置为起点的偏移位置。如果出错,返回-1,同事errno被设置为对应的错误值。

但对于某些设备,它允许返回负的偏移量,这时候就需要同时判断errno与返回值。

读取read

ssize_t read(int fd, void *buf, size_t count);

描述:从fd中读取count字节到buf中,并返回成功读取的字节数,同时将文件偏移向前移动相同的字节数。返回0的时候表示已经到了文件末尾。

read返回值为-1,如果errno为EAGAIN、EWOULDBLOCK或者EINTR,一般不能视为错误。前两者为fd为非阻塞且没有数据可读时返回,后者是read被信号中断。

写入write

ssize_t write(int fd, const void *buf, size_t count);

描述:从buf指向的地址读取count字节,写入到文件描述符fd中,并返回成功写入的字节数,同事将文件偏移向前移动相同的字节数。

文件描述符复制dup

int dup(int oldfd);
int dup2(int oldfd, int newfd);
int dup3(int oldfd, int newfd, int flags);

dup:户使用一个最小的未用的文件描述符作为复制后的文件描述符。

dup2:使用用户制定的文件描述符newfd来复制oldfd的。如果newfd已经是打开的,linux会先关闭newfd。

dup3:多一个flags,目前只支持O_CLOEXEC,避免将文件内容暴露给子进程。

文件数据的同步sync

void sync(void);
int fsync(int fd);
int fdatasync(int fd);

描述:linux会对文件的I/O操作进行缓存,对于读操作,如果内容已经在文件缓存中,就直接读取文件缓存。对于写操作,会先将修改提交到文件缓存中,在合适的时机持久化到磁盘。以上函数会同步缓存中数据到磁盘。

后两者只同步fd指定的文件,并直到同步完成才返回。

文件元数据stat

int stat(const char *path, struct stat *buf);
int fstat(int fd, struct stat *buf);
int lstat(const char *path, struct stat *buf);

描述:3个函数用户得到文件的基本信息,区别在于stat得到路径path所指定的文件基本信息,fstat得到文件描述符fd指定文件的基本信息,lstat与stat基本相同,当path是一个连接文件时,lstat得到是连接文件本身的基本信息,而不是指向文件的信息。

文件的截断truncate

int truncate(const char *path, off_t length);
int ftruncate(int fd, off_t length);

含义相同,参数不同,一个是路径,一个是fd。截断可以是缩小,也可以是放大,将文件大小变成length。

2. 标准I/O库

打开fopen

FILE *fopen(const char *filename, const char *mode)

第1个参数filename:要打开的文件路径

第2个参数mode:

fopen标志位open标志位用途
rO_RDONLY以只读方式打开文件
r+O_RDWR以读写方式打开
wO_WRONLY | O_CREATE | O_TRUNC以写方式打开:文件存在,截断为0;当文件不存在,创建该文件
w+O_RDWR | O_CREATE | O_TRUNC以读写方式打开:文件存在,截断为0;当文件不存在,创建该文件
aO_WRONLY | O_APPEND | O_CREATE以追加写的方式打开文件,当文件不存在时,创建该文件
a+O_RDWR | O_APPEND | O_CREATE以追加读写的方式打开文件,当文件不存在时,创建该文件

返回FILE,表示文件流

打开fdopen与fileno

FILE *fdopen(int fd, const char *mode);
int fileno(FILE *stream);

linux提供了文件描述符,c库提供了文件流。以上2个函数是在这两者之间 的切换。

fdopen用于从文件描述符生成一个文件流FILE

fileno用于从文件流FILE得到对应的文件描述符

关闭fclose

int fclose(FILE *stream);

关闭文件,用fopen、fdopen打开的文件都需要用它来关闭文件。因为只有采用这种方式,fclose作为c库函数,才会释放文件流FILE占用的内存。

读取fget 和 getc

int fgetc(FILE *stream);
int getc(FILE *stream);

返回下一个字符,返回值是int,原因是到了文件末尾,会返回EOF,而EOF是一个int类型的负数。

读取与写入fread和fwrite

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

第1个参数ptr:指向要读取或写入的数据。

第2个参数size是长度。

第3个参数:nmemb用于指示fread和fwrite要执行的对象个数。

第4个参数:stream是目标的文件。

注意他们的返回值,返回的是成功读取或写入的多少个size大小的对象。

创建临时文件tmpnam

FILE *tmpfile(void);
int mkstemp(char *template);

描述:tmpfile返回一个以读写模式打开的、唯一的临时文件流指针。当文件指针关闭或程序正常结束时,该临时文件会被自动删除。

这个临时文件只能生成在固定的路径(/tmp)下,并且有可能因为文件名冲突而失败返回NULL。

mkstemp 会根据template创建并打开一个独一无二的临时文件。template的最后6个字符必须是XXXXXXX。glibc会生成一个独一无二的后缀来替换它。成功后会返回临时文件的文件描述符,失败时返回-1。

几个示例

#include <stdio.h>

int main () {
   FILE *fp;
   int c;
   int n = 0;
  
   fp = fopen("file.txt","r");
   if(fp == NULL) {
      perror("Error in opening file");
      return(-1);
   } do {
      c = fgetc(fp);
      if( feof(fp) ) {
         break ;
      }
      printf("%c", c);
   } while(1);

   fclose(fp);
   return(0);
}
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main(void)
{
    const char str[] = "123456789";
    FILE *fp = fopen("tmp.txt", "w");
    size_t size = fwrite(str, strlen(str), 1, fp);
    printf("size is %d\n", size);
    fclose(fp);
    return 0;
}

3. 进程

getpid获取进程id

pid_t getpid(void);
pid_t getppid(void);

gitpid函数获取进程的pid;

getppid获取父进程的pid。

系统进程数的上限可以从以下获取:

cat /proc/sys/kernel/pid_max
sysctl kernel.pid_max

进程的层次

pid_t getpgrp(void);
pid_t getsid(pid_t pid);
int setpgid(pid_t pid, pid_t pgid);
pid_t setsid(void);

getpgrp获取进程组id,

getsid获取会话的id,

setpgid设置进程的进程组,通常用来创建一个新的进程组

setsid创建新的会话,调用进程不能是进程组组长

可以通过ps axjf查看进程的层次关系

进程组和会话是支持shell作业控制而引入的概念。shell进程会话的首进程,会话的首进程pid就是整个会话的id。登录shell后,用户可能会使用管道让多个进程完成一项工作,这一组进程属于同一个进程组。

进程的创建fork

pid_t fork(void);

fork函数向子进程返回0,向父进程返回子进程的id,如果失败,返回-1,并设置errno。

示例:

ret = fork();
if(ret == 0)
{
... // 此处是 进
代 分支
}
else if(ret > 0)
{
... // 此处是父进
代 分支
}
else
{
... // fork 失败,
error handle
}

ps:注意这里的子进程对内存的写时拷贝技术。子进程执行exec时,关闭父进程打开的文件。

ps:task 与file的关系 4.3.3

daemon进程

int daemon(int nochdir, int noclose);

第1个参数nochir:为0则将当前目录切换到根目录/,为1保持当前目录不变

第2个参数noclose:为0 将标准输入、输出、错误重定向到/dev/null,为1保持不变

一般情况下,这两者都是0.

daemon进程是在后台执行,不予任何终端相关联,生命周期很久的进程。

ps:思路一般是先通过fork出子进程,然后用子进程执行setsid,以及响应的设置。这里有个double-fork magic。

进程的终止exit

void _exit(int status);
void exit(int status);

_exit函数中status参数定义进程的终止状态,父进程可以通过wait()来获取该状态值。

exit函数最后也会调用_exit,但它还会进行一些清理操作,关闭打开的流,所有缓存的数据写入,删除tmpfile等。

等待子进程wait

pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
int waitid(idtype_t idtype, id_t id,siginfo_t *infop, int options);
  • wait

    pid_t wait(int *status);
    

    wait成功时,返回已退出子进程的进程id;失败时返回-1,并设置errno。常见的error如下:

    errno说明
    ECHILD调用进程时发现没有子进程需要等待
    EINTR函数被信号中断

    wait调用时,如果没有子进程退出,会陷入阻塞。

  • waitpid

    pid_t waitpid(pid_t pid, int *status, int options);
    

    waitpid与wait返回值相同,status相同,但多了pid,有了精准等待的能力,根据pid不同,其含义如下:

    pid说明
    pid > 0表示等待进程id为pid的子进程
    pid = 0表示等待与调用进程同一进程组的任意子进程
    pid = -1表示等待任意子进程,与wait类似
    pid < -1等待所有子进程中,进程组id与pid绝对值相等的所有子进程

    option标志位如下:

    options说明
    WUNTRACE关心终止子进程信息,也关心因信号而停止的子进程信息
    WCONTINUED关心终止子进程信息,也关心因信号而恢复执行的子进程信息
    WNOHANG指定的子进程未发生状态变化,立即返回,不会阻塞。这种情况返回0

    status是一个int类型的指针,可以传递NULL,表示不关心子进程的状态信息,如果不为空,则根据status值,可以获取到子进程的更多信息。子进程退出有:正常退出、被信号所杀、被信号停止、被信号恢复等几种形式。

    Linux提供了很多宏来判断

    退出方式说明
    WIFEXITED(status)正常退出如果正常退出,返回true、反之false
    WEXITSTATUS(status) 如果正常退出,获取进程的退出状态
    WIFSIGNALED(status)被信号杀死如果被信号杀死,返回true、反之为false
    WTREMSIG(status) 如果被信号杀死,返回该信号
    WCOREDUMP(status) 如果子进程产生了core dump,返回true
    WIFSTOPPED(status)被信号停止如果被信号暂停,返回ture,反之返回false
    WSTOPSIG(status) 如果处于停止状态,返回暂停它的信号
    WIFCONTINUED(status)被信号恢复如果被信号恢复,返回true
  • waitid

    int waitid(idtype_t idtype, id_t id,siginfo_t *infop, int options);
    

    waitid 是等待最重要的函数,waitpid无论用户是否关心子进程的终止,终止事件都会返回用户。waitid提供了更精准的等待。

    第1、2个参数用户选择用户等待的子进程。idtype=P_PID精准等待某个id;idtype=P_PGID等待所有子进程中进程组id为id的进程;idtype=P_ALL等待任意子进程。

    第4个参数也了改变:WEXITED等待子进程的终止事件,WSTOPPED等待信号暂停的子进程事件,WCONTINUED等待被恢复执行的子进程。这几个标志位或计算可以等待多个。除此之外还有WNOHANG(不阻塞)、WNOWAIT(只获取状态,不给子进程收尸)。

    第3个参数inop是一个返回值。如果成功获取,返回进程的pid、uid,以及进程的退出方式等等。

exec家族

int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[],char *const envp[]);

exec的6个函数都是建立在ececve系统调用之上的,该系统调用的作用是,将新程序加载到进程的地址空间,丢弃旧有的程序,进程的栈、数据段、堆会被新程序替换。

名字中有l表示采用参数列表(list),名字中有v标志参数数组(vector),名字中有p表示可以使用环境遍历PATH,名字中有e表示自己维护环境遍历。

先来看看execve:

int execve(const char *path, char *const argv[],char *const envp[]);

path为要执行文件的路径,argv是参数,与main函数的第2个入参相同,字符串指针数组,envp也是字符串指针数组,指向的字符串的格式为:name=value。

一般来说execve()函数总数跟着fork函数之后,表示抛弃父进程的程序段,和父进程分道。

execve有个特点,如果成功,不会返回,如果没有成功返回-1,并设置errno。

execve第1个参数必须是绝对路径或者是相对当前目录的路径(带p可以使用PATH环境变量),比如ls,需要写/bin/ls,第3个参数是环境变量指针,需要书写大量的“key=value”(不带e可以使用环境变量)。

比较而言execlp调用起来会最方便一些。

system函数

int system(const char *command);

需要执行命令作为command参数传入,最大的好处是使用方便,不需要自己调用fork,exec、waitpid,也不需要自己处理错误、处理信号。缺点是效率低一些,它会创建shell进程,然后再执行相关命令进程。

4. 信号

简介

信号的产生主要有3种:

  • 硬件异常
  • 终端相关的信号
  • 软件事件相关的信号

信号分成2类:不可靠信号,信号值在[1,31]之间的信号;可靠信号[SIGRTMIN, SIGRTMAX],不可靠信号是从Unix集成而来,所谓不可靠指的是发送的信号,内核不一定能传递给目标进程,信号可能会丢失。

信号的默认处理函数包括:

  • 显示的忽略信号ignore

  • 终止进程terminate

  • 生成core文件并终止进程core

  • 停止进程stop

  • 恢复进程continue

  • 信号的安装

    typedef void (*sighandler_t)(int);
    sighandler_t signal(int signum, sighandler_t handler);
    
    int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
    struct sigaction {
    void(*sa_handler)(int);
    void(*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t sa_mask;
    int sa_flags;
    void(*sa_restorer)(void);
    };
    

    signal是对指定的信号signum,设置其回调函数handler,这个hander的入参就是信号值。

    sigaction提供了更精准的控制

    sa_handler是信号的处理函数,

    sa_mask是信号处理函数期间的屏蔽信号集,

    sa_flags标志如下:

    • SA_NOCLDSTOP,这个标志只用于SIGCHLD信号。父进程可以监控子进程状态3个事件:终止、停止、恢复,一旦父进程为SIGCHLD设置了这个标志位,那么子进程的停止、恢复就无须向父进程发送SIGCHLD信号了。
    • SA_NOCLDWAIT,这个标志也只用于SIGCHLD信号,如果父进程为SIGCHLD设置了这个信号,子进程退出就不会进入僵尸状态,而是直接自行了断。
    • SA_ONESHOT和SA_RESETHAND,表示信号处理函数是一次性的,信号处理完之后,就换成默认的处理函数了。
    • SA_NODEFER和SA_NOMASK,在信号处理函数执行期间,不阻塞当前信号。
    • SA_RESTART,如果系统中断被信号中断,则不返回错误,而是自动重启系统调用
    • SA_SIGINFO,表示信号发送者会提供额外的信息,这时候信号的处理函数应该是3个参数的void handle(int, siginfo_t *, void *)

信号的发送kill与sigqueue

int kill(pid_t pid, int sig);  	// 一般与signal连用
int tkill(int tid, int sig);		// 给线程发,系统调用
int tgkill(int tgid, int tid, int sig);	// 给线程发,系统调用
int raise(int sig);	
int sigqueue(pid_t pid, int sig, const union sigval value);		// 一般与sigaction连用
  • kill

    int kill(pid_t pid, int sig);  	// 一般与signal连用
    

    kill向指定进程、指定进程组发送信号。这个pid与waitpid有些类似

    pid说明
    pid > 0发送给进程id等于pid的进程
    pid = 0发送信号给调用进程所在同一个进程组的每一个进程
    pid = -1
    pid < -1向进程组-pid发送信号

    Kill的成功返回0, 失败返回-1,并设置errno。常见错误包括:

    errno说明
    EINVAL无效的信号值
    EPERM该进程没有权限发送信号给目标进程
    ESRCH目标进程或进程组不存在

    示例:

    if(kill(3423,SIGUSR1) == -1)
    {
    /*error handler*/
    }
    
  • tkill与tgkill

    tkilltgkill都是向某个线程发送,它俩都是系统调用,glibc没有封装,采用syscall方式调用

    ret = syscall(SYS_tkill,tid,sig)
    ret = syscall(SYS_tgkill,tgid,tid,sig)
    

    tkill相对tgkill是过时的,tgkill的第一参数tgid,为线程组中主线程的线程id(进程号),起到保护作用,防止向错误的线程发送信号(线程id复用)。

  • raise

    int raise(int sig);	
    

    向进程自身发送信号。对与进程相当于kill(getpid(), sig);,对于多线程相当于pthread_kill(pthread_self(),sig)。信号处理函数执行完毕后才返回。

  • sigqueue

    int sigqueue(pid_t pid, int sig, const union sigval value);		// 一般与sigaction连用
    

    与kill类似,但不能向进程组发送信号。但它有一个payload参数,能制定一点伴随的数据。所谓一点是sigval是一个联合体,如下

    union sigval {
    int sival_int;
    void *sival_ptr;
    };
    

    由于指针多个进程的地址空间各自独立,传递也就没有意义。

    sigqueue与sigaction是搭档,携带的sigval可以通过sigaction来获取到。

等待信号sigsuspend与sigwait

int pause (void);
int sigsuspend(const sigset_t *mask);

int sigwait(const sigset_t *set, int *sig);
int sigwaitinfo(const sigset_t *set, siginfo_t *info);
int sigtimedwait(const sigset_t *set, siginfo_t *info, const struct timespec *timeout);
  • pause

    pause函数将调用线程挂起,使进程进入可中断的睡眠状态,指导传递一个信号为止。信号若执行信号处理函数pause会在执行完后返回,若是终止进程,pause就不返回了,若是内核发出的信号被忽略,进程不会被唤醒。

    pause不能直接指定等待某个信号,即使先通过sigprocmask进行忽略,也存在非原子性,可能造成一致等待不到的情况。

  • sigsuspend

    sigsuspend 函数解决了这个问题,它通过mask指向的掩码设置进程阻塞掩码,并将进程挂起,指导捕捉到信号为止,一旦信号返回,sigsuspend函数会把进程的阻塞掩码恢复为调用之前老的阻塞掩码。

    示例:

    static volatile sig_atomic_t sig_received_flag = 0;
    sigset_t mask_all, mask_most, mask_old;
    
    int signum = SIGUSR1;
    sigfillset(&mask_all);
    sigfillset(&mask_most);
    sigdelset(&mask_most,signum);
    sigprocmask(SIG_SETMASK,&mask_all,&mask_old);
    /* 在 sigprocmask阻塞所有信号之前, SIGUSR1 可能已经 */
    if(sig_received_flag == 0)
    sigsuspend(&mask_most);
    sigprocmask(SIG_SETMASK,&mask_old,NULL);
    
  • sigwait

    sigwait可以更简单的等待某个特定信号的到来,信号集sigset是进程关心的信号。当调用sigwait系列函数时,内核会查看进程的信号挂起队列,检查set中是否有信号处于挂起状态,如果有就立即返回,没有就陷入阻塞。

    sigwait调用成功会返回0,失败-1。sigwaitinfo是加强版的sigwait,调用成功返回信号的值(signo),而不是0,失败返回-1.sigtimedwait与sigwaitinfo类似,它约定一个timeout时间,到时候后不来,就不再等待了。

    sigwait系列函数引入的竞争,正常信号处理流程,会从信号挂起队列中摘取信号传给进程,而sigwait也会从信号挂起队列中摘取信号。所以调用之前可以先将set中的信号阻塞,从而拿到防止竞争。

    示例:

    int sigusr1 = 0;
    sigemptyset(&mask_sigusr1);
    sigaddset(&mask_sigusr1,SIGUSR1);
    sigprocmask(SIG_SETMASK,&mask_sigusr1,NULL);
    while(1)
    {
    sig = sigwaitinfo(&mask_sigusr1,&si);
    if(sig != -1)
    {
    sigusr1_count++;}
    }
    

通过fd等待信号signalfd

int signalfd(int fd, const sigset_t *mask, int flags);

描述:signalfdsigwaitinfo类似,属于同步等待信号范畴,都需要先调用sigprocmask将关注的信号屏蔽,以防止被进程的信号处理函数劫走。不同之处在于signalfd提供了文件系统的接口,可以通过select、poll、epoll来监控。

第1个参数fd,初次创建时,fd参数应该是-1,该函数会创建一个文件描述符,用于读取mask中到来的信号。如果fd不是-1,一般是修改mask的值,此时fd就是指点调用signalfd时返回的值。

第2个参数mask参数表示关注的信号集合,在调用前先用sigprocmask阻塞这些信号。

第3个参数flags用来控制行为,目前支持的标志如下:

  • SFD_CLOEXEC,与O_CLOEXEC一样,调用exec函数时,文件描述符会被关闭。
  • SFD_NONBLOCK,控制将来的读取操作,非阻塞读取。

创建完成后,使用read函数来读取到来的信号。提供的缓冲区大小一般要足以放下一个signalfd_siginfo结构体。

示例:

sigprocmask(SIG_BLOCK,&mask,NULL);
sfd = signalfd(-1,&mask,NULL);
for(;;)
{
n = read(sfd,&fd_siginfo,sizeof(struct signalfd_siginfo));
if(n != sizeof(struct signalfd_siginfo))
{
	/*error handle*/}
else{
	/*process the signal*/}
}

信号异步安全

信号的处理函数是对当前进程的一种中断,同一进程中出现2条执行流。

进程收到信号的执行流

这与嵌入编写回调函数相同,信号处理函数应该是轻量级的,快进快出的。比较常见的做法,信号处理函数非常短,基本就是设置标志位,然后由主程序根据标志位进程处理。另外,可以通过sigwait、signalfd变成同步方式。

异步的示例伪代码如下:

volatile sig_atomic_t get_SIGINT = 0;
/* 信号处理函数 */
void sigint_handler(int sig)
{
    switch(sig){
    case SIGINT:
            get_SIGINT = 1;
          	break;
	...
}
/* 主 序 是循环 */
while(true)
{
    if(get_SIGINT==1)
    {
   		 /* 在主 序处理 SIGINT*/
    }
    job = get_next_job();
    do_single_job(job);
}

5. 线程

简介

进程之间彼此地址空间是独立的,单线程会共享内存地址空间。

多线程的地址空间

POSIX线程库的接口:

POSIX函数函数功能描述
pthread_create创建一个线程
pthread_exit退出线程
pthread_self获取线程ID
pthread_equal检查两个线程ID是否相等
pthread_join等待线程退出
pthread_detach设置线程状态为分离状态
pthread_cancle线程的取消
pthread_cleanup_push线程退出,清理函数的注册
phtread_cleanup_pop线程退出,清理函数的执行

线程的id与pid类似的是tid

线程的创建pthread_create

#include <pthread.h>
int pthread_create(pthread_t *restrict thread,						//  创建成功,返回线程ID
										const pthread_attr_t *restrict attr,		// 定制线程的属性,栈大小、调度策略等
										void *(*start_routine)(void*),		// 线程要执行的函数
										void *restrict arg);			// start_routine的入参

成功返回0,不成功返回非0的错误码。

返回值描述
EAGAIN系统资源不够,或创建线程个数超限
EINVAL第二个参数attr不合法
EPERM没有合适的权限来设置调度策略

示例:

void * thread_worker(void *)
{
    printf( “ I am thread worker ” );
    pthread_exit(NULL)
}
pthread_t tid ;
int ret = 0;
ret = pthread_create(&tid,NULL,&thread_worker,NULL);
if(ret != 0)/*此处,不能用 ret < 0 作为出 判断 */
{
    /*ret is the errno*/
    /*error handler*/
}

这里的pthread_t不是线程的tid,它是NPTL线程库的范畴,线程库的后续操作,就是根据该线程ID来操作的。phread_t本质是分配在mmp区域的一个内存地址,可以通过pmap PID查看进程的地址空间情况。

可以给线程起一个有意义的名字,命名之后可以从procfs或者ps中看到线程的名字。通过proctl系统调用可以完成。

创建线线程时,第2个参数若传入NULL,则会创建默认属性的线程,这里可以设置的属性挺多,需要主要线程栈的大小(stack size)可以通过ulimit -s查看该大小,默认是8MB(8192)

线程的退出pthread_exit

#include <pthread.h>
void pthread_exit(void *value_ptr);

value_ptr存放线程的遗言,如果没有可以直接传递NULL。注意该指针不能指向线程自己栈上的变量。

线程的连接pthread_join

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);

第1个参数是要等待的线程id,第2个参数用来接收返回值。

既然线程退出可以有返回值,那么就需要去获取该值,这里就有pthread_join函数,用来等待某线程的退出,并接收它的返回值。

有点像wait子进程,线程被成为join。但wait进程只能是父进程等子进程,而线程的join可以是该进程组中任一指定线程。

返回值说明
ESRCH传入线程ID不存在,无此线程
EINVAL已有其他线程连接该线程了
EDEADLK死锁

如果不连接已经退出的线程,会导致资源无法释放(栈空间)。

线程的分离pthread_detach

#include <pthread.h>
int pthread_detach(pthread_t thread);

入参是线程id,NPTL提供了该函数用于将线程设置为detached状态,处于该状态的线程退出时,系统将负责回收线程的资源。可以是线程组内其他线程对目标线程进程分离,也可以是线程自己执行该函数进行分离。

另外可以在创建线程时,将线程属性设定为已分离状态。

#include <pthread.h>
int pthread_attr_setdetachstate(pthread_attr_t *attr,int detachstate);
int pthread_attr_getdetachstate(pthread_attr_t *attr,int *detachstate);

detachstate可以是PTHREAD_CREATE_DETACHED表示创建出来就是分离状态,模式情况下是PTHREAD_CREATE_JOINABLE 可连接状态。

互斥量mutex

说分不说合,等于耍流氓。多线程的合作就是线程的同步问题。

#include <pthread.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;		// 静态初始化方法
int pthread_mutex_init(pthread_mutex_t *restrict mutex,		// 动态初始化方法
const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);		// 互斥量销毁

int pthread_mutex_lock(pthread_mutex_t *mutex);			   // 加锁,阻塞
int pthread_mutex_trylock(pthread_mutex_t *mutex);		// 加锁,非阻塞
int pthread_mutex_unlock(pthread_mutex_t *mutex);		// 解锁

动态初始化方法第2个参数设置互斥量的属性,一般传入NULL,采用默认属性。调用之后处于未加锁状态。

对于已经持某个锁的线程,再次申请该锁,根据不同属性会有不同的处理,有时会死锁(PTHREAD_MUTEX_NORMAL),有时会返回错误(PTHREAD_MUTEX_ERRORCHECK),有时会申请成功(PTHREAD_MUTEX_RECURSIVE)。注意:互斥量并不能做到先锁的先得到锁。

这里还有一种互斥锁的类型PTHREAD_MUTEX_ADAPTIVE_NP,它是互斥锁与自旋锁的结合。所谓自旋锁,就是在没有申请到锁之后,并不阻塞,而是一致尝试加锁,知道成功为止,但这样也有缺点,所以有了结合普通互斥锁与自旋锁的这种锁。

读写锁rwlock

#include <pthread.h>
pthread_rwlock_t rwlock=PTHREAD_RWLOCK_INITIALIZER;		// 静态初始化方法
int pthread_rwlock_init(pthread_rwlock_t *rwlock,const pthread_rwlockattr_t *attr); // 动态初始化方法
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);		// 销毁

//  读锁接口
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_timedrdlock(pthread_rwlock_t *rwlock,const struct timespec *abstime);

// 写锁接口
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_timedwrlock(pthread_rwlock_t *rwlock,const struct timespec *abstime);

读写锁创建时,第2个参数是pthread_rwlockattr_t,它可以设置锁的竞争策略,默认是读者优先,意思是当前是状态是读锁,如果线程申请读锁,此时纵然有写锁在等待队列上,仍然允许申请者获取读锁,而不是被写锁阻塞。另外一种是写者优先,就是阻塞再次的读。

读锁是共享锁,可以与其他读共享,但不与写锁共享;写锁是独占锁,不允许其他读、写操作。

当前锁状态读锁请求写锁请求
无锁OKOK
读锁OK阻塞
写锁阻塞阻塞

比较适合读写锁的场景是:临界区比较大,大多数是读,少数是写。

条件等待cond

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
int pthread_cond_destroy(pthread_cond_t *cond);

// 等待
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);

// 发送
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);

pthread_cond_init的第2个参数可以为NULL,表示采用默认属性。

生产者-消费者模型中,如果任务队列为空,则消费者线程应该停工等待,一直到队列不空为止。条件等待是这样:线程在条件不满足时,主动让出互斥量,并进入阻塞等待,一旦条件满足,线程就立即唤醒。线程依赖其他线程的操作,它确信有一个线程在发现条件满足后,将向它发信号,并让出互斥量。

条件等待是需要跟互斥量连用的,等待示例如下:

pthread_mutex_lock(&m);
while(condition_is_false)
	pthread_cond_wait(&v,&m);// 此处阻塞
/* 如果代码运行到此处,则表示等待的条件已经满足 ,
* 并且在此持有斥量 *
/* 在满足条件情况下,做你想做的事情*/
...
pthread_mutex_unlock(&m);

发送示例如下:

pthread_mutex_lock(&m);
/*一些对共享数据的操作,会捯饬另一个线程等待条件满足*/
...
// 此处也可以是 pthread_cond_broadcast 函数
pthread_cond_signal(&cond);
pthread_mutex_unlock(&m);

线程取消cancle、cleanup

int pthread_cancel(pthread_t thread);
int pthread_setcancelstate(int state, int *oldstate);

一个线程向另外一个线程发送取消请求,该函数非阻塞,成功后立即返回0,否则返回错误码。

线程可以设置本线程是否可以被取消,PTHREAD_CANCEL_ENABLE(默认),PTHREAD_CANCEL_DISABLE

void pthread_cleanup_push(void (*routine)(void *),void *arg);
void pthread_cleanup_pop(int execute);

线程可以设置一个或多个清理函数,线程取消或退出时,会自动执行这些清理函数,以确保资源安全。以上2个函数要同时使用,并且在一个代码块中。这两个函数是用宏来实现的。

示例:

void *thread(void *param)
{
int input = (int)param;
printf("thread start\n");
pthread_cleanup_push(clean,"first cleanup handler");
pthread_cleanup_push(clean,"second cleanup handler");
/*work logic here*/
if(input != 0){
/*pthread_exit 退出,清理函数总
*/
pthread_exit((void*)1);
}
pthread_cleanup_pop(0);
pthread_cleanup_pop(0);
/*return 返回, 如果上面 pop 函数的参数是0,则不执行清理函数 */
return ((void *)0);
}

线程与信号

前面说的线程共享信号处理函数,但独享自己的信号掩码。

#include <signal.h>
// 设置掩码
int pthread_sigmask(int how, const sigset_t *new, sigset_t *old);

//  想线程发送信号
int pthread_kill(pthread_t thread, int sig);
int pthread_sigqueue(pthread_t thread, int sig, const union sigval value);

pthread_sigmask的第1个参数:

  • SIG_BLOCK向当前掩码中添加新的掩码
  • SIG_UNBLOCK从当信号掩码中删除
  • SIG_SETMASK将当前掩码替换成new

2个发送函数跟相应的进程发送函数类似,这里不多解释了。

另外:尽量少在多线程中使用信号,更不要在多线程中使用fork。

# Linux 

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×